Look the tool schema up by name for Mcp-Param-* validation instead of running tools/list
Merged
MA
1mo ago+229+229+012345678901234567890123456789-16-16-01234567890123456789
Fixes #3565.
What was wrong
On the 2026-07-28 Streamable HTTP path, a tools/call that carries arguments is checked against the called tool's x-mcp-header declarations before dispatch. To find that tool's input schema, the server ran its own tools/list handler, page by page, on every such call.
- A server whose listing is expensive (an aggregating gateway, a large catalog) paid for a full listing per call.
- A slow listing delayed the call, and a failing one logged an error for it.
- A call to an unknown tool walked every page.
What changes
-
MCPServerlooks the called tool up by name among its registered tools. Nothing to configure. -
The low-level
Servertakes a new optional keyword,get_tool_input_schema=:schemas: dict[str, dict[str, Any]] = {} # kept up to date as the gateway lists its backends server = Server( "gateway", on_list_tools=list_tools, # fans out to every backend on_call_tool=call_tool, get_tool_input_schema=schemas.get, )- It takes the tool name and returns that tool's input schema, or
Nonewhen there is nothing to validate. - When it is set, it is called instead of the
tools/listhandler. - If it raises, the error is logged and the call is served unvalidated, the same as a failed listing.
- A server that declares no
x-mcp-headeron any tool can passlambda name: None. - It is called before middleware runs, so it is not scoped to the caller.
- The value is also readable as
server.get_tool_input_schema.
- It takes the tool name and returns that tool's input schema, or
What users will notice
MCPServer:- Middleware no longer sees a
tools/listfor eachtools/call. - The schema that is validated is the registered one. Middleware that filters or rewrites
tools/listresults no longer affects validation.- A registered tool that middleware hides from a caller used to be skipped. It is now validated like any other registered tool.
- The check runs before middleware. A caller who names a hidden tool and its marked argument, without the header, is answered with the header mismatch, which names the header and the argument, rather than with whatever middleware would have answered. The tool is not run.
- The TypeScript, Go, C#, Rust and PHP SDKs also resolve the tool by name before dispatch.
- A subclass that overrides
MCPServer.list_tools()to add tools outside the registry no longer gets those tools validated.
- Middleware no longer sees a
- Low-level
Serverwithget_tool_input_schema=:- The
tools/listhandler does not run for atools/call. - Validation runs even when no
tools/listhandler is registered. Nonefrom the function skips validation for that call. It does not fall back to the listing.
- The
What is unchanged
- A low-level
Serverwithout the keyword resolves the schema throughtools/listexactly as before. - How a header is compared with the body, the 400 status and the
-32020error code. Apart from the cases above, the same requests are accepted and rejected. - A call with no arguments and no
Mcp-Param-*headers is still not validated. - Earlier protocol versions, stdio and in-memory transports. The check only exists on the 2026-07-28 HTTP entry.
Docs
docs/advanced/header-parameters.mdgains a short section, "Schemas by name", under "On the low-levelServer", with a runnable example (docs_src/header_parameters/tutorial003.py) and a recap line.docs/advanced/low-level-server.mdgains one bullet under "The other handlers" that points to that section.- The "Servers validate
Mcp-Param-*headers against the request body" section is removed fromdocs/migration.md, along with the link to it indocs/whats-new.md. The check is new in 2026-07-28, so there is nothing to migrate from v1, and the section described the internal listing this change replaces.
How it was checked
- New tests in
tests/server/test_streamable_http_modern.py:- An
MCPServertool with anx-mcp-headerargument is accepted with a matching header, rejected with a mismatched or missing one, and an unregistered tool is left to dispatch. Middleware sees only the two dispatchedtools/calls. Onmainit also sees atools/listfor each request. - A low-level server with
get_tool_input_schemagets the same outcomes and itstools/listhandler never runs. - A raising
get_tool_input_schemais logged and the call is served.
- An
- New tests in
tests/docs_src/test_header_parameters.py, over the 2026-07-28 HTTP path: the page's low-level example without the keyword dispatches atools/listbefore eachtools/call, the one with it dispatches only thetools/call, and it still rejects a mismatchedMcp-Param-Regionheader. - The existing tests for the listing path (pagination, cursor cycle, page cap, raising handler, caller envelope) pass unchanged. They cover a low-level server without the keyword.
- For every tool on the everything-server, and on a server with nested models, aliases, optional and annotated arguments, the registered schema equals the
inputSchemathattools/listreturns. - The
http-custom-header-server-validationconformance scenario passes against the everything-server. ./scripts/test(100% coverage),ruffandpyrightare clean.