anthropics / anthropics/claude-code

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

Open
#95,504 1 comment 0 reactions 0 assignees View on GitHub

Nobody has claimed this yet.

api:anthropic area:mcp bug has repro platform:macos
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 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.
  2. 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).
  2. 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.

  2. 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.

Contributor guide

No contributing guide indexed for this repository

First steps

  1. Read the whole issue, then the project's contributing guide.
  2. Comment on the issue to say you are picking it up — it saves two people doing the same work.
  3. Fork the repository and make your change on a branch.
  4. 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

Get new issues in your inbox

A short digest of beginner-friendly GitHub issues.