Three of my MCP servers went down on July 28th. Not because of a deployment, not because of a bad commit — because the spec changed.
That's the reality of building on top of a protocol that hasn't hit 1.0 yet. When the 2026-07-28 spec landed with the stateless-first architecture, I had to migrate. What I didn't expect was that the migration itself would expose three production problems I'd been silently tolerating for months.
This is the story of what broke, what the new spec changed, and what I learned rebuilding everything from scratch.
What the 2026-07-28 Spec Actually Changed
The core shift was deceptively simple: MCP servers could no longer assume they had stateful connections to their clients. Before this spec, most MCP server implementations held onto session context between calls — the server remembered what tools had been called, what resources had been requested, what the client was working on. After July 28th, that assumption was invalid.
The protocol now explicitly requires idempotency. Every request must be self-contained. If your server needs to know "what happened in the last 5 calls," it has to carry that context itself — via the request payload, not via internal state.
This matters for two reasons:
- Horizontal scaling works now. Stateful servers can't run behind a load balancer. Every request has to route to the same instance, or state has to sync across instances. The stateless spec removes this bottleneck entirely.
- Cold starts are faster. No initialization handshake per session. Each call is independent, so a new instance can handle a request immediately.
For me, the first benefit was operational. The second benefit was a pleasant surprise.
The Three Problems the Migration Exposed
Problem 1: My "Stateless" Server Wasn't Actually Stateless
I thought I'd been building stateless servers. I'd designed them without long-running in-memory state. But the 2026-07-28 spec requires something stricter: your handler logic can't depend on any implicit state from previous requests.
Here's what I found in my code:
# OLD — relied on implicit request ordering
class MCPResourceServer:
def __init__(self):
self._cache = {} # This was fine
def handle_request(self, request):
# This looked stateless but wasn't
if request.tool == "list_resources":
# Relied on the fact that clients always called
# "initialize" before "list_resources" in the same session
if not self._initialized:
raise ProtocolError("Not initialized")
The _initialized flag was the problem. It was reset on server startup, but assumed that initialize had been called in the current session. After July 28th, the spec requires every request to be independently valid. The fix was straightforward:
# NEW — truly stateless, validates per-request
class MCPResourceServer:
def handle_request(self, request):
if request.tool == "list_resources":
# Validate prerequisites exist in THIS request
if not request.context.get("session_token"):
raise ProtocolError("Missing session_token in request context")
# No implicit session state
return self._list_resources(request)
The key insight: stateless doesn't mean "no state." It means "state is explicit and self-contained in each request."
Problem 2: I Was Paying for Idle Connections
Before the migration, my servers maintained persistent WebSocket connections per client session. This meant:
- Open connections sitting idle between user interactions (sometimes 10-30 minutes)
- Server instances that couldn't scale to zero because connections were always "active"
- Higher memory usage than necessary
After migrating to the stateless spec, I could finally implement true HTTP(S) transport with no persistent connections:
# NEW — stateless HTTP transport, no persistent connections
from mcp.transport.http import HTTPServer
from mcp.spec_2026_07_28 import StatelessHandler
app = HTTPServer(handler=StatelessHandler(my_mcp_server))
app.run(host="0.0.0.0", port=8080)
# The server now handles individual requests, not sessions.
# Each invocation is completely independent.
The cloud provider bill for my MCP infrastructure dropped by about 34% in the first week after migration. I hadn't realized how much I was paying for connections that were doing nothing.
Problem 3: My Error Recovery Was a Mess
With stateful servers, error recovery meant: reconnect, reinitialize, restore context, retry. If anything went wrong mid-sequence, you had to restart from the beginning of the session.
The stateless spec made error recovery trivial by accident. Each request is independent, so retry logic became:
import httpx
from tenacity import retry, stop_after_attempt, wait_exponential
@retry(stop=stop_after_attempt(3), wait=wait_exponential(multiplier=1, min=1, max=10))
def call_mcp_tool(tool_name: str, params: dict, context: dict) -> dict:
response = httpx.post(
f"https://my-mcp-server.example.com/tools/{tool_name}",
json={"params": params, "context": context},
timeout=30.0
)
response.raise_for_status()
return response.json()
If a request fails, you retry with the same context. No session recovery. No re-initialization. No special error handling. The idempotency requirement of the stateless spec means retries are safe by default.
The Migration Process
If you're running MCP servers and need to migrate, here's what the actual process looked like for me:
Step 1: Audit current request dependencies
Find every place your server code reads from self. or accesses shared state that isn't explicitly passed in the request. I wrote a quick grep:
grep -n "self\._" mcp_server.py | grep -v "self._cache\|self._config"
Anything that looked like session state got flagged.
Step 2: Make context explicit
Every piece of implicit session state — authentication tokens, initialization flags, sequence counters — needed to move into the request context or a separate state store (I used Redis for cross-instance consistency where needed).
Step 3: Update transport
Replace persistent WebSocket connections with HTTP/SSE. The MCP spec now supports HTTP transport natively for stateless servers:
from mcp.transport.http import HTTPServer, SSEClient
# Server side
server = HTTPServer(handler=StatelessHandler(server_instance))
await server.start()
# Client side — each call is independent
async with httpx.AsyncClient() as client:
result = await client.post(url, json=request_payload)
Step 4: Test for idempotency
Once migrated, the test is simple: call your endpoint twice in a row with the same payload. If the second call fails or returns a different result, you have residual state dependency.
What I Learned
The July 2026 spec change forced me to fix three things I didn't know were broken:
- Implicit state is a liability. Any assumption that "this will be called in the right order" will eventually break in production. Make dependencies explicit.
- Idempotency isn't just for APIs — it's for protocols too. The stateless spec works because every request is valid on its own. This is a design constraint worth embracing proactively, not just complying with.
- The operational benefits were bigger than I expected. Eliminating persistent connections didn't just save money — it made my servers easier to reason about, debug, and scale.
The migration took about a day of focused work across three servers. The July 28th spec change was a forcing function I didn't want, but I'm glad I had it. My infrastructure is leaner, more predictable, and cheaper to run.
If you're running MCP servers and haven't migrated yet: the window is closing. The spec is moving fast, and the ecosystem is consolidating around stateless-first implementations. The migration is worth doing on your terms before you're forced to do it on someone else's.
Top comments (0)