Back to AI guides

MCP Goes Stateless: What the 2026-07-28 Spec Changes and How to Migrate

Stanley Ulili
Updated on August 10, 2026

The Model Context Protocol is the open standard that connects LLM clients to tools, data, and services, and until recently every MCP connection carried a session. A client opened a connection with a handshake, the server issued a session id, and every following request had to echo that id back. That worked, but it pinned each client to one server instance and made horizontal scaling awkward.

The 2026-07-28 specification removes all of that. It is the largest revision of MCP since launch, and its headline is a stateless protocol core: the initialize handshake is gone, the session id is gone, and every request now stands on its own. Any request can land on any server instance behind a plain round-robin load balancer, with no shared session store required. The old 2025-11-25 spec keeps working, and a new deprecation policy guarantees at least a twelve-month overlap, so this is a migration you can plan rather than scramble through.

This guide walks through why the stateful design hurt, what the stateless core actually changes on the wire, how to manage state when your application still needs it, and what the upgrade involves.

Why the old stateful protocol hurt

To see why statelessness is worth a breaking change, it helps to look at what the session-based design cost in production. The trouble started at the very first request and compounded from there.

The handshake and session pinning

The old protocol opened with a mandatory handshake. A client sent an initialize request, the server generated a unique Mcp-Session-Id, stored that session in its local memory, and returned the id in the response headers. From then on, every request the client made had to include that exact id.

A diagram illustrating the initial handshake process where the client sends an "initialize" request and the server responds with an Mcp-Session-Id, establishing a pinned session.

That created a hard link between the client and the one server instance that held the session in memory. For a single server it was harmless. In any distributed setup it was an anti-pattern, because modern web services are built to be horizontally scalable and to treat individual instances as interchangeable and disposable.

The load balancer failure

Session pinning collides directly with ordinary load balancing. Picture three identical MCP server instances behind a round-robin balancer.

A clear architectural diagram showing a client, a load balancer, and three server instances. It demonstrates how a request can be routed to the wrong instance, causing a "Session Not Found" error.

A client sends initialize, and the balancer routes it to Instance 2, which creates session 9f2c in its own memory and returns the id. The client then sends a tools/call with Mcp-Session-Id: 9f2c, and the balancer, doing its normal round-robin thing, sends it to Instance 1. Instance 1 has never heard of 9f2c, so it rejects the request with a session-not-found error. Standard load balancing simply does not work, and there is a single point of failure baked in: if Instance 2 restarts, its in-memory session is gone for good, and every request on that id fails even though the other instances are healthy.

The workarounds and their costs

Developers papered over this, but each fix had a real downside. Sticky sessions, where the balancer pins a client to the instance that started its session, undercut the whole point of load balancing by creating hot spots, and they do nothing for resilience since a dead instance still loses the session. The more robust option was to externalize session state into a shared store such as Redis, so any instance could serve any request. That solved scaling and resilience, but it added a piece of critical infrastructure to deploy, manage, and pay for, plus a network hop on every single request to validate the session before any real work could start.

Both workarounds added cost, latency, and complexity purely to compensate for the protocol being stateful. The protocol itself needed to change.

The 2026-07-28 spec: a stateless core

Published on July 28, 2026 by maintainers David Soria Parra and Den Delimarsky, the new spec tears out the stateful core. Two proposals do the heavy lifting.

Removing the handshake and sessions

SEP-2575 removes the initialize and initialized handshake. The protocol version, client info, and client capabilities that were exchanged once at connection time now travel in a _meta object on every request. A new server/discover method lets a client fetch server capabilities when it wants them up front, but it is optional rather than mandatory.

SEP-2567 removes the Mcp-Session-Id header and the protocol-level session entirely. As a side effect, the list endpoints (tools/list, resources/list, prompts/list) no longer vary per connection.

Together they make every request atomic, independent, and self-contained. The server no longer has to remember anything about past interactions to handle a new request, which is the same principle that made HTTP the backbone of the scalable web.

A request, before and after

The cleanest way to feel the change is to compare a single tool call.

A side-by-side code comparison showing the "before" stateful request (requiring two network round trips) and the "after" stateless request (a single, self-contained call).

Before, a first-time tool call took two round trips. First you initialized the session:

 
POST /mcp HTTP/1.1
Content-Type: application/json

{"jsonrpc":"2.0","id":1,"method":"initialize",
 "params":{"protocolVersion":"2025-11-25","capabilities":{},
 "clientInfo":{"name":"my-app","version":"1.0"}}}

The server replied with Mcp-Session-Id: 1868a90c-3a3f-4f5b, and only then could you make the actual call, echoing that id back on the second trip.

After, the same call is a single self-contained request. The protocol version and tool name ride in headers so a gateway can route on them without parsing the body, and client info travels in _meta:

 
POST /mcp HTTP/1.1
MCP-Protocol-Version: 2026-07-28
Mcp-Method: tools/call
Mcp-Name: search
Content-Type: application/json

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

Those routing headers, Mcp-Method and Mcp-Name, are now required on Streamable HTTP requests (SEP-2243), so your gateway, rate limiter, or WAF can route and meter on headers instead of reading JSON.

What statelessness unlocks

This one change removes the whole class of problems above.

An animation demonstrating how a standard round-robin load balancer can now freely distribute requests across multiple instances without issue, highlighting the newfound architectural freedom.

You can put MCP servers behind any off-the-shelf round-robin load balancer, because it no longer matters which instance receives a request. A single instance crashing stops being catastrophic, since the balancer just routes the next request to a healthy instance that can serve it with no knowledge of the failed one. The workarounds disappear: you can drop sticky-session rules and decommission the shared session store, along with the latency it added. And statelessness is the native shape of serverless platforms like Cloudflare Workers, AWS Lambda, and Cloud Run, so an MCP server can scale to zero when idle and spin up per request, which the old persistent-session model could not do.

Managing state on a stateless protocol

Removing sessions from the protocol does not force your application to be stateless. It moves responsibility for state from the transport up to the application layer, where it is easier to reason about. The spec provides clean patterns for the three cases that used to lean on sessions.

Explicit handles

If your application needs continuity across calls, mint an explicit handle and have the model thread it through, exactly as REST APIs have always done. Say a user is building a shopping basket. The model calls a create_basket tool, the server creates a record and returns an id:

 
{ "basket_id": "b_47f2" }

When the user adds an item, the model calls add_item and passes that id as an ordinary argument:

 
add_item(basket_id: "b_47f2", sku: "widget-123")

The server looks up the basket by id, adds the item, and responds. The state lives in your backend, and the basket_id is the visible handle that references it. The maintainers note this works better than hidden session state precisely because the model can see the handle and pass it between tools.

Multi-round-trip requests for user input

Sometimes a tool needs something from the user mid-call, like a confirmation before a destructive action. On a stateless protocol you cannot hold a stream open waiting for an answer, so the spec introduces Multi Round-Trip Requests (SEP-2322), which replace the old server-initiated flows for elicitation, sampling, and roots listing.

A screenshot of the input-required result, highlighting the resultType, the requests to be answered, and the requestState token.

The flow is a clean request and reply. A client calls, say, a delete_files tool. The server decides it needs confirmation, so instead of acting it returns a result with resultType: "input_required", carrying the questions it needs answered and an opaque state token (the spec's requestState) that holds the context needed to resume. The client shows the prompt, collects the answer, and retries the exact same call, this time attaching the answers in inputResponses and returning the state token untouched. Because the token carries the context, any server instance can pick up the retry, apply the confirmation, and complete the action. The user is only ever prompted in response to something they initiated, and the state to continue travels with the request.

Long-running work with the Tasks extension

For operations that outlast a normal HTTP timeout, such as processing a large file or running a refund, holding a connection open is a bad idea. Tasks, now promoted into an official extension (io.modelcontextprotocol/tasks, SEP-2663), handle this asynchronously.

The client calls a long-running tool. The server records the task with a status, kicks off the work in the background, immediately returns a task handle (a taskId), and closes the connection. The conversation is no longer blocked. The client polls at its own pace with tasks/get, which reports the current status, and once the background job finishes, a later tasks/get returns the final result. A companion tasks/update is also available. This decouples the call from its execution, so long jobs never hold a connection or the user hostage.

Migrating

A change this deep comes with breaking changes, but the maintainers shipped a clear path and a generous timeline. Nothing forces a one-shot rewrite.

Deprecations to know

Several features are deprecated alongside sessions. Roots, Sampling, and Logging are deprecated via SEP-2577; they still work and will keep working for at least twelve months, but new implementations should not adopt them, and the server-initiated versions of sampling, elicitation, and roots listing are now handled through Multi Round-Trip Requests instead. The legacy HTTP and SSE transport is also deprecated with a year-long offramp. On the authorization side, the spec hardens security (including RFC 9207 issuer validation) and formally deprecates Dynamic Client Registration in favor of Client ID Metadata Documents, again with backward compatibility during the transition. The new deprecation policy guarantees a minimum twelve-month window between deprecation and removal, so you can schedule upgrades instead of reacting to them.

Updating the SDKs

All four Tier 1 SDKs (TypeScript, Python, Go, and C#) now speak 2026-07-28. The TypeScript SDK sees the largest restructure: v2 is the stable release line, and it replaces the old monolithic @modelcontextprotocol/sdk package with side-specific packages, primarily @modelcontextprotocol/server and @modelcontextprotocol/client, plus @modelcontextprotocol/core and optional framework adapters like @modelcontextprotocol/express. One team reported the client-server split cut their package size by around 83 percent.

Install the side you need:

 
npm install @modelcontextprotocol/server
# or, for a client
npm install @modelcontextprotocol/client

A codemod handles the mechanical part of the migration, rewriting import paths and API names:

 
npx @modelcontextprotocol/codemod@latest v1-to-v2 .

Two things are worth knowing before you start. First, moving to SDK v2 and speaking the new protocol are separate steps: a v2 client or server keeps speaking the 2025-era protocol by default, and serving 2026-07-28 is an explicit opt-in. Second, the v1 and v2 packages have different names, so they can coexist in one project. The recommended path, per the v2 migration guide, is incremental: add the v2 packages while keeping @modelcontextprotocol/sdk, migrate directory by directory, then remove the v1 dependency once nothing imports it. v1.x continues to get bug and security fixes for at least six months after v2's release.

Final thoughts

Moving MCP to a stateless core is less a tweak than a realignment with how the rest of the web is built. By shedding handshakes, sessions, and held-open connections, the protocol becomes simpler to operate, more resilient to instance failure, and far easier to scale on ordinary HTTP infrastructure, right down to serverless. Applications that still need state get clear, explicit patterns for it: server-minted handles, multi-round-trip requests for user input, and the Tasks extension for long-running work.

The upgrade takes real effort, since the biggest changes are breaking ones, but the twelve-month deprecation window and the incremental SDK path make it manageable rather than urgent. For anything you are deploying at scale, the payoff is simpler infrastructure, lower cost, better fault tolerance, and a serverless-friendly foundation. When you are ready for exact details, the full changelog and the migration guides are the authoritative references.

Got an article suggestion? Let us know
Licensed under CC-BY-NC-SA

This work is licensed under a Creative Commons Attribution-NonCommercial-ShareAlike 4.0 International License.