Reject a tool with an invalid x-mcp-header annotation at registration
Fixes #3484.
A tool whose input schema carries an invalid x-mcp-header annotation registered on MCPServer without complaint. Clients on 2026-07-28 then leave the tool out of their tools/list result, as the spec requires of them, and the only diagnostic was a warning in the client's process. Registration now fails with an error that names the tool and the problem.
What changes
-
Tool.from_functionruns the existingfind_invalid_x_mcp_headerover the schema it just generated and raisesInvalidSignaturewhen it finds a problem:InvalidSignature: Tool 'fetch' has an invalid x-mcp-header annotation: property 'tags': x-mcp-header is only permitted on integer/string/boolean properties (got 'array') -
That covers
@server.tool(),MCPServer.add_tool,Tool.from_function, and soMCPServer(tools=[Tool.from_function(...)]). -
It is the same check the client applies before dropping a tool, so registration refuses exactly the schemas this SDK's client would drop.
-
InvalidSignatureis what registration already raises for a function that cannot be a tool as declared. -
No new names, parameters or defaults. Nothing changes on the wire for a tool that registers.
What users will notice
-
A server that registers a tool with an invalid annotation now fails at registration, which for a decorated tool is import time.
- Until now such a server started, served the tool to clients on earlier protocol versions, and lost it for clients on 2026-07-28.
-
The cases that are refused:
- an annotation on anything other than a string, integer or boolean parameter, such as
list[...]orfloat - an annotation on a parameter whose generated schema has no plain
type:str | None(pydantic renders ananyOf), anEnumor a field inside a nested model (pydantic renders a$ref) - a header name that is not an RFC 9110 token
- two parameters whose header names differ only by case
- an annotation on anything other than a string, integer or boolean parameter, such as
-
An optional header parameter can still be declared by giving the schema directly:
region: Annotated[str | None, WithJsonSchema({"type": "string", "x-mcp-header": "Region"})] = None -
On the 2026-07-28 Streamable HTTP path, a
tools/callfor a tool with an invalid annotation skipped theMcp-Param-*header check entirely. Such a tool can no longer be registered this way, so that case is gone for tools built byTool.from_function.
Not included
- A first-class marker such as
McpHeader("Region"). The annotation is still written withField(json_schema_extra={"x-mcp-header": ...}). - A
Toolbuilt by hand withTool(parameters=...)and tools returned from a low-levelServerlist handler are not checked. Both bypassTool.from_function.
How it was checked
- New tests in
tests/server/mcpserver/tools/test_base.py:- an array, a
float, astr | Noneand a non-token header name each raiseInvalidSignatureand nothing is registered - a tool annotating a
str, anintand aboolregisters
- an array, a
- The four invalid-annotation cases fail on
mainand pass with the change. - No existing test, example or docs snippet needed changing.
./scripts/testpasses with 100% coverage; ruff and pyright are clean.