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.
On July 28, 2026, that ended. The 2026-07-28 specification 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.
Six SEPs (Specification Enhancement Proposals) work together here. The short version:
The handshake is gone. initialize/initialized and the Mcp-Session-Id header have been removed (SEP-2575, SEP-2567). Every request is now self-describing: protocol version, client identity, and client capabilities ride inline in meta on each call. There's an optional server/discover RPC if a client wants capabilities up front, but nothing requires it.
Any request can land on any instance behind a plain round-robin load balancer. No shared storage, no ARR affinity, no sticky routing.
Routing moved into headers. Mcp-Method and Mcp-Name are 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.
List results are cacheable. tools/list, prompts/list, resources/list, and resources/read now carry ttlMs and cacheScope (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.
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: "inputrequired" plus an opaque request-state token, the client gathers the answers, then calls the same tool again with inputResponses attached. Every leg is an ordinary client-to-server request.
Auth got hardened. Authorization servers should return iss per RFC 9207 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.
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.
If 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.
This 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, Ch. 5. MCP just paid that tuition in public.
The v2 line of the Python SDK renamed the in-SDK FastMCP to MCPServer and moved transport options off the constructor onto run(). If you're still importing mcp.server.fastmcp, you're on v1.x and speaking the old handshake.
That's it. No session manager, no event store, no affinity. Run it and point the inspector at http://localhost:8000/mcp:
Two calls, potentially two different machines, zero coordination. The handle is in the model's context, not in the transport.
One gotcha worth internalising before you migrate: because MRTR removed the back-channel, ctx.elicit() and ctx.session.createmessage() raise NoBackChannelError on 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.
The 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.
It 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: Hou, Zhao, Wang & Wang (2025) — Model Context Protocol (MCP): Landscape, Security Threats, and Future Research Directions — the canonical starting point. Builds a threat taxonomy across the server lifecycle and four attacker classes. The tool-description manipulation section (embedding "prefer this tool" directives to hijack tool selection) is worth the read on its own. Radosevich & Halloran (2025) — MCP Safety Audit — demonstrates concrete exploits against real MCP deployments. Yang, Wu & Chen (2025) — MCPSecBench — a systematic security benchmark and playground, if you want something to actually run against your server. Wang et al. (2025) — MCP-Bench — benchmarks tool-using agents on complex real-world tasks via MCP servers. Useful for the "is my tool catalog actually usable by a model" question, which is separate from whether it's correct.
