MCP Stateful-to-Stateless Migration: The 2026 Protocol
A practical migration guide from MCP's initialize-and-session era to the stateless 2026-07-28 protocol, with compatibility and state-design patterns.
On this page
Version verifiedChecked August 25, 2026 against MCP 2026-07-28. Review method.
The short version
- MCP 2026-07-28 removes initialize, initialized, and protocol-level sessions; version and capabilities now travel on every request.
- Stateless protocol does not mean stateless applications: cross-call data uses explicit handles, durable tasks, or ordinary application storage.
- MRTR replaces independent server-to-client requests, while subscriptions/listen replaces the standalone HTTP GET event channel.
- Dual-era clients and servers can preserve compatibility, but must negotiate deliberately and never mix modern and legacy wire rules.
The 2026-07-28 release is the clearest dividing line in MCP’s history. Earlier revisions opened a connection with initialize, negotiated capabilities once, and expected the server to remember that context. Streamable HTTP could add an Mcp-Session-Id tying later requests to server-held state. The modern protocol removes both the handshake and the protocol session.
The host, client, and server roles that remain after statelessness are logical architecture boundaries, not evidence that the old connection-scoped state survived.
The result is a new wire era, not a cosmetic transport update. The official 2026-07-28 release announcement describes a stateless core designed for ordinary horizontal scaling S058, while the current changelog lists the breaking changes S055. The MCP vs function calling guide explains why this protocol-state change does not make either layer equivalent to an agent loop.
The legacy lifecycle stored protocol context on a connection
Through the 2025-11-25 specification, an MCP connection began with a three-message lifecycle S048:
Client Server
|--- initialize ---------------------------->|
| preferred version, capabilities, info |
|<-- initialize result ----------------------|
| selected version, capabilities, info |
|--- notifications/initialized ------------>|
|--- subsequent requests ------------------->|
The server interpreted later requests using facts established during initialization. On 2025-era Streamable HTTP, it could mint an Mcp-Session-Id, require the client to echo it, retain a standalone GET stream for server messages, and accept DELETE to end the session. Resumable SSE used Last-Event-ID to reconnect to retained stream state.
That model worked, but it coupled traffic to memory held by a particular server instance. A load balancer needed affinity or shared session storage. Losing an instance meant losing its sessions and in-flight stream state. Servers also had to create, expire, and garbage-collect protocol sessions even when their business operation was a simple read.
The accepted stateless MCP proposal framed the problem as scalability, resilience, and implementation complexity S061. Its design principle was “pay as you go”: keep ordinary requests self-contained, pass references when application state is needed, and accept long-lived state only for a feature that truly requires it.
The modern lifecycle makes each request self-contained
In the current base protocol, there is no mandatory opening exchange S054. A client may call server/discover to learn the server’s supported versions and capabilities, but it can also send an ordinary method immediately.
Optional discovery
Client Server
|--- server/discover ----------------------->|
|<-- versions + capabilities ----------------|
Ordinary call
|--- tools/call ---------------------------->|
| _meta.protocolVersion |
| _meta.clientCapabilities |
|<-- complete result -------------------------|
Every request contains io.modelcontextprotocol/protocolVersion and io.modelcontextprotocol/clientCapabilities in params._meta. clientInfo should normally be present but is optional in the final specification. Each result should normally carry io.modelcontextprotocol/serverInfo in its _meta; it too is optional and self-reported.
On Streamable HTTP, MCP-Protocol-Version mirrors the body version, Mcp-Method mirrors the method, and Mcp-Name mirrors the relevant tool, prompt, or resource identifier. Those duplicated values must agree so infrastructure can safely route or authorize on headers. There is one POST endpoint, with no modern GET, DELETE, session ID, or replay cursor. The current transport specification defines that binding S057; the transport guide compares all three historical HTTP shapes.
The server processes each request independently. It must not infer capabilities, version, identity, task, conversation, or authorization from another request using the same connection or stdio process. An open process is a delivery channel, not a conversation.
Application state survives through explicit references
“MCP is stateless” does not mean a server cannot maintain a shopping cart, database transaction, browser context, upload, workflow, or long-running job. It means the protocol does not hide that continuity in connection state. The current changelog says servers needing cross-call state should mint explicit handles and accept them as ordinary arguments S055.
tools/call: begin_import
result: { "importHandle": "imp_7f3a" }
tools/call: add_file
arguments: { "importHandle": "imp_7f3a", "uri": "..." }
tools/call: commit_import
arguments: { "importHandle": "imp_7f3a" }
The server may store data behind imp_7f3a in memory, a database, an object store, or another service. Any instance can handle the next request if it can resolve the handle. The handle should be scoped, unguessable where necessary, authorized on every use, expired deliberately, and never treated as proof of user identity by itself.
Three modern patterns cover other kinds of continuity:
- MRTR for input during one logical operation. A server returns
resultType: "input_required"withinputRequests. The client obtains elicitation, sampling, or roots input and retries the original request withinputResponses. Server-specific correlation data can travel inrequestState. See the MRTR proposal S062. subscriptions/listenfor change events. A client explicitly opts into tool-list, prompt-list, resource-list, or named-resource updates. HTTP carries them on the response stream of the POSTed listen request; stdio tags them with a subscription ID. Reconnect by listening again.- Tasks for durable long-running work. Tasks first appeared in the 2025-11-25 core S050, then moved into the
io.modelcontextprotocol/tasksextension. The redesigned Tasks extension uses explicit task handles and polling, includingtasks/getandtasks/updateS060. It is appropriate when work must outlive a broken response stream.
These mechanisms make state visible in data contracts. They also force a useful design decision: is continuity request-scoped, subscription-scoped, task-scoped, or application-scoped?
Migration requires more than deleting a session header
Start by inventorying every place the implementation relies on prior traffic. Common dependencies include cached client capabilities, negotiated log level, an in-memory map keyed by Mcp-Session-Id, server-to-client JSON-RPC requests, the standalone GET stream, resource subscriptions, and Last-Event-ID replay.
Then migrate each concern deliberately:
- Accept the modern request envelope. Validate per-request protocol version and client capabilities. Treat absent required metadata as invalid params rather than borrowing values from a connection.
- Implement
server/discover. Advertise supported modern versions and server capabilities. Cache hints may let clients avoid repeating discovery unnecessarily. - Make results era-correct. Modern results carry
resultType; use"complete"for ordinary results and"input_required"for MRTR. A 2026 server must emit the field. For backward compatibility, clients still treat an absentresultTypefrom an earlier-protocol server as"complete". - Remove hidden protocol state. Replace session-backed business data with an explicit, authorized handle or an extension designed for durability.
- Replace server-originated requests. Convert sampling, elicitation, and roots requests to MRTR responses and retry handling.
- Replace event delivery. Move change notifications to
subscriptions/listen; keep request progress on that request’s own response stream. - Update Streamable HTTP routing. Require POST, add and validate the modern headers, return
405for GET/DELETE on a modern-only endpoint, and ignore legacyMcp-Session-Idrather than creating a session. - Revisit failure behavior. A dropped stream cancels an in-flight request. Retry with a new request ID when safe; use an idempotency key, explicit operation handle, or Tasks when duplicate side effects would be dangerous.
- Authenticate every request. Do not let a successful earlier handshake, connection reuse, or self-reported
clientInfostand in for current authorization.
Test with at least two backend instances behind a non-sticky load balancer. Interleave unrelated calls over one stdio process. Restart a server between calls that share an application handle. Break response streams. These tests expose hidden connection assumptions better than a happy-path single-process demo.
Dual-era support needs an explicit negotiation branch
Migration does not require dropping all existing clients on day one. The versioning and compatibility rules permit clients and servers to support both eras S056. The important constraint is to select an era and keep its rules internally consistent.
A dual-era server can answer server/discover and modern self-contained requests while continuing to implement initialize and 2025 behavior for legacy clients. Legacy requests may still need session state; modern requests must not. Keep those paths separate in code, metrics, and tests.
A dual-era stdio client should probe with server/discover. A discovery result identifies a modern server. A recognized modern unsupported-version error means the peer is modern and the client should choose an advertised version. Other errors or a reasonable timeout may lead to the legacy initialize handshake.
An HTTP client can attempt a modern POST first, but must inspect error bodies before falling back. A structured modern error for a header mismatch, missing capability, or unsupported version is not evidence of a legacy server. Once an era is selected, do not send initialize into a modern flow or attach Mcp-Session-Id to try to make a modern request stateful.
Publish this compatibility contract. “Supports Streamable HTTP” is insufficient because both eras use that name. State the MCP revisions accepted, whether legacy HTTP+SSE is available, which extensions are enabled, and the planned retirement date for legacy paths.
Removed and deprecated features are different
Some old mechanisms are absent from the 2026-07-28 core; others remain specified but are in a deprecation window. Conflating those states leads either to broken modern implementations or premature removal of compatibility code.
| Status in the modern era | Feature | Modern replacement or direction |
|---|---|---|
| Removed from modern core | initialize / notifications/initialized | Per-request metadata; optional server/discover call by clients |
| Removed from modern HTTP | Mcp-Session-Id, GET, DELETE, Last-Event-ID replay | Self-contained POSTs, explicit handles, subscriptions/listen, Tasks |
| Removed from modern core | Independent server-to-client JSON-RPC requests | MRTR InputRequiredResult and retry |
| Removed from modern core | resources/subscribe / resources/unsubscribe | Resource URIs in subscriptions/listen |
| Removed from modern core | ping, logging/setLevel | Ordinary RPC/transport health; per-request log level |
| Deprecated, still specified | Roots, Sampling, Logging | Explicit arguments/resources/config; provider APIs; stderr or OpenTelemetry |
| Deprecated | HTTP+SSE transport | Streamable HTTP |
| Deprecated | OAuth Dynamic Client Registration | Client ID Metadata Documents |
The current deprecated-features registry lists feature-specific migration paths, while the project’s feature lifecycle policy defines statuses and removal windows S065. Deprecation means new implementations should not adopt the feature, but it remains part of the specification during its window. Removed-from-modern means a 2026-07-28 request cannot use that old mechanism even if the same product still supports it on a legacy connection.
The migration is complete when any instance can understand a modern request without connection history, all continuing state has an explicit scope and identifier, and legacy behavior exists only behind an intentional compatibility branch. That is the operational promise of stateless MCP: not “no state,” but no invisible protocol state.