MCP becomes stateless: initialize handshake and Mcp-Session-Id header removed

MCP Becomes Stateless: initialize Handshake and Mcp-Session-Id Header Gone

Open your MCP server code and search for two things: the initialize handler and any reads of the Mcp-Session-Id header. In the spec that dropped yesterday, both stop being part of the protocol.

Spec 2026-07-28 was published on July 28 and is the biggest change in MCP since remote servers arrived, roughly eighteen months ago. The headline is a phrase from the announcement: MCP goes “from a stateful bidirectional protocol to a stateless request/response protocol”.

It’s not a cosmetic change. It rewrites how a request identifies itself, how you deploy your servers, and what capabilities you can keep depending on.

What Actually Got Removed

Three things, and they’re connected:

  • The initialize/initialized exchange. There’s no longer an initial negotiation round before you can call a tool.
  • The Mcp-Session-Id header. It doesn’t exist in the new spec.
  • The protocol-level session requirement, removed entirely.

Everything that handshake established—protocol version, client identity, client capabilities—now travels in each request instead of being negotiated once and remembered.

What a Request Looks Like Now

Client identity moves to _meta, under a namespaced key:

{
  "jsonrpc": "2.0",
  "id": 1,
  "method": "tools/call",
  "params": {
    "name": "search",
    "arguments": { "q": "otters" },
    "_meta": {
      "io.modelcontextprotocol/clientInfo": { "name": "my-app", "version": "1.0" }
    }
  }
}

Protocol version doesn’t go in _meta: it travels in the MCP-Protocol-Version header. _meta also carries W3C Trace Context (traceparent, tracestate, baggage), which means distributed tracing across an agent’s tool calls stops being something you have to build yourself.

There are two more headers that matter for Streamable HTTP: Mcp-Method and Mcp-Name. They let a proxy or load balancer route a request without parsing the JSON body. Servers must reject requests where headers and body don’t match, so you can’t use them to sneak a different call past the router.

Why They Did It

The stated reason is deployment, and it’s worth quoting directly: “Any request can now land on any instance of the server behind a simple round-robin load balancer, without needing shared storage”.

If you’ve run an MCP server in production, you know what that sentence is buying you. Session affinity meant either sticky routing or a Redis in the middle whose only job was to remember which client said hello. Both are operational cost, and both are a failure mode: if you lose the session store, all connected clients have to handshake again.

The spec adds two things pointing at the same idea. Results from listings and resources can now carry ttlMs and cacheScope, modeled on HTTP’s Cache-Control: cacheable responses instead of state sustained by connection. And for calls that do need more than one round trip, there’s InputRequiredResult, with inputRequests, requestState, and the inputResponses the client provides. State travels in the request instead of living on the server.

What’s Deprecated and What Replaces It

Roots, Sampling, and Logging are deprecated, as is the legacy HTTP+SSE transport. The spec names a replacement for each:

Deprecated Replacement
Roots Tool parameters, resource URIs, or server configuration
Sampling Direct integration with your LLM provider’s API
Logging stderr for stdio transports; OpenTelemetry for structured observability
HTTP+SSE Transport Streamable HTTP

Sampling is worth looking at twice. It let a server ask the client to run an inference on its behalf, which is exactly the kind of bidirectional, stateful thing the new core is designed not to do. If your server used it, you’re not renaming a field: you’re assuming an integration with an LLM provider you didn’t have before.

Along with the deprecations, the spec introduces a formal deprecation policy: “a minimum twelve-month window for you to plan updates instead of reacting to them”. That’s the part with the longest tail. Until now, anyone consuming MCP had no contractual answer to “how long do I have?”. Now they do, and it’s at least a year.

Migration Checklist

The TypeScript, Python, Go, and C# SDKs—all Tier 1—already support 2026-07-28. A practical order of work:

  1. Update the SDK and read your framework’s migration notes. Most of the wire format change gets resolved for you.
  2. Grep for Mcp-Session-Id and anything built on a session identifier: per-session caches, per-session auth, per-session working directories. Each one has to become a request parameter or a server-side lookup with a durable key.
  3. Remove assumptions from the initialize handler. If you defined defaults during initialization and read them afterward, those defaults now have to be derived per-request or come from server configuration.
  4. Replace Roots with explicit tool parameters or resource URIs. Usually the easiest of the three, and tends to leave your tool schemas more honest about what they actually need.
  5. Replace Sampling with a direct call to the provider, or drop the feature.
  6. Move Logging to stderr or OpenTelemetry.
  7. Get out of HTTP+SSE toward Streamable HTTP, and add Mcp-Method/Mcp-Name if something in front of your server routes by content.
  8. Only then, tear down session infrastructure: sticky sessions, session store. And confirm you can round-robin between instances.

Step 8 comes last on purpose. It’s the payoff, and it’s also the step that tells you whether steps 2 and 3 are complete.

The Honest Part

The announcement doesn’t pretend this is free: “there will be some migration cost, especially for developers who did rely on session identifiers”. That fits the shape of the change. If your server was already effectively stateless—receives arguments, returns a result—migration is an SDK bump and a cleanup pass. If you built stateful workflows on session identity, you’re refactoring, not upgrading.

Twelve months is enough to do it without rushing. It’s not enough to never do it.


Does your MCP server depend on per-session state, or does migrating to stateless land on an SDK bump and cleaning up two handlers? Tell us what breaks.