Reject a tool with an invalid x-mcp-header annotation at registration

MA
22d ago
+368-1

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_function runs the existing find_invalid_x_mcp_header over the schema it just generated and raises InvalidSignature when 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 so MCPServer(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.

  • InvalidSignature is 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[...] or float
    • an annotation on a parameter whose generated schema has no plain type: str | None (pydantic renders an anyOf), an Enum or 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 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/call for a tool with an invalid annotation skipped the Mcp-Param-* header check entirely. Such a tool can no longer be registered this way, so that case is gone for tools built by Tool.from_function.

Not included

  • A first-class marker such as McpHeader("Region"). The annotation is still written with Field(json_schema_extra={"x-mcp-header": ...}).
  • A Tool built by hand with Tool(parameters=...) and tools returned from a low-level Server list handler are not checked. Both bypass Tool.from_function.

How it was checked

  • New tests in tests/server/mcpserver/tools/test_base.py:
    • an array, a float, a str | None and a non-token header name each raise InvalidSignature and nothing is registered
    • a tool annotating a str, an int and a bool registers
  • The four invalid-annotation cases fail on main and pass with the change.
  • No existing test, example or docs snippet needed changing.
  • ./scripts/test passes with 100% coverage; ruff and pyright are clean.

AI Disclaimer