MCP Went Stateless: Migrating to the 2026-07-28 Spec Without Breaking Production

The 2026-07-28 MCP revision removes sessions and the initialize handshake. What breaks, why header routing matters most, and how to migrate.

MCP Went Stateless: Migrating to the 2026-07-28 Spec Without Breaking Production

The Model Context Protocol shipped its 2026-07-28 revision, and it is the largest change to the protocol since launch. The short version: MCP removed sessions. The initialize / notifications/initialized handshake is gone. The Mcp-Session-Id header is gone. Every request now carries its own protocol version, client identity, and capabilities in _meta.

That is an admission, and a welcome one. The sticky-session design that shipped with the original Streamable HTTP transport was an operations tax, and everyone running a remote MCP server in production was paying it. You needed session affinity at the load balancer, or a shared store for session state, or both. Now neither. Plain round-robin works.

Here is what actually breaks, what to change first, and why the line everyone is repeating — “MCP is stateless now” — is not the most consequential thing in this release.

What was removed#

The removals are blunt:

  • initialize and notifications/initialized. Per-request _meta keys carry the same information: io.modelcontextprotocol/protocolVersion, io.modelcontextprotocol/clientCapabilities, io.modelcontextprotocol/clientInfo.
  • Mcp-Session-Id, dropped from the Streamable HTTP transport entirely.
  • The HTTP GET endpoint plus resources/subscribe and resources/unsubscribe, replaced by a single subscriptions/listen long-lived POST-response stream that clients opt into by notification type.
  • ping, logging/setLevel, and notifications/roots/list_changed. Log level is now per-request via io.modelcontextprotocol/logLevel.
  • SSE stream resumability. Last-Event-ID and event IDs are gone; a broken response stream loses the in-flight request and the client must re-issue it with a new request ID.

In their place: server/discover, which servers MUST implement to advertise supported protocol versions, capabilities, and identity. Clients MAY call it once up front instead of negotiating per connection.

Roots, Sampling, and Logging are formally deprecated but still functional — the spec now carries a feature lifecycle policy with a minimum twelve-month window before removal. Nothing detonates on a Tuesday, but the direction is set.

What breaks if you built on session IDs#

If your server keeps per-session state, you have three options and only one of them is good.

Server-minted handles passed as tool arguments. This is what the spec points at, and it is right. If a tool needs continuity — an open cursor, a transaction, a half-built query — mint an opaque handle, return it in the result, and let the client pass it back as an ordinary tool parameter. That handle is application state with a lifetime you control, not protocol state the transport pretends to manage for you.

Externalise to a shared store. Redis behind every replica works, but you have reintroduced exactly the coupling the spec just removed.

Hidden per-instance state. Do not. Under round-robin this becomes a bug that appears only under load, only on the second replica, only in production.

Easy to miss: tools/list, resources/list, and prompts/list no longer vary per connection. Per-caller filtering now happens through authorization rather than connection identity — more honest anyway, since the old approach was access control implemented inside a list endpoint.

MRTR inverts server-initiated requests#

Multi Round-Trip Requests replace roots/list, sampling/createMessage, and elicitation/create — every case where the server called back into the client mid-request. Those needed a held-open bidirectional stream, which is precisely what makes a protocol stateful.

The new shape: the server returns an InputRequiredResult with resultType: "input_required" and an inputRequests field describing what it needs. The client retries the original request with inputResponses attached. All results now carry a required resultType"complete" or "input_required" — and clients must treat a missing field from older servers as "complete".

The tradeoff is real. You have swapped one long-lived connection for several independent HTTP requests, and your handler must reconstruct enough context on retry to continue. The spec’s answer is requestState, encoded by the server itself. That means designing interactive tools as resumable, idempotent handlers — more work than a callback, and the only version of this that survives a rolling deploy mid-conversation.

Header routing is the change your infra team actually wanted#

Mcp-Method and Mcp-Name are now required headers on Streamable HTTP POST requests. Method name and tool name, in HTTP headers, available before anyone parses a JSON body.

This is the quiet change, and for whoever runs the infrastructure the most useful line in the changelog:

  • Gateway routing without body inspection. Send tools/call with Mcp-Name: run_report to the heavy compute pool and tools/list to a cheap cached tier. Envoy or NGINX does that with a header match — no Lua, no WASM filter, no buffering the body to make a routing decision.
  • Per-tool rate limiting and metering. Throttle or bill the expensive tool without an application-layer counter.
  • WAF and audit at the edge. A security team can block or log a specific tool name centrally. In a regulated deployment — a Hospital Management System exposing patient lookups over MCP — the gap between “list the tools” and “call the tool that reads a chart” is the gap between an audit trail you can defend and one you cannot.
  • Observability without decoding. Existing HTTP dashboards get per-method cardinality for free. The revision also documents OpenTelemetry trace context propagation through _meta (traceparent, tracestate, baggage), so traces stitch across the MCP boundary properly.
  • Custom routing keys. x-mcp-header passes headers derived from tool parameters — the escape hatch for tenant IDs.

Deploys stop being a special case#

Autoscaling runs on ordinary request metrics. Under the old model, scaling in killed live sessions, so your policy had to be conservative and your termination grace period long. A replica can now leave the pool between requests.

Blue-green and rolling deploys no longer need draining logic tied to session lifetime. The unit of work is a request, and every HTTP-aware proxy already drains in-flight requests.

No shared state means no Redis in the critical path purely for the protocol — one less dependency, one less failure mode, one less line item.

One caveat worth budgeting for: subscriptions/listen is still a long-lived POST-response stream. If clients opt into change notifications you still have connections to drain. A stateless core does not mean every connection is short-lived — that is the detail that will surprise you on the first rolling restart.

Caching finally has a contract#

A new CacheableResult interface makes ttlMs and cacheScope required on results from tools/list, prompts/list, resources/list, resources/read, and resources/templates/list. ttlMs is a freshness hint in milliseconds; cacheScope is "public" or "private" and controls whether shared intermediaries may cache.

If you run a gateway, cacheScope: "public" on a tool list is free throughput. Discovery traffic is a meaningful share of MCP load, identical across callers, and until now there was no sanctioned way to cache it.

Servers SHOULD also return tools in deterministic order, explicitly to improve LLM prompt cache hit rates. Small line, direct cost impact: a tool list that reshuffles between calls invalidates the model provider’s prompt cache every turn. Sort the list.

The bigger story: Tasks is an extension now#

Here is the argument. The stateless core is the headline, but the consequential change is the extensions framework — specifically that Tasks moved out of core into the io.modelcontextprotocol/tasks extension, alongside MCP Apps and Enterprise Managed Authorization.

Tasks is how long-running work is expressed in MCP. The redesigned extension drops the blocking tasks/result for polling via tasks/get, adds tasks/update for client-to-server input, removes tasks/list, and lets servers return task handles unsolicited.

Moving it out of core makes long-running agent work explicitly optional. A conformant host can implement the entire core specification and have no idea what a task is. If you are building a server whose whole value is asynchronous — a batch reconciliation, a human approval, a twenty-minute migration — you can no longer assume the host at the other end will handle it.

Check the new extensions field on ServerCapabilities and ClientCapabilities, and build a degraded path: return a handle as a plain tool result and let the model poll it through a normal tool call. Uglier, and it works everywhere.

That is what an extensions framework does: the core gets smaller and more implementable — part of why MCP reached this adoption level, with Tier 1 SDKs (TypeScript, Python, Go, C#) supporting the revision on day one, Rust in beta, close to half a billion SDK downloads a month, and TypeScript and Python each past a billion cumulative. But it shifts work onto anyone building on the interesting parts. Feature detection is now part of the job.

Auth: DCR is on its way out#

Dynamic Client Registration (RFC 7591) is deprecated in favour of Client ID Metadata Documents, and stays available only for authorization servers that do not support CIMD. Alongside it: authorization servers SHOULD return the iss parameter per RFC 9207, clients MUST validate a present iss against the recorded issuer before redeeming the code, and client credentials are bound to the authorization server that issued them. Plan the CIMD migration now rather than in month eleven of the window.

A migration order that works#

  1. Pin your SDK to a version supporting 2026-07-28.
  2. Implement server/discover. Mandatory, and the cheapest item here.
  3. Audit for session-scoped state; convert it to server-minted handles passed as tool arguments.
  4. Set honest ttlMs and cacheScope values, and sort tool lists deterministically.
  5. Convert Sampling, Elicitation, and Roots callbacks to MRTR with resumable handlers.
  6. Only then touch the load balancer. Pulling sticky sessions before the server is genuinely stateless is how you find out which handler still kept a local map.

Deprecated features stay functional for at least twelve months, so this is a quarter of planned work, not a fire drill.

For teams whose AI implementation runs on MCP as its integration layer — wiring agents into Data Platforms, internal APIs, and Operational Automation workflows — this revision turns the protocol from something you special-case in your infrastructure into something that behaves like every other HTTP service you operate. A School ERP exposing enrolment and attendance tools over MCP should deploy with the same chart, probes, and autoscaling policy as the REST API next to it. That is the win.

We covered where MCP sits as an integration standard and the interoperability picture across vendors previously. This revision is the operational follow-through.


Stateless is the easy part. Feature detection is the part that bites. If you are running remote MCP servers and want the migration mapped against your actual deploy topology, talk to our engineers.