In June I wrote that the MCP release candidate was about to delete the session handshake, and that you had about ten weeks. That clock ran out yesterday. The 2026-07-28 specification is final — the fifth revision of MCP, and the largest since it launched.
Two of the three things I flagged in June landed roughly as expected: the stateless core, and the OAuth hardening. Fine. But re-reading the finished spec against the RC, the change I underweighted is the one I’d now put first for anyone operating this stuff — and it isn’t statelessness.
It’s that MCP became a protocol your existing L7 infrastructure can actually see.
The part everyone will lead with
Sessions are gone. No initialize handshake, no Mcp-Session-Id. Every request is self-contained, carrying its protocol version, client identity, and capabilities in _meta.io.modelcontextprotocol/* fields. Any instance behind a round-robin load balancer can serve any request.
This is genuinely good and genuinely boring — which is the point. It converts MCP servers from stateful connection-holders into ordinary horizontally-scalable HTTP services: no sticky sessions, no session affinity, no in-memory state to lose on a pod restart, and serverless and edge deployment become viable. Anthropic’s David Soria Parra called it the most important release since remote MCP launched, and on scaling economics he’s right.
The adoption numbers explain the urgency behind it: MCP passed 400M monthly SDK downloads, 4x growth this year, with over 950 servers in Claude’s connectors directory. A protocol at that volume can’t keep asking operators for session affinity.
But “my servers scale sideways now” is a capacity story. Here’s the operability story.
Headers changed what a gateway can be
Under Streamable HTTP, clients must now mirror selected body fields into HTTP headers, explicitly so that “intermediaries (load balancers, gateways, observability tooling) can route and inspect requests without parsing the body.”
| Header | Mirrors | Required for |
|---|---|---|
MCP-Protocol-Version | _meta protocol version | every POST |
Mcp-Method | method | all requests |
Mcp-Name | params.name or params.uri | tools/call, resources/read, prompts/get |
Mcp-Param-{Name} | any tool param annotated x-mcp-header | when the server annotates it |
That last row is the sleeper. A server can annotate individual tool parameters in its inputSchema with x-mcp-header, and conforming clients must mirror those values into headers. Using the spec’s own example, a Spanner query tool can surface its region argument as a routable header:
POST /mcp HTTP/1.1
MCP-Protocol-Version: 2026-07-28
Mcp-Method: tools/call
Mcp-Name: execute_sql
Mcp-Param-Region: us-west1
Read that as an operator. Tool identity and selected tool arguments are now visible to Envoy, nginx, your WAF, and your rate limiter as plain header values. Per-tool rate limits, per-tenant quotas, region-pinned routing, tool-level metrics, blocking one misbehaving tool at the edge — all of it becomes ordinary L7 configuration instead of a body-parsing proxy you wrote yourself.
I’ve argued for a while that agent platforms need an MCP gateway doing identity exchange, catalog brokering, schema firewalling, and audit. That argument doesn’t go away — policy, redaction, and human-approval queues still need to understand payloads. But the cheap half of it just became infrastructure config, and the expensive half gets a much better substrate to sit on. For token cost attribution in particular, Mcp-Name in an access log is a materially better dataset than anything I’ve had to work with.
There are constraints worth knowing before you design around it: x-mcp-header applies only to primitive parameters (integer, string, boolean — number is excluded), only to properties statically reachable through a chain of properties keys, and values that aren’t header-safe ASCII get a Base64 sentinel wrapper (=?base64?…?=). Clients must reject tools that violate these rules — by excluding just that tool from tools/list, not the whole server.
And it opens a confused-deputy seam the spec tells you to close
Mirroring creates two copies of the truth, and the spec is unusually direct about the risk: servers must reject requests where headers disagree with the body, “when different components in the network rely on different sources of truth (e.g., a load balancer routing on the header value while the MCP server executes based on the body value).”
The body wins. Mismatch gets 400 Bad Request and JSON-RPC error -32020 (HeaderMismatch), and servers must Base64-decode encoded header values before comparing. If you write an intermediary that rewrites a mirrored header, you have built a request the backend is required to reject.
The sharper trap is version-dependent. Header–body validation is only guaranteed on revisions that mandate it, so an intermediary enforcing policy on mirrored headers should verify MCP-Protocol-Version indicates such a revision — and reject rather than trust the headers if it doesn’t. Skip that check and you’ve built exactly the vulnerability the validation rule exists to prevent: an older client can hand your gateway header values nobody will ever reconcile against the body, and your routing, metering, and tenant isolation all key off attacker-chosen strings.
If you’re standing up header-based policy this quarter, that one check is the highest-value line of code in the migration. This is the same class of problem as trusting an agent’s self-asserted identity: the mechanism is fine, and the failure is believing an unverified claim about it.
Your one tool call is now several HTTP requests
Statelessness had to be paid for somewhere, and Multi Round-Trip Requests (SEP-2322) is where. Servers may no longer send JSON-RPC requests at all — not on SSE streams, not anywhere. When a server needs a user confirmation or an LLM completion, it returns an InputRequiredResult carrying inputRequests plus an opaque, server-defined requestState. The client gathers the input and re-POSTs the original call with inputResponses and that same requestState echoed back unchanged. Any instance can pick up the retry, because the continuation travels in the payload.
Clean design. Four consequences if you operate it:
- Latency budgets multiply. A tool call needing confirmation is now at minimum two full request cycles including human think-time between them. Anything you sized as one timeout is now a sequence, and the p99 you measured last month describes a different protocol.
- Idempotency stops being optional. A retried tool call arrives as a new request with a new id. If your handler has side effects and your only guard was “one call, one connection,” that guard is gone.
- Traces need stitching. Two POSTs are one logical operation, and nothing in HTTP tells your backend that. Whatever correlation you carry — and if you’re following the OTel GenAI conventions, you have somewhere to put it — has to span the round trips, or every elicitation flow reads as two unrelated calls forever.
requestStateis opaque to you too. It’s the server’s continuation, so its size, sensitivity, and lifetime are the server’s business. If you run the gateway, it’s an unbounded blob transiting your policy layer in both directions. Bound it and log its size before someone puts a megabyte of context in there.
Change notifications moved too: the standalone GET stream endpoint is gone, replaced by a long-lived subscriptions/listen request whose response stream carries only the notification types the client opted into.
What you quietly lost
Some removals are easy to miss because they’re framed as simplification:
- Stream resumability.
Last-Event-IDis not supported. Combined with “closing the SSE response stream MUST be treated as cancellation,” a dropped connection mid-call is now unambiguously a cancelled request. For long-running tool calls over imperfect networks — the wobbly VPN, the laptop that slept — this is a real regression, and the mitigation is application-level: make long work resumable yourself, most likely via the Tasks extension, which moved out of the experimental core into a versionedio.modelcontextprotocol/tasksextension with polling and durable handles. - Roots, sampling, and logging are deprecated. They still work; new implementations shouldn’t adopt them.
- The legacy HTTP+SSE transport (2024-11-05) is formally deprecated under the new feature-lifecycle policy (SEP-2596) and eligible for removal in a future revision.
- Dynamic Client Registration is deprecated in favor of Client ID Metadata Documents.
The genuinely new thing here is that all of those come with a clock instead of a vibe: the revised spec guarantees at least 12 months between deprecation and removal. Boring governance is the strongest signal in this release. It’s the difference between a protocol you experiment with and one you can put in a compliance document — and if you’re navigating the EU AI Act timeline, a written deprecation floor is worth more than any feature in the changelog.
Honest caveats
I’m reading a spec, not a fleet. Nobody has operated this at scale yet, and the interesting failures in stateless protocols are the ones that only appear under real traffic — thundering herds against tools/list when a cache TTL expires fleet-wide, requestState blobs growing until they hit a proxy’s header or body limit, MRTR retry storms when a client mishandles input_required. My chaos experiments for MCP need rewriting for this revision, and I’d rather do that than predict which one bites first.
Coverage so far has also been notably one-sided. The Register’s piece quotes Anthropic and Microsoft on the benefits and concedes only that “some migration cost is expected” — no skeptical practitioner voices, because there hasn’t been time for any. Treat the enthusiasm accordingly, mine included.
And the SDK story is uneven: TypeScript, Python, Go, and C# ship full support, Rust is beta. If Rust is load-bearing for you, that’s your timeline, not the spec’s.
What to do Monday
- Grep for session state. Every place you key state on
Mcp-Session-Idor assume connection affinity is a rewrite. Start here; it’s the longest pole. - Inventory your elicitation and sampling flows. Anything built on a held-open stream where the server calls the client back needs to become MRTR. Check idempotency on every tool with side effects while you’re in there.
- Find every intermediary that parses bodies to make decisions. Most of those can now read headers instead — and should, since it’s faster and the spec sanctions it.
- If you run a gateway, add the version check first. Verify
MCP-Protocol-Versionmandates header–body validation before trusting any mirrored header for routing, metering, or tenant isolation. One conditional, and it’s the difference between L7 policy and a spoofable one. - Bound
requestState. Size limits and logging on the opaque blob now transiting your policy layer, before someone discovers how much context fits in it. - Put a date on the deprecated features you use. Roots, sampling, logging, HTTP+SSE, DCR. You have a 12-month floor and a written policy; use it to schedule the work instead of discovering it.
The June version of this post said the interesting thing about MCP 2.0 was that it made the protocol scale. Having read the final spec, I’d revise that. Statelessness is what lets you run a lot of MCP. Header mirroring is what lets the infrastructure you already own govern a lot of MCP — and governability, not throughput, is the thing standing between agent platforms and the regulated environments where they’re headed.
Comments