Migrate Your MCP Server to the 2026-07-28 Stateless Spec (Before It Ships) — Cesar Ayala
← All posts

Migrate Your MCP Server to the 2026-07-28 Stateless Spec (Before It Ships)

The MCP 2026-07-28 spec goes stateless: SEP-2575 removes the initialize/initialized handshake, so protocol version, client info, and capabilities ride in _meta on every request. Your remote server drops sticky sessions and runs behind a plain round-robin load balancer. Tasks becomes an extension; tasks/list is removed; -32002 moves to -32602. The final ships July 28, 2026.

What just changed in MCP, and what’s the deadline?

MCP goes stateless: the initialize handshake (SEP-2575) and protocol-level sessions (SEP-2567) are removed, and per-request _meta replaces them. The 2026-07-28 Release Candidate is out now and the final ships July 28, 2026 — the largest revision since the Model Context Protocol (MCP, the open standard that lets AI agents talk to your tools and data) launched.

The headline lands straight in your deployment. The protocol version, client info, and client capabilities — things you used to exchange exactly once at connect — now ride in _meta on every request (SEP-2575), and the per-connection session that pinned a client to one box is gone (SEP-2567). Your remote MCP server no longer needs sticky sessions, a shared session store, or gateway deep-packet-inspection. It can run behind a plain round-robin load balancer.

This is not a “what is MCP” intro — for that, see my piece on connecting an AI agent to your data with MCP. This is the migration map. If you ship a remote MCP server, the ground moved under you and you need a plan — the ordered version is in the migration section below.

Before you reach for the panic button: there’s a 10-week validation window for SDK maintainers and a 12-month deprecation runway that takes you to roughly mid-2027. So this is a migration to schedule, not an emergency to firefight. The claims here come straight from the MCP blog — 2026-07-28 Release Candidate and the MCP spec — confirm the current rules there, since spec details move.

Why does stateless MCP change your whole deployment?

To see why this matters, look at what the old handshake actually did. In the session-based spec, the client and server performed an initialize / initialized handshake once at connect. They exchanged the protocol version, the client info, and the client capabilities, and then held a stateful session for the whole connection. Every later request implicitly relied on that session existing on that specific server instance.

The statelessness story is a few coordinated SEPs, not one. SEP-2575 takes that connect-time state and moves it into _meta on every request — protocol version, client info, and client capabilities now ride along on each call. SEP-2567 removes the protocol-level session and the Mcp-Session-Id header that came with it — that’s the change that actually lets any instance answer any request. SEP-2243 makes the Streamable HTTP transport require Mcp-Method and Mcp-Name headers so load balancers, gateways, and rate-limiters can route on the operation without inspecting the body. And SEP-2549 gives list and read results ttlMs and cacheScope, modeled on HTTP Cache-Control, so discovery is cacheable.

The infra payoff is concrete. No sticky sessions. No shared session store (no Redis just to remember who handshook with whom). No gateway deep-packet-inspection to figure out which session a request belongs to. The server sits behind a plain round-robin load balancer and routes on the Mcp-Method and Mcp-Name headers. And if a client still needs the server’s capabilities up front instead of reading them off _meta per request, it calls the new server/discover method — the explicit replacement for the capability half of the old handshake.

Here’s an illustrative shape of _meta carried on a request. The real keys are namespaced reverse-DNS style (e.g. io.modelcontextprotocol/clientInfo); verify the exact names against your SDK, since they’re spec/SDK-specific:

{
  "jsonrpc": "2.0",
  "id": 42,
  "method": "tools/call",
  "params": {
    "name": "issue_cfdi",
    "arguments": { "rfc": "XAXX010101000" },
    "_meta": {
      "io.modelcontextprotocol/protocolVersion": "2026-07-28",
      "io.modelcontextprotocol/clientInfo": { "name": "my-agent", "version": "1.4.0" },
      "io.modelcontextprotocol/capabilities": { "tasks": {}, "elicitation": {} }
    }
  }
}

My opinion, labeled as such: this is the change that makes remote MCP boring to operate, and boring is exactly what you want in production. Stateless horizontal scaling is the unlock most teams actually needed — it’s the same reason stateless HTTP services won. If you’re running agent infrastructure at any scale, see LLMs in production for why statelessness is the right default.

Session-based spec (before)

  • initialize / initialized handshake at connect
  • Sticky sessions per connection
  • Shared session store (e.g. Redis)
  • Gateway deep-packet-inspection to route

Stateless spec 2026-07-28 (after)

  • _meta on every request (SEP-2575)
  • Sessions + Mcp-Session-Id removed (SEP-2567)
  • Mcp-Method + Mcp-Name routing (SEP-2243)
  • Cacheable tools/list via ttlMs (SEP-2549)

What’s the stateless request lifecycle look like?

Picture the new lifecycle in plain steps. The client sends _meta (protocol version, client info, capabilities) plus the method it wants. Any server instance handles it. The server responds. That’s the whole loop — no setup call, no teardown.

Because every request is self-describing, the load balancer routes on the Mcp-Method and Mcp-Name headers round-robin. No session affinity, no instance pinning, no “make sure request 2 lands on the same box as request 1.” Discovery is cacheable on top of that: the client caches tools/list per the server’s ttlMs, so repeat discovery doesn’t even reach a fresh call — it’s served from the client’s own cache. When a client genuinely needs capabilities up front, it calls server/discover once rather than re-running a handshake on every connection.

Contrast that with the old loop. Previously the first instance held the session, so every later request either had to land on the same box or replay the handshake. That’s the constraint that forced sticky sessions and shared stores in the first place. Now it’s gone.

A short illustrative sketch of a request carrying _meta plus the routing headers (verify against your gateway and SDK):

curl -X POST https://mcp.example.com/rpc \
  -H "Content-Type: application/json" \
  -H "Mcp-Method: tools/list" \
  -H "Mcp-Name: tools/list" \
  -d '{
    "jsonrpc": "2.0",
    "id": 1,
    "method": "tools/list",
    "params": {
      "_meta": {
        "io.modelcontextprotocol/protocolVersion": "2026-07-28",
        "io.modelcontextprotocol/clientInfo": { "name": "my-agent", "version": "1.4.0" }
      }
    }
  }'
Client sends _meta + methodprotocol version, client info, capabilities ride on the request
ANY server instance handles itround-robin on Mcp-Method + Mcp-Name headers, no session affinity
Server respondsno connect-time session to look up
Client caches tools/listper the server's ttlMs — repeat discovery skips the wire

Tasks is now an extension — when do you use it?

Tasks had an odd status before: it was an experimental core feature in the 2025-11-25 spec. In 2026-07-28 it becomes an extension — not core. That means it’s opt-in, declared in capabilities, and not baked into the base protocol every server must speak.

The mechanic is clean. A server can answer tools/call with a task handle instead of a finished result. The client then drives the work with tasks/get, tasks/update, and tasks/cancel. Task creation is server-directed — the server decides to hand back a task rather than block. One breaking detail to flag right here: tasks/list is removed. Don’t build on it.

This is exactly what you want for long-running work. Think a CFDI batch (CFDI is Mexico’s mandatory electronic invoice; issuing many of them through a PAC — an authorized certification provider — takes real time), or a payments reconciliation that runs for minutes. Instead of holding an HTTP request open and praying nothing times out, you return a handle and let the client poll tasks/get.

Illustrative shapes — the exact fields are SDK-specific, so verify:

// Server answers tools/call with a task handle
{
  "jsonrpc": "2.0",
  "id": 7,
  "result": {
    "task": {
      "taskId": "cfdi-batch-2026-0042",
      "status": "working"
    }
  }
}
// Client polls tasks/get until the batch finishes
{
  "jsonrpc": "2.0",
  "id": 8,
  "method": "tasks/get",
  "params": {
    "taskId": "cfdi-batch-2026-0042",
    "_meta": { "io.modelcontextprotocol/protocolVersion": "2026-07-28" }
  }
}

The pattern is the point, and it’s vendor-neutral: it works the same against any PAC or any payments processor. You’re not coupling to a provider, you’re coupling to a polling contract.

What about MCP Apps and the new auth?

Two more additive pieces are worth knowing. First, MCP Apps (SEP-1865): servers can ship interactive HTML interfaces that hosts render in a sandboxed iframe. Tools declare their UI templates ahead of time, so hosts can prefetch, cache, and security-review the UI before they ever render it.

Why builders should care: you can ship a real interface — a payment-confirmation panel, an invoice-preview before issuance — inside the host instead of dumping raw JSON at the model and hoping it renders something sensible. The model stays in the loop, but the human sees a real UI.

Second, authorization is hardened to align more closely with OAuth and OpenID Connect. Fewer bespoke auth shims, more standard flows. This ties directly to the confused-deputy risk I keep harping on: standard OAuth/OIDC alignment makes it cleaner to scope the server to the real user’s privileges instead of the server’s own. But keep the boundary straight — auth still lives in your API, not in the protocol. MCP standardizes the handshake; it does not replace your authorization layer. If agents are new to you, what is an AI agent sets the foundations.

Which breaking changes will bite you?

Here’s the precise list, with exact codes and SEP numbers, because correctness is the whole value:

  • tasks/list is removed. Any client or server relying on it breaks. Rework around tasks/get, tasks/update, and tasks/cancel.
  • The Mcp-Session-Id header is removed (SEP-2567). The protocol-level session went with it. Any code reading or setting that header breaks — this is the surface you’re rewriting off initialize + sessions.
  • Roots, Sampling, and Logging are deprecated (SEP-2577). They keep working during the runway, but plan to move off them.
  • The “resource not found” error code moves from the non-standard -32002 to the standard JSON-RPC -32602 Invalid Params (SEP-2164). Update any code matching on the old number.

And the deprecation policy, stated exactly: deprecated features keep working in every spec version published within 12 months of July 28, 2026 — a runway to mid-2027 at minimum. Treat that as your real deadline, not the RC date.

tasks/listREMOVED — rework around tasks/get|update|cancel
Mcp-Session-Id (SEP-2567)REMOVED — protocol sessions gone, any code reading it breaks
Roots / Sampling / Logging (SEP-2577)DEPRECATED — still work during the runway
Error code (SEP-2164)resource not found: -32002 → -32602

How do you migrate without breaking your agent?

Here’s the ordered plan I’d run against a production server.

Step 1 — Read the RC against your server. Inventory where you rely on the initialize handshake, on sessions and the Mcp-Session-Id header, on tasks/list, on Roots/Sampling/Logging, and on -32002. You can’t migrate what you haven’t found.

Step 2 — Move connect-time state into _meta per request. Protocol version, client info, and capabilities now travel on every call (SEP-2575), using the namespaced keys. Add a server/discover call for any client that needs capabilities up front. This is the core rewrite.

Step 3 — Drop sticky sessions and the shared session store. With Mcp-Session-Id and protocol sessions gone (SEP-2567), let any instance answer. Route round-robin on the Mcp-Method and Mcp-Name headers (SEP-2243). Cache tools/list per the server’s ttlMs (SEP-2549).

Step 4 — Adopt the Tasks extension for long jobs. CFDI batches, payments reconciliations — return a task handle, let the client poll tasks/get instead of holding a request open.

Step 5 — Fix the breaking changes. Remove tasks/list usage, stop reading Mcp-Session-Id, migrate off Roots/Sampling/Logging, change -32002 to -32602.

Step 6 — Validate in the 10-week window. Tier 1 SDKs are expected to ship support within it. Test your client-and-server interop before the final lands.

Timeline honesty: you have a 10-week validation window for SDK maintainers and client implementers, plus a 12-month deprecation runway to roughly mid-2027. So plan the migration. Don’t treat it as an emergency.

  1. Read the RC against your serverinventory initialize, Mcp-Session-Id, tasks/list, Roots/Sampling/Logging, -32002
  2. Move connect-time state into _metaprotocol version + client info + capabilities per request (SEP-2575), add server/discover
  3. Drop sticky sessionsMcp-Session-Id gone (SEP-2567), route on Mcp-Method + Mcp-Name, cache tools/list per ttlMs
  4. Adopt Tasks for long jobsreturn a task handle, poll tasks/get
  5. Fix breaking changesremove tasks/list, move off deprecated, -32002 → -32602
  6. Validate in the 10-week windowtest client+server interop before the final ships

FAQ: migrating to the stateless MCP spec

Do I have to migrate by July 28, 2026? No. Deprecated features keep working in every spec version published within 12 months of that date — a runway to roughly mid-2027. Plan it, don’t panic.

What exactly replaces the initialize handshake? Two SEPs. SEP-2575 moves protocol version, client info, and capabilities into _meta on every request, so there’s no connect-time exchange. SEP-2567 removes the protocol-level session and the Mcp-Session-Id header. A client that needs capabilities up front calls the new server/discover method instead.

Can my remote server really run behind a plain load balancer now? Yes. No sticky sessions, no shared session store. Route round-robin on the Mcp-Method and Mcp-Name headers (SEP-2243) and cache tools/list per the server’s ttlMs (SEP-2549).

Is Tasks gone? No — it moved from experimental core to an extension. Use tasks/get, tasks/update, and tasks/cancel. tasks/list is removed, and task creation is server-directed via a task handle returned from tools/call.

What’s the one error-code change I can’t miss? “Resource not found” moves from -32002 to the standard -32602 Invalid Params (SEP-2164). Update any code matching the old number.

What’s deprecated that I’m probably using? Roots, Sampling, and Logging (SEP-2577). They still work during the runway, but plan to move off them.

Plan the migration, don’t panic

The one-breath model: MCP goes stateless (state in _meta per request, sessions removed), Tasks becomes an extension, MCP Apps add sandboxed UIs, auth aligns to OAuth/OIDC, and a handful of breaking changes need cleanup. That’s the whole revision.

The infra unlock is the real prize — stateless servers behind a round-robin load balancer, with cacheable discovery. That’s the kind of boring, horizontally-scalable plumbing you actually want under a production agent.

And you have time to do it right: a 10-week validation window plus a 12-month deprecation runway to mid-2027. The concrete first move is small and lives in Step 1 of the migration plan above — read the Release Candidate, inventory your server, and start moving connect-time state into _meta.

This is the same plumbing under the production agents we run at Nixbly — stateless, scalable, boring on purpose. If you want more on building this kind of infrastructure, the AI engineering guides go deeper.