If you built an MCP server in 2025, you learned a specific choreography: the client sends initialize, you negotiate a protocol version and capabilities, the client sends notifications/initialized, and — over HTTP — you hand back an Mcp-Session-Id header that the client echoes on every subsequent request. Maybe you wired up the standalone GET endpoint so you could push notifications, and SSE event IDs so a dropped stream could resume via Last-Event-ID.

The current spec revision, 2026-07-28, deletes all of it. No initialize handshake. No protocol-level sessions. No GET stream. No resumability. Every request now stands alone. This is one of the more instructive protocol redesigns in recent memory, because it's a case study in what happens when a stateful protocol collides with the stateless infrastructure of the web — and what you have to give up to fix that.

What the old design actually required

The 2025-06-18 revision defined a "rigorous lifecycle": initialization MUST be the first interaction, the client MUST send initialize with its protocol version and capabilities, the server MUST respond with its own, and the client MUST follow up with notifications/initialized before normal traffic. The negotiated result — version, capabilities, session — was connection state that both sides had to remember.

Over the Streamable HTTP transport, that state materialized as the Mcp-Session-Id header. The rules were strict:

  • The server MAY assign a session ID on the InitializeResult; if it does, the client MUST send it on every subsequent request.
  • Servers requiring a session SHOULD reject requests without one with HTTP 400.
  • If the server terminates a session, it MUST answer that session ID with HTTP 404 — and the client MUST then start over with a fresh InitializeRequest.
  • Clients SHOULD send an HTTP DELETE to end a session explicitly.

On top of that sat a second stateful mechanism: the client could open a long-lived GET request to receive an SSE stream of server-initiated messages — including full JSON-RPC requests flowing backwards, server to client, for sampling and elicitation. And because streams break, the spec layered on resumability: servers could attach SSE event IDs (globally unique within a session, per-stream cursors), and a client could reconnect with Last-Event-ID to get missed messages replayed.

Each piece is individually reasonable. Together they mean an MCP server is a stateful service: it must route a session's requests to whatever holds that session's memory, keep a long-lived stream open per client, and buffer sent messages in case a client asks for redelivery. That's a miserable fit for the way HTTP services actually scale — load-balanced fleets, serverless functions, and gateways that assume any replica can serve any request. The session ID also leaked into semantics: the changelog notes that under the old model, list endpoints like tools/list could vary per-connection, which defeats shared caching.

The new shape: one request, everything included

The 2026-07-28 revision (SEP-2575) removes the handshake and moves what it negotiated into every request's _meta:

json
{
  "jsonrpc": "2.0",
  "id": 1,
  "method": "tools/call",
  "params": {
    "name": "get_weather",
    "arguments": { "location": "Seattle, WA" },
    "_meta": {
      "io.modelcontextprotocol/protocolVersion": "2026-07-28",
      "io.modelcontextprotocol/clientInfo": { "name": "ExampleClient", "version": "1.0.0" },
      "io.modelcontextprotocol/clientCapabilities": {}
    }
  }
}

There is no negotiation round-trip. The server accepts or rejects each request independently: if it doesn't support the declared version, it returns an UnsupportedProtocolVersionError (code -32022) listing the versions it does support, and the client retries with one of those. Clients that want to pick a version up front can call server/discover — a new mandatory RPC returning the server's supported versions, capabilities, and identity — but calling it is optional.

Session state didn't just move; it was demoted out of the protocol entirely (SEP-2567). Servers that need cross-call state now use explicit, server-minted handles passed as ordinary tool arguments — the same pattern as a database cursor or an upload ID in any REST API. The transport no longer knows or cares.

Every server-to-client channel got redesigned

Removing sessions forced a rethink of everything that depended on a persistent connection:

2025 mechanism2026-07-28 replacement
initialize handshakePer-request _meta + server/discover
Mcp-Session-Id headerServer-minted handles as tool arguments
Standalone GET SSE streamsubscriptions/listen request
Server-initiated JSON-RPC requests (sampling, elicitation, roots)Multi Round-Trip Requests (MRTR)
Last-Event-ID resumabilityNone — re-issue the request
notifications/cancelled over HTTPClosing the SSE stream is cancellation

The most interesting replacement is MRTR (SEP-2322). Previously, a server that needed user input mid-tool-call would send its own elicitation/create request back down an SSE stream — an inversion that only works if a bidirectional channel exists. Now the server simply returns early: it answers the tool call with an InputRequiredResult (resultType: "input_required") whose inputRequests field carries what it needs. The client gathers the input and retries the original request with matching inputResponses. The interaction becomes a sequence of plain request/response pairs — every one of them routable to any replica. To support this, all results now carry a required resultType field, with "complete" for ordinary results.

Change notifications survived, but opt-in: a client that wants tools/list_changed or resource updates POSTs a subscriptions/listen request, and the response to that request is a long-lived SSE stream carrying only the notification types the client asked for. Request-scoped notifications like notifications/progress still flow on the response stream of the request they relate to — streams didn't disappear, they just became strictly request-scoped.

One more consequence of statelessness: the transport now mirrors routing-relevant fields into HTTP headers (MCP-Protocol-Version, Mcp-Method, Mcp-Name, and optionally Mcp-Param-* via the x-mcp-header schema extension), so load balancers and gateways can route on a tool name without parsing JSON-RPC bodies. Servers MUST validate that headers match the body and reject mismatches with a -32020 HeaderMismatch error — otherwise an attacker could route on one value and execute another.

What it costs

Statelessness wasn't free, and the spec is candid about the bill:

  • Lost work on disconnect. With Last-Event-ID gone, a broken response stream loses the in-flight request; the client MUST re-issue it as a new request with a new ID. If your tool call ran eight minutes before the stream died, you run it again — or build task-style polling via the io.modelcontextprotocol/tasks extension, which was moved out of core for exactly this reason.
  • Retries carry the payload. MRTR means the original request (plus accumulated inputResponses) is resent on every round trip, and servers correlate across retries with their own requestState.
  • Fat requests. Version, capabilities, and client info ride on every single call rather than once per session — the price of any-replica routability, and the same tradeoff HTTP itself made with cookies and tokens.
  • A compatibility era split. The spec now defines "modern" (2026-07-28+) and "legacy" (2025-11-25 and earlier) eras with a full client/server compatibility matrix. A modern-only server answering a legacy client is a hard failure — legacy clients have no fall-forward — so real deployments need dual-era servers that answer initialize with legacy semantics while serving per-request-metadata traffic statelessly on the same endpoint.

The deprecations tell the same story from another angle: Roots, Sampling, and Logging — the three features most entangled with server-initiated requests and connection-scoped state — are all now deprecated (SEP-2577), with migrations pointing to tool parameters, direct LLM provider integration, and stderr/OpenTelemetry respectively.

If you maintain an MCP server, the practical takeaway: stop minting session IDs, treat every request as complete in itself, move any cross-call state into handles your tools hand out explicitly, and implement server/discover. If you maintain a client, probe modern-first and inspect 400 bodies before falling back — a modern error body means retry smarter, not fall back. And if you design protocols: MCP just re-learned, in eighteen months of shipped revisions, why the web went stateless thirty years ago.

References