anthropics / anthropics/claude-code
[BUG] MCP tools silently dropped when inputSchema uses root-level allOf/if/then, which MCP 2026-07-28 permits
Nobody has claimed this yet.
- Dominant language
- TypeScript
- Stars
- 146k
- Forks
- 23.8k
- PR merge metrics
- PR metrics pending
Description
Preflight Checklist
- I have searched existing issues and this hasn't been reported yet
- This is a single bug report (please file separate reports for different bugs)
- 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 alongsidetype— 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:
- 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. - 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:
- 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). - 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
- Save this as
repro_server.py. It is a dependency-free MCP stdio server exposing three
tools whoseinputSchemavalues are all valid JSON Schema 2020-12 and all valid against
the published MCP2026-07-28Tooldefinition:
#!/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"], {})
- 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.
- Register it and restart Claude Code:
claude mcp add schema-repro -- python3 /absolute/path/to/repro_server.py
-
Run
/mcp. The server shows as connected. -
Observed: only
flat_okandnested_oneof_okare available.conditional_droppedis
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 sendsMcp-Protocol-Version: 2025-11-25while
_meta.protocolVersionsays2026-07-28. Relevant here because it establishes that the CLI
declares2026-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.
Contributor guide
No contributing guide indexed for this repository
First steps
- Read the whole issue, then the project's contributing guide.
- Comment on the issue to say you are picking it up — it saves two people doing the same work.
- Fork the repository and make your change on a branch.
- Open a pull request that references the issue number.
Research direction
Start with the dependency-free repro_server.py and its tools/list response, then trace how Claude Code handles the three root and nested schemas after the server is added with claude mcp add. Re-run /mcp and verify that all three tools are listed, or that any dropped tool produces a warning naming the tool, server, and unsupported keyword.
Written by the indexing model from the issue text.
Assessment
- Tech stack
- python
- Domain
- api, cli
- Issue type
- Bug
- Difficulty
- 4/5
- Estimated time
- 3-5 days
- Activity status
- Active
- Clarity
- Mostly clear
- Newbie friendliness
- 55/100