Skip to content

Support routable transport headers for gateways (SEP-2243) - #4622

Merged
jlowin merged 2 commits into
mainfrom
feature/sep2243-routable-headers
Jul 26, 2026
Merged

Support routable transport headers for gateways (SEP-2243)#4622
jlowin merged 2 commits into
mainfrom
feature/sep2243-routable-headers

Conversation

@jlowin

@jlowin jlowin commented Jul 24, 2026

Copy link
Copy Markdown
Member

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-Method on every request, Mcp-Name on named operations (tool/prompt name or resource URI), and Mcp-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/Origin request guard inspects only those two headers, so the routing headers reach a gateway exactly as the client sent them. The one authoring hook — the x-mcp-header JSON Schema annotation that opts a parameter into an Mcp-Param-* header — already flows through FastMCP's Pydantic-based schema generation via json_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.

from typing import Annotated
from pydantic import Field
from fastmcp import FastMCP

mcp = FastMCP("My Server")

@mcp.tool
def query_tenant(
    tenant: Annotated[str, Field(json_schema_extra={"x-mcp-header": "Tenant"})],
    sql: str,
) -> str:
    """A call to this tool sends `tenant` as an `Mcp-Param-Tenant` header a gateway can route on."""
    ...

The added tests assert that Mcp-Method, Mcp-Name, and Mcp-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.

@jlowin jlowin added the v4 label Jul 24, 2026

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 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".

Comment thread docs/deployment/http.mdx
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.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 Badge 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 👍 / 👎.

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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_map

New 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.

Comment thread docs/deployment/http.mdx Outdated
Comment on lines +154 to +156
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.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge 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 👍 / 👎.

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

@jlowin jlowin added the enhancement Improvement to existing functionality. For issues and smaller PR improvements. label Jul 26, 2026
@jlowin
jlowin merged commit 2f992f7 into main Jul 26, 2026
18 of 20 checks passed
@jlowin
jlowin deleted the feature/sep2243-routable-headers branch July 26, 2026 17:40
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

enhancement Improvement to existing functionality. For issues and smaller PR improvements. v4

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant