cd /news/artificial-intelligence/mcp-goes-stateless-with-headers-do-y… · home topics artificial-intelligence article
[ARTICLE · art-66978] src=agentgateway.dev ↗ pub= topic=artificial-intelligence verified=true sentiment=· neutral

MCP goes stateless with headers. Do you need an MCP-native data plane?

The next revision of the Model Context Protocol (MCP) makes every request self-contained and independently routable by removing sessions, the initialize handshake, and server-to-client callbacks, with routing fields now carried in HTTP headers. However, platform teams are warned that a naive header-only proxy is more dangerous under the new spec, as it breaks security, authorization, federation, and identity, and an MCP-native data plane like agentgateway is still required.

read9 min views1 publishedJul 21, 2026

The next revision of the Model Context Protocol (MCP) is supposed to make implementation infrastructure (ie, gateways, routers, etc) easier to reuse/implement. The draft (2026-07-28

) is intended to make every request self-contained and independently routable, and clean up anything that got in the way of that. Sessions, the initialize

handshake, server-to-client callbacks, etc are all gone or deprecated.

If you expose MCP servers from a MCP/agentgateway, this is a consequential set of changes. And it’s already prompting a question I am hearing from platform teams: if MCP is now stateless and its routing fields live in HTTP headers, can’t I just use a plain L7 load balancer or API gateway? Do I need an MCP native data plane like agentgateway?

The premise is misunderstood. The short answer is no! and the new spec actually makes a naive header-only proxy more dangerous, not less. This post walks through what changed, why a traditional proxy breaks under it, and what an MCP-native data plane like agentgateway does instead. We’ll even give you the most basic, runnable demo to prove it out.

Three “mega-SEPs” reshape everything else, and most of the rest of the draft falls out of them.

initialize

/ notifications/initialized

handshake is removed. Every request now carries its own io.modelcontextprotocol/protocolVersion

, clientInfo

, and clientCapabilities

in _meta

, and a new mandatory server/discover

RPC handles up-front capability and version negotiation. ping

, logging/setLevel

, and SSE resumability (Last-Event-ID

) are gone.Mcp-Session-Id

header and all session-lifecycle language are removed. List endpoints (tools/list

, etc.) no longer vary per connection, which makes them cacheable. Cross-call state moves to explicit, server-minted handles passed as ordinary tool arguments.Mcp-Method

mirrors method

, Mcp-Name

mirrors params.name

/params.uri

, and servers can promote individual tool arguments into Mcp-Param-*

headers via an x-mcp-header

annotation in the tool schema.Rounding it out: server-initiated requests (sampling, elicitation, roots callbacks) are replaced by the Multi Round-Trip Request pattern (SEP-2322); Tasks becomes an official extension (SEP-2663); and Roots, Sampling, and Logging are deprecated (SEP-2577).

The current finalized version (as of writing, one week before 7-28) is still 2025-11-25

. agentgateway already implements the draft line as protocol version 2026-07-28

, running the old and new protocols side by side, so we can talk about this concretely rather than hypothetically.

Historically, routing information was “buried deep within the JSON-RPC payload,” forcing intermediaries to “terminate TLS and perform deep packet inspection to route traffic.” Lifting Mcp-Method

and Mcp-Name

into headers fixes that. And it genuinely does make coarse routing and rate-limiting easy for standard infrastructure.

And that’s exactly where it should stop. The mistake is extrapolating from routing to policy and trust. Once you decide the header is the source of truth, four things break: security, authorization UX, federation, and identity. Let’s dig into them.

Headers and body are two copies of the same story, and nothing forces them to tell it the same way. Both are sent by the client. SEP-2243 anticipates exactly this and mandates that “any server processing the message body MUST validate that the HTTP header values exactly match the corresponding values in the JSON-RPC body.”

But a header-only proxy, by definition, never opens the body. Picture this: a support-desk agent is allowed to reach one MCP server through the gateway, and the platform team has locked it down to a single safe tool (echo

) with a header allowlist. Reasonable enough. Except the same server also exposes printEnv

. An attacker (or a prompt-injected agent) sends:

POST /mcp HTTP/1.1
Mcp-Method: tools/call
Mcp-Name: echo

{"jsonrpc":"2.0","id":2,"method":"tools/call","params":{"name":"printEnv","arguments":{}}}

The proxy sees Mcp-Name: echo

, decides “echo is safe,” and forwards. The header said echo

. The body ran printEnv

and dumped its environment variables, secrets and all. This is simple header/body mismatching and it’s been the root cause of a long line of CVEs in other protocols. Relying on the server SDK to catch it is not a great plan: it’s exactly the “someone else will validate it” assumption that these bugs live in.

An MCP-native gateway parses the body and validates the headers against it. Send that spoof through agentgateway and it never reaches the backend:

$ curl -sS http://localhost:3000/mcp \
 -H 'Content-Type: application/json' \
 -H 'Accept: application/json, text/event-stream' \
 -H 'Mcp-Method: tools/call' -H 'Mcp-Name: echo' \
 -d '{"jsonrpc":"2.0","id":2,"method":"tools/call","params":{"name":"printEnv","arguments":{}}}'

{"jsonrpc":"2.0","id":2,"error":{"code":-32020,"message":"Mcp-Name header/body mismatch"}}

(The full walkthrough of honest calls, method spoof, and policy denial is in Try it yourself at the end.)

The spoofed request is rejected with a JSON-RPC HeaderMismatch

error (code -32020

, the exact allocation SEP-2243 reserves) before it ever reaches a backend. The validation runs on every Streamable HTTP POST, ahead of any routing or session logic, and the check fires whenever a header is present and disagrees with the body, regardless of which protocol version the client negotiated.

Suppose you push authorization down to the header anyway. Two things go wrong immediately.

You can’t filter the tool list. tools/list

is a body-level response; a header-only proxy can’t rewrite it. So a user authorized for 2 of 100 tools sees all 100, and the model repeatedly tries to call tools it can’t use. That’s a miserable agent experience, and it leaks the shape of your whole tool surface to every caller.

agentgateway evaluates policy against the real request and filters the list. Policies are CEL expressions that can reference the tool name, the call arguments, and JWT claims, not just the header-exposed name (examples/mcp-authorization

):

mcp:
 policies:
 jwtAuth:
 issuer: agentgateway.dev
 audiences: [test.agentgateway.dev]
 jwks: { file: ./manifests/jwt/pub-key }
 mcpAuthorization:
 rules:
 - 'mcp.tool.name == "echo"' # anyone may echo
 - 'jwt.sub == "test-user" && mcp.tool.name == "get-sum"' # only test-user
 - 'mcp.tool.name == "get-env" && jwt.nested.key == "value"'

Tools that fail policy are dropped from tools/list

, so the model only ever sees what the caller can actually use. This is fundamentally a body-and-identity operation; there is no header that carries it.

You return the wrong kind of error. In MCP, application-level failures are supposed to come back as JSON-RPC errors that the agent reads and routes around (“that tool isn’t available, I’ll try another path”). A naive proxy that answers a denied call with a bare HTTP 403

instead signals a transport- or auth-level failure, and clients treat that very differently, often tearing down the connection rather than gracefully continuing. agentgateway returns a structured JSON-RPC error the agent can act on, and it deliberately hides the tool’s existence rather than confirming it was denied:

{"jsonrpc":"2.0","id":4,"error":{"code":-32602,"message":"Unknown tool: printEnv"}}

That distinction matters more than it looks. “Access denied” confirms the tool is there and worth attacking; “unknown tool” hands a prober nothing to work with. The higher-level modes people are building on MCP (code mode, search mode, and similar) all depend on this body-level intelligence. None of it is reachable from headers.

A big feature that nearly all of our users use: “Virtual MCP” where a single MCP endpoint exposed on the gateway exposes multiple backend MCP servers. Here is where header-only routing goes from limiting to impossible. Multiplexing (via the Virtual MCP concept) requires rewriting the body: namespacing tool names so they don’t collide, demultiplexing a tools/call

back to the server that owns the tool, merging tools/list

responses, and reconciling protocol versions across backends. agentgateway does all of this (examples/mcp-multiplex

): tools are namespaced as target_tool

, list operations fan out and merge, and calls are routed to the single owning backend. A header-only proxy has nowhere to put any of that logic.

The statelessness change does have a nice payoff worth calling. Under the old protocol, sessions pinned a client to a specific server instance; a proxy had to track and map session IDs to preserve that, and until proxies solved it you were effectively limited to a single replica per MCP server. agentgateway solved session mapping, but the draft makes the whole problem disappear for everyone: with no Mcp-Session-Id

and self-contained requests, any request can go to any replica. Scaling an MCP backend horizontally becomes ordinary load balancing. That’s a real win, and credit goes to the MCP maintainers for designing the session problem out of existence rather than papering over it. It also frees the gateway to focus on trust, policy, and federation rather than session bookkeeping.

Most of the draft’s authorization work is about clients and servers, and much of it doesn’t strictly require an MCP-native proxy. But it leans heavily on there being a capable identity layer in the path, and that’s exactly the territory agentgateway has been building out. It’s already on main

, with tests and examples.

.well-known/oauth-protected-resource

and oauth-authorization-server

metadata, issues WWW-Authenticate

challenges, and validates JWTs across multiple issuers, with provider-specific handling for Auth0, Okta, Keycloak, and Descope.examples/traffic-token-exchange

).jwt-bearer

→ access token) for cross-application access, so an MCP call can carry a properly scoped, audience-bound identity to the backend (examples/traffic-cross-app-access

).This connects directly to the draft’s smaller auth items, including issuer (iss

) validation for multi-AS mix-up protection (SEP-2468) and the multi-authorization-server migration guidance (SEP-2352), where per-issuer JWT validation is already in place.

The draft’s thesis of self-contained, independently routable requests is the right direction, and it does make MCP easier to move through standard infrastructure. But moving bytes is not the same as enforcing trust. If anything, the new spec makes that boundary more important: a header-only proxy is now not just insufficient but actively unsafe.

If you’re working through stateless MCP behind a gateway and hitting these edges, I’d genuinely like to hear about it. Reach out on LinkedIn.

The entire setup is one small config file. It puts agentgateway in front of a local MCP server (the reference server-everything

), runs statelessly so every request is a self-contained POST, and adds a body-based policy that allows only the echo

tool. Save it as agw.yaml

:

mcp:
 port: 3000
 statefulMode: stateless  # each request is a self-contained POST; no session handshake
 targets:
 - name: everything
 stdio:
 cmd: npx
 args: ["-y", "@modelcontextprotocol/server-everything"]
 policies:
 cors:
 allowOrigins: ["*"]
 allowHeaders: ["*"]
 mcpAuthorization:
 rules:
 - 'mcp.tool.name == "echo"' # body-based authz: only `echo` is permitted

Start the gateway (needs npx

for the stdio server):

agentgateway -f agw.yaml

Now drive it by hand from another terminal. Every request needs Content-Type: application/json

and Accept: application/json, text/event-stream

.

1. Honest call: header matches body, tool is allowlisted → forwarded

curl -sS http://localhost:3000/mcp \
 -H 'Content-Type: application/json' -H 'Accept: application/json, text/event-stream' \
 -H 'Mcp-Method: tools/call' -H 'Mcp-Name: echo' \
 -d '{"jsonrpc":"2.0","id":1,"method":"tools/call","params":{"name":"echo","arguments":{"message":"hello"}}}'

2. The attack: header says echo, body calls printEnv (env exfiltration) → rejected

curl -sS http://localhost:3000/mcp \
 -H 'Content-Type: application/json' -H 'Accept: application/json, text/event-stream' \
 -H 'Mcp-Method: tools/call' -H 'Mcp-Name: echo' \
 -d '{"jsonrpc":"2.0","id":2,"method":"tools/call","params":{"name":"printEnv","arguments":{}}}'

3. Method spoof: header tools/list, body tools/call → rejected

curl -sS http://localhost:3000/mcp \
 -H 'Content-Type: application/json' -H 'Accept: application/json, text/event-stream' \
 -H 'Mcp-Method: tools/list' -H 'Mcp-Name: echo' \
 -d '{"jsonrpc":"2.0","id":3,"method":"tools/call","params":{"name":"echo","arguments":{"message":"x"}}}'

4. Honest but denied by the body policy: printEnv blocked, tool hidden

Headers are consistent, but the policy allows only echo

. Note the response says Unknown tool

rather than “access denied”: the tool’s existence is hidden, not just its authorization.

curl -sS http://localhost:3000/mcp \
 -H 'Content-Type: application/json' -H 'Accept: application/json, text/event-stream' \
 -H 'Mcp-Method: tools/call' -H 'Mcp-Name: printEnv' \
 -d '{"jsonrpc":"2.0","id":4,"method":"tools/call","params":{"name":"printEnv","arguments":{}}}'
── more in #artificial-intelligence 4 stories · sorted by recency
── more on @model context protocol 3 stories trending now
sponsored brought to you by zahid.host 4,200+ EU-deployed projects
reading about agents? ship yours in a single git push.

Run your AI side-project on zahid.host

EU-based hosting, git-push deploys, automatic HTTPS, no cold starts. Free tier with a custom domain — perfect for shipping the agent you just read about.

$git push zahid main
Live at https://your-agent.zahid.host
Get free account → Pricing
from €0/mo · no card required
LIVE [news/mcp-goes-stateless-w…] indexed:0 read:9min 2026-07-21 ·