Support routable transport headers for gateways (SEP-2243) - #4622
Conversation
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: 42ebfe7e25
ℹ️ About Codex in GitHub
Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you
- Open a pull request for review
- Mark a draft as ready
- Comment "@codex review".
If Codex has suggestions, it will comment; otherwise it will react with 👍.
Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".
| A gateway can now route on `Mcp-Param-Tenant` — for example, pinning each tenant to a dedicated backend — without inspecting the request body. The annotation is only permitted on `string`, `integer`, and `boolean` parameters. These headers advertise routing intent; treat them as untrusted hints, since the server still validates the request body as the source of truth. | ||
|
|
||
| <Tip> | ||
| When you put a FastMCP [proxy](/servers/proxy) in front of another server, the proxy re-advertises each backend tool's `x-mcp-header` annotation, so routing headers work across the proxy hop as well. The headers themselves are regenerated per hop rather than forwarded verbatim, since each describes a single HTTP request. |
There was a problem hiding this comment.
Fix routing headers across the HTTP proxy hop
With a Streamable HTTP backend, this documented proxy scenario fails rather than routing successfully: ProxyTool.run() creates a fresh backend client and calls client.session.call_tool() without first listing tools, so the SDK has not cached the advertised x-mcp-header annotation and omits Mcp-Param-Tenant; the modern backend then rejects the call with HEADER_MISMATCH because the body contains tenant without its required header. The added proxy test uses an in-process backend and only checks the re-advertised schema, so it never exercises the validating HTTP hop; add an HTTP round-trip test and ensure the backend session absorbs or lists the tool schema before calling it.
AGENTS.md reference: AGENTS.md:L146-L152
Useful? React with 👍 / 👎.
There was a problem hiding this comment.
Confirmed and fixed in 5be0c22.
You're right — this reproduces over a real HTTP hop. A modern Streamable-HTTP backend rejects the proxied call with HEADER_MISMATCH ("Mcp-Param-Tenant header is missing but the request body's 'tenant' argument is present"), because ProxyTool.run() calls client.session.call_tool() directly and the SDK only emits Mcp-Param-* for tools it has cached via list_tools. My earlier proxy test used an in-process backend and only checked the re-advertised schema, so it never exercised the validating hop.
Note the failure is gated on a modern backend: ProxyClient defaults to mode="legacy", which neither emits nor validates these headers, so the default path was unaffected — but mode="auto" against a modern backend fails.
The fix seeds the backend session's header map from the tool's already-mirrored input schema before the call, using the SDK's own x_mcp_header_map, so no extra list_tools round-trip is added and an unannotated tool produces no headers:
header_map = x_mcp_header_map(self.parameters)
if header_map:
client.session._x_mcp_header_maps[backend_name] = header_mapNew test test_proxy_forwards_mcp_param_header_to_modern_http_backend puts a FastMCP proxy (mode="auto") in front of a real modern Streamable-HTTP backend with an x-mcp-header-annotated param and asserts the call routes through successfully — it reproduces the failure without the fix and passes with it.
| A gateway, load balancer, or reverse proxy in front of your MCP server often needs to route a request before it reads the JSON-RPC body — the body may be an SSE stream, or the gateway may simply want to avoid parsing it. Streamable HTTP clients built on the MCP Python SDK (including FastMCP's own client) attach routing information to each request as HTTP headers so an intermediary can dispatch on headers alone: | ||
|
|
||
| - `Mcp-Method` carries the JSON-RPC method (for example `tools/call`) on every request. |
There was a problem hiding this comment.
Gate routing-header docs on the modern protocol
For Streamable HTTP clients that negotiate the legacy protocol era (including Client(..., mode="legacy") or auto-fallback to a legacy server), the SDK's handshake stamp does not emit Mcp-Method, and Mcp-Param-* generation is only installed by the modern session stamp. Therefore the unconditional claim that these clients attach routing information to each request can lead gateways to depend on headers that are absent; qualify this section as requiring the modern 2026-07-28 protocol and document the legacy behavior.
Useful? React with 👍 / 👎.
There was a problem hiding this comment.
Good catch — fixed in 5be0c22.
The section now states up front that the headers are a feature of the modern 2026-07-28 protocol, and a new callout warns that a client on an earlier revision (including legacy mode or auto-fallback to a legacy server) sends no routing headers at all, advising gateway operators to treat the headers as optional — fall back to body inspection or a default backend rather than depending on their presence.
SEP-2243 adds routable HTTP headers so a gateway, load balancer, or reverse proxy can dispatch an MCP request without parsing its JSON-RPC body:
Mcp-Methodon every request,Mcp-Nameon named operations (tool/prompt name or resource URI), andMcp-Param-*for selected arguments. The MCP Python SDK v2 already implements both ends of this on the wire — the client session emits the headers and the modern streamable-HTTP server transport validates them — so the framework's job is to stay out of the way and expose the authoring surface.That is exactly what this PR verifies and documents. FastMCP's HTTP transport passes the ASGI request through to the SDK's session manager untouched, and the
Host/Originrequest guard inspects only those two headers, so the routing headers reach a gateway exactly as the client sent them. The one authoring hook — thex-mcp-headerJSON Schema annotation that opts a parameter into anMcp-Param-*header — already flows through FastMCP's Pydantic-based schema generation viajson_schema_extra, and the proxy re-advertises it verbatim, so it works across a proxy hop too. No new machinery was needed; the change is regression coverage plus a gateway-routing docs section.The added tests assert that
Mcp-Method,Mcp-Name, andMcp-Param-*survive a round trip through a FastMCP HTTP server (including with the Host/Origin guard enabled) and that the proxy preserves the annotation. Suggested labels:enhancement,server.