On July 28, 2026, the Model Context Protocol shipped its most substantial revision since authorization: the protocol core went stateless. The initialize handshake is gone, the Mcp-Session-Id header is gone, and every request now carries its own protocol version, client identity, and capabilities. Any request can land on any server instance behind a plain round-robin load balancer — no shared session store, no sticky sessions, no gateway parsing JSON bodies to figure out where a request belongs.
If you run a remote MCP server that lets agents deploy apps, tail logs, and roll back releases, this is the best infrastructure news you have had all year — and a migration you should plan now. Here is the whole plan up front; the rest of this post walks through each step on a concrete deploy/logs/rollback server.
The 8-step migration checklist: 1) delete the handshake and session plumbing; 2) make every request self-describing with per-request _meta; 3) route, authorize, and meter at the gateway on the Mcp-Method/Mcp-Name headers; 4) mark tools/list cacheable with ttlMs/cacheScope; 5) move streaming logs off the SSE stream onto the Tasks extension with tasks/get polling; 6) move the rollback confirmation to a Multi Round-Trip Request (MRTR) elicitation; 7) re-key the audit trail from session id onto per-request identity plus trace context, with per-request OAuth validation; 8) serve both eras during the 12-month deprecation window and then drop the legacy transport.
What actually changed in 2026-07-28
The November 2025 anniversary post had already reported the scale: close to half-a-billion Tier 1 SDK downloads a month, with the TypeScript and Python SDKs each past a billion total downloads. MCP had become the data and interactivity substrate for agentic workflows — and the number-one operator complaint was that remote servers were painful to scale. As Google's developers blog put it, running remote MCP servers meant shared Redis session stores or gateway-level packet inspection: massive latency and operational cost just to keep a logical session pinned to the right replica.
The July 28 revision, led by maintainers David Soria Parra and Den Delimarsky, removes the session from the protocol layer entirely:
- No handshake, no sessions. SEP-2575 retires
initialize/notifications/initialized; SEP-2567 removesMcp-Session-Id. Each request carries protocol version, client identity, and capabilities in_meta(keys likeio.modelcontextprotocol/protocolVersion,clientCapabilities, andclientInfo). A new optionalserver/discoverRPC lets clients learn capabilities up front, but nothing requires it. - Header-based routing. SEP-2243 requires
Mcp-MethodandMcp-Nameon Streamable HTTP POSTs, with custom headers namespaced underx-mcp-header. Gateways, rate limiters, and WAFs can route and meter on headers instead of parsing JSON-RPC bodies. - Cacheable lists. SEP-2549 puts
ttlMsandcacheScope(publicorprivate) ontools/list,prompts/list,resources/list, andresources/read, with deterministic ordering so prompt caches stay stable across reconnects. - Mid-call interaction without streams. SEP-2322's Multi Round-Trip Requests replace server-initiated
elicitation/create,sampling/createMessage, androots/list: the server answersresultType: "input_required"plus the questions it needs, and the client retries withinputResponsesattached. - Authorization hardening. RFC 9207 issuer (
iss) validation (SEP-2468),application_typeso authorization servers stop rejectinglocalhostredirects for CLI apps (SEP-837), credentials bound to the issuer that minted them (SEP-2352), and Dynamic Client Registration formally deprecated in favor of Client ID Metadata Documents (CIMD). - Long work moves to Tasks. Tasks graduate into the
io.modelcontextprotocol/tasksextension with poll-basedtasks/getandtasks/update(SEP-2663); notifications move to an opt-insubscriptions/listenstream. - A real deprecation policy. Roots, Sampling, Logging, and the legacy HTTP+SSE transport keep working for at least twelve months (SEP-2577) — then they go away. New implementations should not adopt them.
The before/after for a single tool call tells the whole story. Before, a client shook hands, got a session id, and every later request had to be routed back to the replica holding that session:
POST /mcp HTTP/1.1
Mcp-Session-Id: 7f3a9c1e-…
{"jsonrpc": "2.0", "id": 2, "method": "tools/call",
"params": {"name": "deploy", "arguments": {"app": "api", "ref": "a1b2c3d"}}}After, the same call is self-contained — any replica can serve it, and the gateway knows what it is without opening the body:
POST /mcp HTTP/1.1
MCP-Protocol-Version: 2026-07-28
Mcp-Method: tools/call
Mcp-Name: deploy
{"jsonrpc": "2.0", "id": 2, "method": "tools/call",
"params": {"name": "deploy", "arguments": {"app": "api", "ref": "a1b2c3d"},
"_meta": {"io.modelcontextprotocol/protocolVersion": "2026-07-28",
"io.modelcontextprotocol/clientInfo": {"name": "deploy-agent", "version": "3.2"}}}}The migration, step by step, on a deploy/logs/rollback server
Take the canonical operator's MCP server: three tools, deploy(app, ref), logs(deploymentId, tail), and rollback(deploymentId). In the old world it ran as one stateful replica (or N replicas behind sticky sessions plus a Redis store holding session→replica affinity and in-flight deploy state). Here is each leg, migrated.
Step 1–2: delete the session layer, describe every request. Remove the initialize handler, the session-id issuance, the session lookup middleware, and the Redis affinity store. Every handler now reads protocol version, client identity, and capabilities from _meta and rejects requests independently — the spec even gives you the error codes: -32602 for missing _meta, -32020 for a header/body mismatch, -32022 for an unsupported protocol version. Statelessness here is a spectrum worth being honest about: several servers (FastMCP's stateless_http mode, for example) run a fresh transport per request by default and only opt back into sessions where genuinely needed. If your deploy tool kept in-flight state in the session, do what the spec authors recommend — mint an explicit handle from the tool and have the model pass it back as an argument on the next call. A visible handle the model threads between tools beats hidden transport state every time: it survives replica restarts, load-balancer rebalancing, and tasks/get polling alike.
Step 3: let the gateway do its job on headers. With Mcp-Method and Mcp-Name on every POST, the load balancer stops being MCP-aware in the expensive way (no body parsing, no stick-tables keyed on session id) and becomes MCP-aware in the cheap way. Route destructive tools to a hardened pool, rate-limit logs calls per tool name, and meter per-customer usage — all on headers:
# Destructive tools go to the canary pool; everything else round-robins.
map $http_mcp_name $upstream {
default mcp_general;
rollback mcp_canary;
deploy mcp_canary;
}Authorization gets the same treatment: a gateway can enforce "this token may call logs but never rollback" from Mcp-Name plus the bearer token, before the request ever reaches a replica. And per-request filtering is explicitly spec-legal — the set returned by tools/list MAY vary with the authorization on the request — so scoping one tenant's token to a subset of your 300 tools is a feature, not a statelessness violation.
Step 4: cache the catalog. tools/list is the highest-volume read on most servers and the least likely to change between calls. Return ttlMs and a cacheScope, keep the order deterministic, and watch reconnect storms get cheaper: clients cache the catalog instead of re-fetching it on every new connection, and stable ordering keeps upstream prompt caches hitting. One rule: if you filter tools/list per token (Step 3's trick), that response is private-scoped, never public — a shared cache must not serve one tenant's tool catalog to another.
Step 5: move streaming logs onto Tasks. This is the leg most migrations under-plan, because the old long-lived GET SSE stream made log tailing feel free. It was not free — it was the reason you needed session affinity in the first place.
In the new model, deploy returns a task handle; log output is tailed with poll-based tasks/get (or paginated resources/read for finished runs); live notifications become opt-in through subscriptions/listen. The honest tradeoff is polling cadence versus stream immediacy: a 1–2 second poll loop is plenty for "watch this deploy," costs no held-open connections across your fleet, and lets any replica answer any poll. Save subscriptions/listen for the clients that genuinely need push, not as the default every agent pays for.
Keeping agent actions authorized and auditable without a session id
This is the step the TODO item calls out explicitly, and it is where stateless migrations actually fail — not in routing, but in the audit log. When Mcp-Session-Id disappears, three things that used to come for free have to be rebuilt deliberately: proving who called, proving what they were allowed to do, and reconstructing what happened afterward.
Authenticate and authorize every request, independently. There is no longer a handshake whose success a later call can inherit, so each request carries its own OAuth token and each is validated on its own: check the RFC 9207 iss parameter against the issuer you expect (this closes the authorization-server mix-up hole), bind client credentials to the issuer that minted them, and plan the move from Dynamic Client Registration to Client ID Metadata Documents while DCR still works. deploy and rollback should additionally require a token scope the read-only logs token never has — the gateway rule from Step 3 enforces the split before application code runs.
Gate the destructive tool with a confirmation the protocol can see. Mark rollback honestly with the spec's tool annotations — non-read-only, destructive, non-idempotent — so every client that honors them adds its own approval prompt. Then enforce confirmation server-side with an MRTR elicitation instead of trusting the client's UI: the first rollback call returns resultType: "input_required" asking "roll back deployment d-8841 to ref a1b2c3d?"; the client retries with the human's answer in inputResponses; only then does the rollback execute. The confirmation round-trip is now part of the protocol transcript rather than a handshake-era side channel, which is exactly what makes it auditable.
Re-key telemetry off the session id. One operator's migration audit found the failure mode precisely: session-derived telemetry — client info, session_id columns — degrades silently when the session disappears, leaving dashboards that look healthy and say nothing. Replace the session key with two per-request keys before you migrate: the client identity from _meta (who asked) and a W3C trace-context id you generate at the edge and propagate through every tool call, MRTR round-trip, and tasks/get poll (what belongs together). A rollback's audit row should read as one story without any session anywhere in it:
{"trace_id": "4bf92f3577b34da6a3fb97300959e_at", "client": "deploy-agent/3.2",
"tool": "rollback", "args": {"deployment": "d-8841", "ref": "a1b2c3d"},
"elicitation": "confirmed by on-call at 14:02:11Z", "result": "ok", "replicas_hit": 2}Note the last field: in a stateless fleet, one logical operation routinely touches two replicas (the elicitation round-trip lands wherever the load balancer sends it). That is correct behavior, not a bug — and it is why the trace id, not replica affinity, is now the unit of "one action."
Compatibility and rollout
The maintainers paired a breaking change with a humane rollout, so use it. Serve both eras from the same fleet: modern requests (per-request _meta, no handshake) go stateless; legacy clients still get their initialize round-trip and session ids until the twelve-month window closes. The new error codes double as your migration telemetry — a spike in -32022 (unsupported protocol version) tells you exactly which clients have not upgraded.
Track the TypeScript, Python, Go, and C# Tier 1 SDKs (Rust is in beta) and upgrade the server first: a new-spec server can still speak to old clients during the transition, while the reverse is not true. Ecosystem momentum is on your side — AWS Bedrock AgentCore and Cloudflare's Agents SDK supported the spec from day zero, Honeycomb already sees nearly a fifth of its interactive queries arriving from agents, and SDK v2's client-server split cut one framework's package size by roughly 83% while making it 25% faster. Put a calendar date on dropping the legacy HTTP+SSE transport now, while the twelve-month clock is easy to read, and make per-request tools/list filtering part of the same release — it is the cheapest authorization upgrade in the whole checklist.
Why this lands hardest on machines you own
Every step above is infrastructure-shaped: replicas behind a plain load balancer, header rules at the gateway, a trace id minted at the edge, no Redis session store to operate. That is precisely the shape a self-hosted fleet is already good at — ordinary HTTP load balancing, ordinary headers, ordinary logs — and precisely the shape that used to require either a managed MCP hosting layer or a hand-built session-affinity stack. The stateless revision collapses "run a deploy API your agents can call" into a deployment any team that already runs HTTP services knows how to operate: N identical replicas, a round-robin balancer, and an audit log keyed on identity instead of connection state. The protocol finally works like the rest of the web — stateless, cacheable, routable — which means the team that owns its own machines gets the scalable version without renting anyone's session layer to get there.
Bex.co is the open-source, AI-native Render alternative — push a git repo, get a running HTTPS service on machines you own. Run your own deploy-and-rollback MCP server as N identical replicas behind a plain load balancer. Star the repo on GitHub or deploy your first app today.



