Running Handshake and Stateless MCP in the Same Gateway · William Collins↓<br>Skip to main content
William Collins
Table of Contents<br>Table of Contents
On July 28th, the MCP maintainers shipped the 2026-07-28 revision. No more initialize. No more initialized. No more Mcp-Session-Id. David Soria Parra called it “MCP’s most important release since remote MCP first launched over a year ago,” and I couldn’t agree more. In the weeks leading up to the release, I had the opportunity to have Angie Jones on the podcast to talk through some of the changes, community, and excitement on Episode 80 of The Cloud Gambit Podcast - (have a listen!)<br>I maintain gridctl, an MCP gateway that sits between your LLM client and a fleet of downstream MCP servers. Sessions were not a detail in that design. They were the spine. Identity, access scoping, telemetry, and rate limiting all hung off a session created at handshake time. The spec removing sessions was a big deal that took some time to plan out.<br>This post is about what it actually took to support the new revision without breaking the old one, plus a second standards drop nine days later that turned out to be the easier half of the story.<br>What Actually Changed<br>The headline is that MCP went from a bidirectional stateful protocol to plain request/response. That single change cascades:<br>Sessions are gone. Every request carries its own protocol version, client capabilities, and client identity in a _meta block. There’s no handshake to negotiate against.<br>server/discover replaces initialize. A read-only method that tells you what the server is, with no side effects.<br>Multi Round-Trip Requests (MRTR) replace server-initiated requests. Instead of holding an open stream so the server can ask the client something, the server returns resultType: "input_required" and the client comes back with inputResponses.<br>Method and tool names travel in HTTP headers. Mcp-Method and Mcp-Name mirror fields from the body.<br>List results are cacheable. Responses carry ttlMs and cacheScope.<br>Roots, Sampling, Logging, and HTTP+SSE are deprecated with a 12-month support window.<br>That header change is the one I want to sit on for a second, because it exists specifically for people building what I’m building. If the method and tool name are in headers, a gateway can route and authorize a request without parsing the JSON body at all. That’s a real gift to intermediaries! A gift that I would love discussing over Christmas dinner, but alas, people in my family would just think I lost the rest of my marbles.
Note
The spec is careful here: headers are a mirror, not a source of truth. Any server that actually processes the request has to validate the header against the body and reject a mismatch with error code -32020. Trusting the header blindly is a request-smuggling primitive.
You Cannot Just Flip<br>Let’s talk constraints that shape things. A gateway has two sides. Upstream, your LLM client connects to it. Downstream, it connects to some number of MCP servers you don’t control. Claude Desktop might be on the new revision while three of your five downstream servers are still on 2025-11-25 and one is a hosted endpoint that will upgrade whenever its vendor feels inclined to do so.<br>So “support the new spec” really means “speak both revisions concurrently, per peer, in both directions.” I ended up calling them eras , and the whole trick was deciding where an era is allowed to exist:<br>10<br>11<br>// Era is a property of the peer, never of an individual request, and it<br>// is normalized at the transport edges: everything inboard (router,<br>// access policy, call gates, pins, telemetry) stays era-free.
// EraHandshake covers revisions that establish a session via the<br>// initialize handshake (2025-11-25 and earlier).<br>EraHandshake ProtocolEra = "handshake"
// EraStateless covers revisions that carry per-request metadata<br>// with no handshake (2026-07-28 and later).<br>EraStateless ProtocolEra = "stateless"
Era gets resolved at the transport boundary and never travels inward. The router doesn’t know. The access policy doesn’t know. Telemetry doesn’t know. If era had leaked into the middle of the gateway, every one of those subsystems would have grown a branch, and the branches would have drifted out of sync within a month.<br>Downstream negotiation is a probe. Send server/discover, see what comes back:<br>// Downstream era resolution: gridctl probes each MCP server with<br>// server/discover before anything else. A positively modern answer<br>// selects the stateless era; a recognized modern error identifies a<br>// modern server that needs different handling; everything else falls<br>// back to the legacy initialize handshake on the same connection.
Note the asymmetry: only positive evidence of a modern peer selects the stateless era. Auth challenges and 5xx responses reject outright rather than getting classified. A legacy server that silently swallows...