Nine days ago the Model Context Protocol shipped its largest revision since launch. The 2026-07-28 specification is final, all four Tier 1 SDKs speak it, and the stateless core that was a roadmap item when we wrote our July 25 production server guide is now the protocol [1]. If you run a remote MCP server, the initialize handshake, the Mcp-Session-Id header, and the open-stream model your deployment was built around are gone from the wire.

This post covers three things: what the release candidate promised and what actually landed on July 28, what breaks in existing servers, and the nine-step migration checklist we’d run against any production server today [4].

From stateful to stateless: why the rewrite happened

The statefulness complaint is as old as remote MCP. The original stateless proposal, SEP-1442 (opened September 8, 2025), framed it directly: “a simple stateless load balancer cannot be used, as it would route a client’s requests to different backend servers” without proper session state [5]. Every earlier MCP transport began with an initialize/initialized exchange that started a session, and every subsequent request had to find the state associated with its Mcp-Session-Id header [2]. In practice that meant autoscaling infrastructure had to preserve active sessions, deployments had to drain or migrate them, losing an instance forced clients to reconnect, and a single server restart blew up every in-flight session [2][5]. Cloudflare’s analysis puts it bluntly: MCP took a stateful model that worked for local stdio processes and “transposed it onto web infrastructure” — sticky sessions, held-open streams, and message replay overhead that a traditional web server never has to think about [2].

The 2026-07-28 spec removes the problem instead of managing it.

The stateless core: SEP-2575 and SEP-2567

Two SEPs carry the weight. SEP-2575 retires the initialize/initialized handshake; SEP-2567 removes the Mcp-Session-Id header and protocol-level sessions entirely [1][3]. Every request is now self-describing: protocol version, client identity, and client capabilities travel in _meta on the JSON-RPC envelope, under keys like io.modelcontextprotocol/protocolVersion, /clientInfo, and /clientCapabilities [3]. A version mismatch returns UnsupportedProtocolVersionError instead of a failed handshake [3]. The official announcement shows the resulting request shape [1]:

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

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

Clients that want capabilities up front can call the new server/discover RPC, which servers MUST implement and which advertises supported protocol versions, capabilities, and identity — but it is optional, not a handshake [1][3]. The operational payoff is the headline: any request can land on any instance behind a plain round-robin load balancer with no shared storage, no sticky sessions, and no deep packet inspection at the gateway [1][5]. Cloudflare notes the protocol no longer needs a stateful primitive like Durable Objects to speak it at all — a plain Worker suffices [2].

Dropping the protocol session doesn’t force your application to be stateless. The official guidance: if your server needs state across calls, mint an explicit handle from a tool and have the model pass it back as an argument [1]. The migration guide’s before/after makes the pattern concrete [4]:

# Before: state hangs off the protocol session (removed by SEP-2567)
sessions[mcp_session_id]["document"] = doc

# After: the tool mints an explicit handle...
return {
    "content": [{"type": "text", "text": json.dumps({"document_id": "doc_8f3a"})}],
}

# ...and every later call passes it back as a normal argument:
# tools/call { "name": "append_page",
#              "arguments": { "document_id": "doc_8f3a", ... } }

The list endpoints (tools/list, resources/list, prompts/list) also no longer vary per connection, which is what makes client-side catalog caching safe [3].

MRTR: interactivity without open streams

The hardest problem for a stateless protocol is server-initiated interaction — elicitation (asking the user for input mid-call), sampling (asking the client for an LLM completion), and roots management all previously required a held-open bidirectional stream [1]. Multi Round-Trip Requests (SEP-2322) rework them. Under SEP-2260, a server may only send requests to the client while it is actively processing a client request — the long-lived GET SSE stream as a push channel is over [4].

The new pattern: a tools/call, prompts/get, or resources/read can return resultType: "input_required" instead of completing, carrying an inputRequests map of the server-initiated requests it needs answered plus an opaque requestState [1][3]. The client gathers the answers and retries the original call with inputResponses keyed identically, echoing requestState back unmodified [3]. Because all state rides in the payload, any stateless instance can resume the work — and one InputRequiredResult can batch an elicitation and a sampling request into a single round trip [3].

{
  "jsonrpc": "2.0",
  "id": 7,
  "result": {
    "resultType": "input_required",
    "requestState": "st_9f2c1a",
    "inputRequests": {
      "confirm": {
        "method": "elicitation/create",
        "params": {
          "action": "accept",
          "content": {
            "type": "text",
            "text": "Approve $1,240 invoice payment to Acme Corp?"
          }
        }
      }
    }
  }
}

The client answers and re-issues the original call:

{
  "jsonrpc": "2.0",
  "id": 8,
  "method": "tools/call",
  "params": {
    "name": "pay_invoice",
    "arguments": { "invoice_id": "inv_7712" },
    "inputResponses": { "confirm": { "action": "accept" } },
    "requestState": "st_9f2c1a"
  }
}

Cloudflare calls this a breaking change from the old elicitation flow but “operationally much simpler to implement,” and it unlocks interactivity for stateless deployments that previously couldn’t support it — Supabase, which runs its MCP statelessly, cites MRTR as the thing that finally lets its tools confirm costs before creating a project or confirm before a destructive query [1][2]. The migration constraint for your tools: design every tool to complete within a single request, and express genuinely interactive flows as input_required round trips [4].

Header-based routing: SEP-2243

Streamable HTTP requests must now include Mcp-Method (e.g. tools/call) and Mcp-Name (the tool or resource name) headers [1]. A gateway, rate limiter, or WAF can route, meter, and authorize on those headers without parsing a JSON body — and servers must reject requests where the headers and body disagree [1][4]. The same SEP adds x-mcp-header for passing custom headers from tool parameters [3]. This is the change that lets a server run behind a plain round-robin load balancer instead of a deep-packet-inspection gateway [3].

The trap is infrastructure, not code: a WAF, reverse proxy, or firewall rule that strips or blocks unknown Mcp-* headers will fail every request, and from the outside it will look like a client bug [4]. Walk the full request path — CDN, WAF, proxy, load balancer — and confirm the headers pass through untouched [4]. Every gateway vendor at the AAIF MCP Dev Summit in April 2026 (Kong, Docker, Solo.io, Uber’s internal platform) was reverse-engineering tool names out of request bodies; SEP-2243 is the reason that work disappears [5].

Cacheable list results: SEP-2549

Responses from tools/list, prompts/list, resources/list, and resources/read now carry ttlMs (a freshness hint in milliseconds) and cacheScope ("public" or "private", controlling whether shared intermediaries may cache) [1][3]. The model is borrowed from HTTP Cache-Control and complements the existing listChanged notifications rather than replacing them [3]. Tool catalogs are deterministically ordered, so clients can cache them and keep upstream prompt caches stable across reconnects [1][2]. For a stateless protocol where list endpoints are connection-independent, this is how clients stop re-fetching the same catalog on every call [3].

Authorization hardening

Six SEPs tighten the OAuth story. Clients must validate the iss parameter on authorization responses per RFC 9207 before redeeming a code (SEP-2468) — closing the authorization-server mix-up hole [1][2]. Clients declare application_type during registration so authorization servers stop rejecting localhost redirects for desktop and CLI apps (SEP-837) [1]. Client credentials are bound to the issuer that minted them, so clients must key persisted credentials by issuer and re-register when the authorization server changes (SEP-2352) [1][3]. And clients now send the canonical server URI as the RFC 8707 resource in authorization and token requests, so tokens are issued for, and accepted only by, that audience [2].

The headline: Dynamic Client Registration is formally deprecated in favor of Client ID Metadata Documents (CIMD). DCR keeps working for backward compatibility but is slated for removal after summer 2027 [1][2]. On the server side, that means publishing accurate RFC 9728 protected-resource metadata with stable authServerUrls, keeping your issuer stable — an issuer change now forces every client to re-register — and validating audience-bound tokens rather than accepting anything your authorization server ever minted [4].

Tasks becomes an extension: SEP-2663

The experimental Tasks API from 2025-11-25 moves out of the core and into the io.modelcontextprotocol/tasks extension with a changed lifecycle: tools/call returns a task handle, the client drives the task through poll-based tasks/get, a new tasks/update for client-to-server input, and tasks/cancel; blocking tasks/result is gone and tasks/list is removed entirely — listing tasks across clients has no well-defined scope without sessions [1][3][4][5]. Change notifications move from the old HTTP GET endpoint to a single subscriptions/listen stream clients opt into per notification type [1]. If you built on the experimental API, plan a real port — the lifecycle changed, it wasn’t just relocated [4].

Deprecations: Roots, Sampling, Logging, and the error code

SEP-2577 deprecates Roots, Sampling, and Logging — they still work, and will for at least twelve months, but new implementations shouldn’t adopt them [1]. The designated replacements: Roots → tool parameters and configuration; Sampling → direct LLM provider APIs; Logging → stderr and OpenTelemetry [4]. The legacy HTTP+SSE transport is also officially deprecated with a year-long offramp [1][3].

Two subtler breaks: the resource-not-found error code moves from the custom -32002 to the standard JSON-RPC -32602 (SEP-2164) — grep your server, in-house clients, and every test suite for a hardcoded -32002, and treat error semantics, not magic numbers, as the contract [4]. And the whole deprecation story now runs under SEP-2596, MCP’s first formal lifecycle policy: Active → Deprecated → Removed, with a guaranteed minimum twelve-month window between deprecation and removal [3][4].

Operability: tracing and schemas

Two additive changes worth adopting on their own merits. SEP-414 documents W3C trace context propagation in _meta with fixed traceparent, tracestate, and baggage keys — OpenTelemetry collectors see MCP traffic end-to-end with no glue code [4][5]. And SEP-2106 brings full JSON Schema 2020-12 for tool input and output schemas, with the type: "object" root still required for inputs [4][6].

RC vs final: what actually landed

The release candidate locked on May 21, 2026, and the ten-week gap was a validation window for SDK maintainers and client implementers against a frozen change set [4][8]. The final publication confirmed that change set — with one addition worth knowing: every result now carries a required resultType field, "complete" for ordinary results and "input_required" for MRTR interim results, with results from earlier-protocol servers treated as complete (SEP-2322) [3]. SDK betas went GA on July 28 across TypeScript, Python, Go, and C#, with the Rust SDK supporting the new spec in beta [1]. Cloudflare’s Agents SDK shipped support the day before final (v0.20.0, July 27, 2026), and its migration path from McpAgent to createMcpHandler ran against the release candidate with production traffic [2]. The official build-server docs now target the new spec and require the Python SDK 2.0.0+ [7].

The scale signal behind the rewrite: the Tier 1 SDKs see close to half a billion downloads a month, and both TypeScript and Python crossed 1 billion total downloads [1].

The 9-step migration checklist

This is the checklist from the MCP Migration Studio guide, which they run against every server they migrate — including their own [4]:

  1. Remove session-keyed state. Grep for every read of Mcp-Session-Id and replace it with explicit handles passed as tool arguments [4].
  2. Make every handler self-contained. No in-memory state that has to survive between requests [4].
  3. Walk the request path (CDN, WAF, proxy, firewall) and confirm Mcp-* headers pass through unstripped [4].
  4. Confirm every tool completes within one request. Rebuild push-style flows as input_required round trips [4].
  5. Grep server, clients, and tests for hardcoded -32002 — expect -32602 [4].
  6. Stay off the 2025-11-25 experimental Tasks API — and if you’re on it, plan the port to the extension lifecycle [4].
  7. Audit auth: RFC 9728 protected-resource metadata accurate, authServerUrls and issuer stable, audience-bound token validation in place [4].
  8. Remove — or schedule the removal of — Roots, Sampling, and MCP Logging in favor of their replacements [4].
  9. Bump the SDK. The guide’s original advice was to do this inside the validation window; that window has closed, which makes the follow-on warning sharper — if you migrate now you do it on your own schedule, and if you wait, you do it on someone else’s [4].

None of this removes anything on day one — a remote server shipped against 2025-11-25 keeps working, and current clients keep speaking it [4]. But the twelve-month clocks on sessions, the handshake, Roots, Sampling, and Logging are already running, and spec-tracking clients and directories are already checking against the new version [4]. The stateless rewrite landed; the migration is now a schedule you control or one that controls you.

Sources

  • Hermes Tutorials — Hermes Agent setup, configuration, and advanced workflows
  • ToolBrain — tool reviews, LLM comparisons, and AI workflow guides
  • CodeIntel Log — code quality, debugging, and software engineering benchmarks

Cross-links automatically generated from NiteAgent.

← Back to all posts