Migration guideChecked for spec 2026-07-28

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.

Modern means revision 2026-07-28 and later stateless-style revisions. Legacy means 2025-11-25 and earlier handshake-style revisions. A dual-era implementation may support both, but one request follows one era's rules.

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" with inputRequests. The client obtains elicitation, sampling, or roots input and retries the original request with inputResponses. Server-specific correlation data can travel in requestState. See the MRTR proposal S062.
  • subscriptions/listen for 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/tasks extension. The redesigned Tasks extension uses explicit task handles and polling, including tasks/get and tasks/update S060. 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:

  1. 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.
  2. Implement server/discover. Advertise supported modern versions and server capabilities. Cache hints may let clients avoid repeating discovery unnecessarily.
  3. 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 absent resultType from an earlier-protocol server as "complete".
  4. Remove hidden protocol state. Replace session-backed business data with an explicit, authorized handle or an extension designed for durability.
  5. Replace server-originated requests. Convert sampling, elicitation, and roots requests to MRTR responses and retry handling.
  6. Replace event delivery. Move change notifications to subscriptions/listen; keep request progress on that request’s own response stream.
  7. Update Streamable HTTP routing. Require POST, add and validate the modern headers, return 405 for GET/DELETE on a modern-only endpoint, and ignore legacy Mcp-Session-Id rather than creating a session.
  8. 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.
  9. Authenticate every request. Do not let a successful earlier handshake, connection reuse, or self-reported clientInfo stand 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.

Removed and deprecated features are different comparison table
Status in the modern eraFeatureModern replacement or direction
Removed from modern coreinitialize / notifications/initializedPer-request metadata; optional server/discover call by clients
Removed from modern HTTPMcp-Session-Id, GET, DELETE, Last-Event-ID replaySelf-contained POSTs, explicit handles, subscriptions/listen, Tasks
Removed from modern coreIndependent server-to-client JSON-RPC requestsMRTR InputRequiredResult and retry
Removed from modern coreresources/subscribe / resources/unsubscribeResource URIs in subscriptions/listen
Removed from modern coreping, logging/setLevelOrdinary RPC/transport health; per-request log level
Deprecated, still specifiedRoots, Sampling, LoggingExplicit arguments/resources/config; provider APIs; stderr or OpenTelemetry
DeprecatedHTTP+SSE transportStreamable HTTP
DeprecatedOAuth Dynamic Client RegistrationClient 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.

Evidence11 cited primary or authoritative sources
Last reviewedAugust 25, 2026
How we research