Repository navigation
MCPServer has no x-mcp-header declaration mechanism and never validates one, so an invalid annotation is served happily and dropped by every client #3484
Description
Activity
Re-checked against the v2.2.0 tag — unchanged, so this is not stale.
(Correcting my earlier comment here, which pasted the checklist from the client-side issue #3483. Only its last line was about this one. The server-side facts:)
src/mcp/server/still contains nox-mcp-headeranything — no declaration mechanism, andFastMCP/MCPServertool registration does not look for one.find_invalid_x_mcp_headeris still insrc/mcp/shared/inbound.pyand is still imported by the modern streamable-HTTP server transport, where it validates the annotations on an incoming request's schema. It is still never run against a tool the server itself registers, which is the gap: an invalid annotation is accepted at registration and only rejected later, by clients.- So the reproduction in the issue body stands on 2.2.0: an
x-mcp-headeron anarray-typed parameter registers, starts and serves without complaint, and every conforming client drops the tool.
#3442 in that release does touch SEP-2243, but it adds conformance fixtures for the resource/prompt
Mcp-Nameheaders rather than anything on this path.I traced the current main implementation and confirmed the smallest fix boundary:
- ToolManager.add_tool() builds the Tool and stores it without validating tool.parameters.
- find_invalid_x_mcp_header() already walks the relevant JSON Schema positions and is already used by the client-side listing filter.
- The server-side registration path can reuse that validator before storing the tool, turning an invalid x-mcp-header into a local registration error instead of serving a tool every conforming client drops.
I think this should stay scoped to registration-time validation; a first-class annotation API is a separate design question. The regression should cover an array-typed parameter with x-mcp-header and confirm registration fails, while valid string/integer/boolean annotations remain accepted.
AI assistance was used for repository investigation and this implementation analysis; I reviewed the source and the proposed scope.
Thanks — I checked all four points against the
v2.2.0tag andmain, and they hold. Three things to add, plus one argument for the fix that I think strengthens the case.Confirmations.
ToolManager.add_tool(tool_manager.py:39) builds viaTool.from_functionand stores at:66with nothing in between;Tool.parameters(base.py:41) is the JSON Schema; andserver.py:515serves it verbatim asinput_schema=info.parameters— so validatingtool.parametersat registration validates exactly the object a client receives, with no transformation in between.1.
add_toolis not the only door.ToolManager.__init__(tools=...)(tool_manager.py:23-27) insertsToolobjects straight intoself._tools, bypassingadd_toolentirely. A check placed only inadd_toolleaves that path open. A single private helper both call sites use covers it.Separately, the lowlevel
Server(on_list_tools=...)path never goes throughToolManagerat all — the author returnsToolobjects themselves. I do not think that needs to be in scope, but it is worth saying out loud that the guarantee isMCPServer-scoped rather than server-wide, so nobody reads the fix as stronger than it is.2. The predicate to reuse is the client's drop condition, not the SEP's table.
find_invalid_x_mcp_headeris strictly stronger than the three MUST rows — it also enforces the RFC 9110 token shape, case-insensitive uniqueness, and reachability via a purepropertieschain. That extra strictness is the point rather than over-reach:client/session.py:1301calls the same function, so rejecting exactly what it rejects gives the invariant "the server never serves a tool a conforming client would drop." Worth stating in the PR so the extra conditions do not later get trimmed back to the table.3. A mis-annotated tool is not only invisible — it is also unvalidated.
validate_mcp_param_headersopens withif find_invalid_x_mcp_header(input_schema) is not None: return None(inbound.py:543), and the docstring says so deliberately: "A schemafind_invalid_x_mcp_headerrejects validates nothing: conforming clients drop the tool and emit no headers." That reasoning is sound for conforming clients, but it means that today a tool with a bad annotation gets zero header/body consistency checking when a non-conforming client does call it. Registration-time rejection closes that too, which I think makes this more than an ergonomics fix.On the regression test, I would add the
ToolManager(tools=[...])constructor path alongside the array-typed case, and a duplicate-token case — the latter is the one where reusing the shared validator visibly buys something the SEP table would not.Happy for you to take this — you have clearly done the tracing. I will stay out of the way unless you would rather I open it.
- addedv2Affects the v2 line (2.x on main)Affects the v2 line (2.x on main)spec-2026-07-28Concerns the SDK's implementation of the 2026-07-28 MCP spec revisionConcerns the SDK's implementation of the 2026-07-28 MCP spec revision
on Sep 10, 2026 The silent failure here is fixed in #3620, thanks for the thorough report.
Registering a tool on
MCPServernow runs the samex-mcp-headercheck the client uses, and raisesInvalidSignaturenaming the tool and the problem. There's also a new Header parameters page in the docs.The first-class marker (
McpHeader(...)) isn't included, to keep this a small fix with no new public API. A hand-builtTool(parameters=...)or a low-levelServeris still unchecked.If either of those gaps matters for you, a new issue is welcome.
Summary
MCPServeroffers no way to mark a tool parameter withx-mcp-header, and does not validate the annotation when one is smuggled in through pydantic. A server author who gets it wrong gets no signal at all: the tool is served happily, and every conforming client silently drops it.SEP-2243's Reference Implementation section names this as a server-SDK requirement:
The client half is implemented. The server half is not:
x-mcp-headerappears inmcp/client/session.py,mcp/shared/inbound.pyandmcp_types/_v2026_07_28/, and nowhere undermcp/server/.1. No declaration mechanism
The only route is pydantic passthrough:
This works — the annotation reaches
inputSchema, the client mirrors it,mcp/shared/inbound.pyvalidates it — so this is an ergonomics and discoverability gap rather than a functional one. But it means the feature is invisible from the server API, and that a server author must know the extension keyword's exact spelling from the spec.2. Nothing validates the declaration server-side
This is the part that fails silently. SEP-2243 puts type restrictions on
x-mcp-headerand assigns their enforcement to the server:MCPServerrejects none of them.Output on
mcp2.1.1:Registration succeeded, startup succeeded,
tools/listserved it. The client — correctly, per the client-side MUST — drops it. So the failure mode is a tool that exists on the server and is invisible to every client, with the only diagnostic emitted in the client's process, which in a real deployment belongs to someone else.The validator that would catch this already exists and is already imported by the server package's transport:
find_invalid_x_mcp_headerinmcp/shared/inbound.py. It is simply never run against a tool the server itself is registering.Suggested fixes
find_invalid_x_mcp_headerat tool-registration time and raise. This is the one that matters: it turns a silent cross-process failure into an error at the line that caused it, and it reuses code that is already there.Annotated[str, McpHeader("Region")].Happy to open a PR for (1) if the direction is agreeable.
Environment
mcp2.1.1,mcp-types2.1.1, Python 3.12.92026-07-28