{"slug": "mcp-went-stateless-what-the-2026-07-28-spec-actually-changes", "title": "MCP Went Stateless: What the 2026-07-28 Spec Actually Changes", "summary": "The Model Context Protocol (MCP) became stateless at the protocol layer with the 2026-07-28 specification, removing the handshake and session ID header to enable horizontal scaling without sticky sessions. The revision introduces six Specification Enhancement Proposals, including cacheable list results, routing headers, and hardened authentication, while deprecating older transports and features.", "body_md": "For eighteen months, running a remote MCP server meant fighting your own infrastructure. You had a bidirectional, stateful protocol sitting on top of HTTP, which meant sticky sessions, a shared Redis for session state, or a gateway doing packet inspection to route requests to the one box that held the connection. Every horizontal scaling story started with an apology.\n\nOn July 28, 2026, that ended. The `2026-07-28`\n\nspecification makes MCP **stateless at the protocol layer** — the largest revision since launch, and the first one that makes MCP behave like the rest of the web.\n\nSix SEPs (Specification Enhancement Proposals) work together here. The short version:\n\n**The handshake is gone.** `initialize`\n\n/`initialized`\n\nand the `Mcp-Session-Id`\n\nheader have been removed (SEP-2575, SEP-2567). Every request is now self-describing: protocol version, client identity, and client capabilities ride inline in `_meta`\n\non each call. There's an optional `server/discover`\n\nRPC if a client wants capabilities up front, but nothing requires it.\n\n```\nPOST /mcp HTTP/1.1\nMCP-Protocol-Version: 2026-07-28\nMcp-Method: tools/call\nMcp-Name: search\n\n{\"jsonrpc\":\"2.0\",\"id\":1,\"method\":\"tools/call\",\n \"params\":{\"name\":\"search\",\"arguments\":{\"q\":\"otters\"},\n \"_meta\":{\"io.modelcontextprotocol/clientInfo\":{\"name\":\"my-app\",\"version\":\"1.0\"}}}}\n```\n\nAny request can land on any instance behind a plain round-robin load balancer. No shared storage, no ARR affinity, no sticky routing.\n\n**Routing moved into headers.** `Mcp-Method`\n\nand `Mcp-Name`\n\nare now required on Streamable HTTP requests (SEP-2243). Your gateway, WAF, or rate limiter can route and meter without parsing JSON bodies. If you've ever written a Lua script to peek inside an MCP payload at the edge, you can delete it.\n\n**List results are cacheable.** `tools/list`\n\n, `prompts/list`\n\n, `resources/list`\n\n, and `resources/read`\n\nnow carry `ttlMs`\n\nand `cacheScope`\n\n(SEP-2549), with deterministic ordering. That last part matters more than it looks: a stable tool catalog keeps upstream *prompt* caches stable across reconnects, which is a real token-cost line item.\n\n**Server→client calls became round trips.** This is the clever bit. Elicitation and sampling used to require a held-open stream, which is exactly what a stateless protocol can't offer. Multi Round-Trip Requests (SEP-2322) invert it: the server returns `resultType: \"input_required\"`\n\nplus an opaque request-state token, the client gathers the answers, then calls the same tool again with `inputResponses`\n\nattached. Every leg is an ordinary client-to-server request.\n\n**Auth got hardened.** Authorization servers should return `iss`\n\nper [RFC 9207](https://www.rfc-editor.org/rfc/rfc9207) and clients must validate it before redeeming a code (SEP-2468) — that closes an AS mix-up hole. Client credentials are now bound to the issuer that minted them (SEP-2352), and Dynamic Client Registration is formally deprecated in favour of Client ID Metadata Documents.\n\n**Deprecations.** Roots, Sampling, and Logging are deprecated (SEP-2577), as is the legacy HTTP+SSE transport (SEP-2596). Both get a twelve-month minimum offramp under the new deprecation policy — which is itself the quiet good news here. This is a protocol that now plans its breakage.\n\n\"Stateless protocol\" does not mean \"stateless application.\"\n\nIf your server needs to carry state across calls, you mint an **explicit handle** from a tool and let the model pass it back as an ordinary argument. The maintainers are direct that this works better than state hidden in the transport, and I think they're right for a reason that isn't primarily architectural: the model can *see* the handle. It becomes a thing the agent reasons about and threads between tools, instead of an invisible coupling that breaks the moment a load balancer does its job.\n\nThis is the same lesson REST landed on twenty-five years ago. Roy Fielding's dissertation argued the statelessness constraint buys you visibility, reliability, and scalability at the cost of repeated per-request data — [ Architectural Styles and the Design of Network-based Software Architectures](https://ics.uci.edu/~fielding/pubs/dissertation/top.htm), Ch. 5. MCP just paid that tuition in public.\n\nThe v2 line of the Python SDK renamed the in-SDK `FastMCP`\n\nto `MCPServer`\n\nand moved transport options off the constructor onto `run()`\n\n. If you're still importing `mcp.server.fastmcp`\n\n, you're on v1.x and speaking the old handshake.\n\n```\nuv init mcp-units && cd mcp-units\nuv add \"mcp[cli]\"\npython\n# server.py\nfrom mcp.server.mcpserver import MCPServer\n\nmcp = MCPServer(\"units\")\n\n@mcp.tool()\ndef to_celsius(fahrenheit: float) -> float:\n    \"\"\"Convert Fahrenheit to Celsius.\"\"\"\n    return round((fahrenheit - 32) * 5 / 9, 2)\n\nif __name__ == \"__main__\":\n    # stateless_http + json_response = plain HTTP request/response\n    mcp.run(transport=\"streamable-http\", stateless_http=True, json_response=True)\n```\n\nThat's it. No session manager, no event store, no affinity. Run it and point the inspector at `http://localhost:8000/mcp`\n\n:\n\n```\nuv run server.py\nnpx -y @modelcontextprotocol/inspector\n```\n\nNow the interesting version — state without a session:\n\n``` python\nimport uuid\n\nfrom mcp.server.mcpserver import MCPServer\n\nmcp = MCPServer(\"reports\")\n\n# In production this is Redis/Postgres, not a dict — but note that it's\n# *your* store, keyed by a handle the model holds, not transport state.\nJOBS: dict[str, dict] = {}\n\n@mcp.tool()\ndef start_export(dataset: str) -> str:\n    \"\"\"Begin an export. Returns a job handle to pass to check_export.\"\"\"\n    job_id = f\"job_{uuid.uuid4().hex[:8]}\"\n    JOBS[job_id] = {\"dataset\": dataset, \"status\": \"running\"}\n    return job_id\n\n@mcp.tool()\ndef check_export(job_id: str) -> dict[str, str]:\n    \"\"\"Check an export by its handle.\"\"\"\n    return JOBS.get(job_id, {\"status\": \"unknown\"})\n```\n\nTwo calls, potentially two different machines, zero coordination. The handle is in the model's context, not in the transport.\n\n**One gotcha worth internalising before you migrate:** because MRTR removed the back-channel, `ctx.elicit()`\n\nand `ctx.session.create_message()`\n\nraise `NoBackChannelError`\n\non a modern connection. If your server asks the user mid-call, that code needs rewriting around the input-required round trip — it's the single most likely thing to break.\n\nNote\n\n**This is v2 of the MCP Python SDK, the current stable release line.** It is a major rework of the SDK, both to support the [2026-07-28 MCP specification](https://modelcontextprotocol.io/specification/2026-07-28) (and every earlier revision) and to fix long-standing architectural issues. Coming from v1? See [What's new in v2](https://py.sdk.modelcontextprotocol.io/whats-new/) for the tour of what changed and the [migration guide](https://py.sdk.modelcontextprotocol.io/migration/) for every breaking change.\n\n**Not ready to migrate?** v1.x lives on the [ v1.x branch](https://github.com/modelcontextprotocol/python-sdk/tree/v1.x), continues to receive critical bug fixes and security patches, and is documented at\n\n`pip install mcp`\n\nnow installs 2.x, keep a `<2`\n\nupper bound on your requirement (for example `mcp>=1.28,<2`\n\n) until you've migrated.Something rough, confusing, or broken? [Open an issue](https://github.com/modelcontextprotocol/python-sdk/issues/new?template=v2-feedback.yaml) or find us in [#python-sdk-dev on the MCP Contributors Discord](https://discord.gg/6CSzBmMkjX).\n\n**The documentation lives at https://py.sdk.modelcontextprotocol.io/.**\n\nIt has a [Get](https://py.sdk.modelcontextprotocol.io/get-started/)…\n\nThe scaling story is the headline, but I think the deeper shift is that MCP stopped being a *transport-flavoured* protocol and became an *HTTP-flavoured* one. That means the boring, battle-tested layer of the web now applies to agent tooling: CDNs, edge workers, standard load balancers, cache-control semantics, header-based authorization at the gateway.\n\nIt also narrows some real attack surface. The research on MCP security has been fairly damning, and a lot of it clusters around trust boundaries between independently operated components:\n\nRead those with a date-stamp in mind: they all predate the stateless core, so the session-layer threats they describe are partly answered by a protocol that no longer has a session layer. The prompt-level and supply-chain threats are entirely untouched. Stateless MCP is a scalability fix, not a security fix — treat any vendor claiming otherwise with suspicion.\n\nMigration checklist`2026-07-28`\n\n(v2 for Python; pin exactly)`mcp.server.fastmcp.FastMCP`\n\n→ `mcp.server.mcpserver.MCPServer`\n\n`stateless_http`\n\n/ `json_response`\n\nfrom the constructor to `run()`\n\n/ `streamable_http_app()`\n\n`ctx.elicit()`\n\nor `create_message()`\n\naround MRTR`ttlMs`\n\n/ `cacheScope`\n\nto list responses`iss`\n\n(RFC 9207) if you're a client; plan the DCR → CIMD move if you're a server`structured_content`\n\n, `next_cursor`\n\n, `input_schema`\n\n)If you've already migrated a production server, I'd like to hear what broke. My guess is it was the elicitation rewrite and not the sessions.\n\n*Building AI integrations at Codiva. Comments and corrections welcome.*", "url": "https://wpnews.pro/news/mcp-went-stateless-what-the-2026-07-28-spec-actually-changes", "canonical_source": "https://dev.to/krlz/mcp-went-stateless-what-the-2026-07-28-spec-actually-changes-273k", "published_at": "2026-08-09 14:01:44+00:00", "updated_at": "2026-08-09 14:22:29.440677+00:00", "lang": "en", "topics": ["artificial-intelligence", "ai-infrastructure", "developer-tools", "ai-agents"], "entities": ["Model Context Protocol", "SEP-2575", "SEP-2567", "SEP-2243", "SEP-2549", "SEP-2322", "SEP-2468", "SEP-2352"], "alternates": {"html": "https://wpnews.pro/news/mcp-went-stateless-what-the-2026-07-28-spec-actually-changes", "markdown": "https://wpnews.pro/news/mcp-went-stateless-what-the-2026-07-28-spec-actually-changes.md", "text": "https://wpnews.pro/news/mcp-went-stateless-what-the-2026-07-28-spec-actually-changes.txt", "jsonld": "https://wpnews.pro/news/mcp-went-stateless-what-the-2026-07-28-spec-actually-changes.jsonld"}}