[BUG] MCP tools silently dropped when inputSchema uses root-level allOf/if/then, which MCP 2026-07-28 permits

Status Open
Maintainer reply None cached
Activity 2 comments · opened Sep 19, 2026

Preflight Checklist

  • [x] I have searched existing issues and this hasn't been reported yet
  • [x] This is a single bug report (please file separate reports for different bugs)
  • [x] I am using the latest version of Claude Code

What's Wrong?

When an MCP server advertises a tool whose inputSchema uses a composition or conditional
keyword at the root of the schema (allOf, oneOf, anyOf, not, if/then/else),
Claude Code drops that tool from the tool list. The server connects, other tools from the same
server work normally, and there is no error, warning, or log entry indicating anything was
discarded.

These schemas are valid. MCP revision 2026-07-28 explicitly permits them — see
SEP-2106 (status: Final),
which landed in that revision per the
changelog (Minor changes,
item 10: "Loosen inputSchema and outputSchema to allow any JSON Schema 2020-12 keywords").

The protocol's own machine-readable schema says so directly. From
schema/2026-07-28/schema.json,
$defs.Tool.properties.inputSchema.description:

Tool arguments are always JSON objects, so type: "object" is required at the root. Beyond that, any JSON Schema 2020-12 keyword may appear alongside type — including composition keywords (oneOf, anyOf, allOf, not), conditional keywords (if/then/else), reference keywords ($ref, $defs, $anchor), and any other standard validation or annotation keywords.

Claude Code advertises MCP support (docs),
and per #93290 the CLI sends _meta.protocolVersion: 2026-07-28. A client declaring that
revision should accept these tool definitions.

Two things make this hard to diagnose:

  1. It is silent. Nothing surfaces. A conforming server appears to load successfully with a

subset of its tools. In my case 7 of 11 tools from one server appeared and 4 did not, with
no indication that the other 4 existed. I spent a long time investigating my own gateway's
authorization policy for a problem that was entirely client-side.

  1. Only the root position matters. The same keyword nested inside a property is accepted.

That asymmetry is invisible without bisecting schemas by hand.

What Should Happen?

Either of these would be fine:

  1. Accept them. Normalize the schema before sending it to the API — flatten or drop the

unsupported keywords for the API payload while keeping the tool callable. Claude Desktop
reportedly does something like this already (noted in #45106).

  1. Fail loudly. If a tool cannot be represented, keep dropping it but say so — one warning

line naming the tool, the server, and the offending keyword. Silent removal of a declared
capability is the core problem here, independent of which keywords are supported.

Error Messages/Logs

# Current behaviour: no error is produced. The tool is absent from the tool list,
# `/mcp` shows the server as connected, and nothing is logged.

# Older versions surfaced the underlying API constraint (see #45106):
API Error: 400 {"type":"error","error":{"type":"invalid_request_error",
"message":"tools.11.custom.input_schema: input_schema does not support oneOf, allOf, or anyOf at the top level"},
"request_id":"req_011CZr1kf8NdxBHLJYyB5cTT"}

Steps to Reproduce

  1. Save this as repro_server.py. It is a dependency-free MCP stdio server exposing three

tools whose inputSchema values are all valid JSON Schema 2020-12 and all valid against
the published MCP 2026-07-28 Tool definition:

#!/usr/bin/env python3
"""Minimal MCP stdio server exposing three tools with valid 2020-12 inputSchemas."""
import json
import sys

TOOLS = [
    {
        "name": "flat_ok",
        "description": "Flat schema. No composition keywords.",
        "inputSchema": {
            "type": "object",
            "properties": {"name": {"type": "string"}},
            "required": ["name"],
        },
    },
    {
        "name": "nested_oneof_ok",
        "description": "oneOf nested inside a property, not at the root.",
        "inputSchema": {
            "type": "object",
            "properties": {
                "target": {
                    "oneOf": [
                        {"type": "object", "properties": {"id": {"type": "string"}}, "required": ["id"]},
                        {"type": "object", "properties": {"slug": {"type": "string"}}, "required": ["slug"]},
                    ]
                }
            },
            "required": ["target"],
        },
    },
    {
        "name": "conditional_dropped",
        "description": "Size cap depends on mode. allOf + if/then/else at the root.",
        "inputSchema": {
            "type": "object",
            "properties": {"mode": {"type": "string"}, "size": {"type": "integer"}},
            "required": ["mode"],
            "allOf": [
                {
                    "if": {"properties": {"mode": {"const": "large"}}},
                    "then": {"properties": {"size": {"maximum": 2000000000}}},
                    "else": {"properties": {"size": {"maximum": 16000000}}},
                }
            ],
        },
    },
]


def reply(request_id, result):
    sys.stdout.write(json.dumps({"jsonrpc": "2.0", "id": request_id, "result": result}) + "\n")
    sys.stdout.flush()


for line in sys.stdin:
    line = line.strip()
    if not line:
        continue
    message = json.loads(line)
    method = message.get("method")
    if method == "initialize":
        reply(message["id"], {
            "protocolVersion": message.get("params", {}).get("protocolVersion", "2025-06-18"),
            "capabilities": {"tools": {}},
            "serverInfo": {"name": "schema-repro", "version": "1.0.0"},
        })
    elif method == "tools/list":
        reply(message["id"], {"tools": TOOLS})
    elif method == "tools/call":
        reply(message["id"], {"content": [{"type": "text", "text": "ok"}]})
    elif method == "ping":
        reply(message["id"], {})
    elif "id" in message:
        reply(message["id"], {})
  1. Confirm the server really does advertise all three tools:
printf '%s\n' \
 '{"jsonrpc":"2.0","id":1,"method":"initialize","params":{"protocolVersion":"2025-06-18","capabilities":{},"clientInfo":{"name":"t","version":"0"}}}' \
 '{"jsonrpc":"2.0","method":"notifications/initialized"}' \
 '{"jsonrpc":"2.0","id":2,"method":"tools/list","params":{}}' \
 | python3 repro_server.py

tools/list returns flat_ok, nested_oneof_ok, conditional_dropped.

  1. Register it and restart Claude Code:
claude mcp add schema-repro -- python3 /absolute/path/to/repro_server.py
  1. Run /mcp. The server shows as connected.
  1. Observed: only flat_ok and nested_oneof_ok are available. conditional_dropped is

absent, with no error anywhere.
Expected: all three are available, or an explicit message explaining why one is not.

Note that nested_oneof_ok is the control: it proves nested composition keywords are fine and
isolates the failure to the root position.

Claude Model

Not sure / Multiple models — this is independent of model.

Is this a regression?

No, this never worked

Claude Model

Not sure / Multiple models

Is this a regression?

No, this never worked

Last Working Version

n/a

Claude Code Version

v2.1.278

Platform

Anthropic API

Operating System

macOS

Terminal/Shell

Xterm

Additional Information

The schemas validate. Against the published MCP schema and the JSON Schema 2020-12
metaschema, using python-jsonschema:

flat_ok              metaschema=VALID  mcp-2026-07-28=VALID
nested_oneof_ok      metaschema=VALID  mcp-2026-07-28=VALID
conditional_dropped  metaschema=VALID  mcp-2026-07-28=VALID

Reproduce with:

import json
from jsonschema import Draft202012Validator

mcp = json.load(open("schema-2026-07-28.json"))   # from modelcontextprotocol/modelcontextprotocol
tool_schema = dict(mcp["$defs"]["Tool"]); tool_schema["$defs"] = mcp["$defs"]
v = Draft202012Validator(tool_schema)

for t in TOOLS:
    Draft202012Validator.check_schema(t["inputSchema"])   # valid 2020-12 document
    assert not list(v.iter_errors(t))                     # valid MCP 2026-07-28 Tool

The conditional is load-bearing, so flattening it is not free — it encodes a real
constraint that a flat schema cannot express:

{"mode": "large", "size": 1500000000}  -> accepted
{"mode": "small", "size": 1500000000}  -> rejected
{"mode": "small", "size": 1000000}     -> accepted

Real-world context. I hit this on an MCP gateway that fronts several upstream APIs. Its
execution tools use exactly this pattern: a per-operation payload size cap expressed as
if operation_id == X then 2 GiB else 16 MiB. Four tools on one server became invisible. The
tools were not high-risk or permission-gated — the low-risk read-only one was dropped
identically — so nothing about the failure pointed at schemas.

Related issues.

  • #45106 — same root cause, older failure mode where the API returned a 400 that made the

session unrecoverable. Closed as not planned, no maintainer response. The current behaviour
is gentler but silent, so it is arguably harder to diagnose.

  • #93290 — open, has repro. Claude Code sends Mcp-Protocol-Version: 2025-11-25 while

_meta.protocolVersion says 2026-07-28. Relevant here because it establishes that the CLI
declares 2026-07-28, the revision under which these schemas are explicitly legal.

  • #88049 — a more severe variant where one unrepresentable schema drops every tool on a server.

View original on GitHub ↗

This issue has 2 comments on GitHub. Read the full discussion on GitHub ↗