Blue-green deploying a stateless MCP server means running two identical fleets — blue (currently live) and green (the new version) — and swinging all traffic from one to the other at the load balancer in a single reload. It is now trivial because the 2026-07-28 spec made MCP stateless: the Mcp-Session-Id header and the initialize/initialized handshake are gone, so any replica can serve any request. With nothing pinned to a specific instance, there is no session store to keep in sync and nothing to strand when you retire the old fleet.
Why stateless is the whole precondition#
Before this revision, an MCP client did a handshake, got back an Mcp-Session-Id, and every subsequent request had to return to the same instance — the session lived in that process's memory. That forced sticky routing at the load balancer, and draining a replica meant waiting for every open session on it to end, which could be a long time.
The 2026-07-28 release candidate removed both pieces (SEP-2567 dropped the header, SEP-2575 dropped the handshake). Protocol version, client info, and client capabilities now travel in _meta on every request, and a new server/discover method fetches capabilities on demand. The practical consequence, in the spec's own words: "any MCP request can land on any server instance." That is the precondition for everything below.
The non-obvious payoff: with stateless MCP, "drain" is just "stop sending new requests." There is no session state living on the old fleet, so once in-flight calls finish, the old replicas hold nothing you need. Compare that to the old world, where draining meant babysitting sessions until they expired.
Step 1: Run two identical environments#
Blue and green are byte-for-byte identical deployments behind the same load balancer. Only the version differs. Give each its own upstream group.
# /etc/nginx/conf.d/mcp.conf
upstream mcp_blue { server 10.0.1.11:8080; server 10.0.1.12:8080; }
upstream mcp_green { server 10.0.2.11:8080; server 10.0.2.12:8080; }
# The active pool is a variable we flip in one place.
upstream mcp_active { server 10.0.1.11:8080; server 10.0.1.12:8080; } # -> blue
server {
listen 443 ssl;
server_name mcp.example.com;
location /mcp {
proxy_pass http://mcp_active;
proxy_http_version 1.1;
proxy_read_timeout 120s; # cover your slowest tool call
}
}
Note what is absent: no ip_hash, no sticky cookie, no session affinity. A stateless server wants a plain round-robin balancer — the exact setup we load-test in a separate walkthrough. (If you are standing up the fleet itself for the first time, start with how to deploy an MCP server.)
Step 2: Expose a readiness endpoint#
The cutover must gate on green actually being ready, not just running. Serve a cheap HTTP /healthz that returns 200 only when the process can handle a real JSON-RPC call. In Kubernetes, wire it as a readiness probe so a replica receives no traffic until it passes.
readinessProbe:
httpGet:
path: /healthz
port: 8080
initialDelaySeconds: 3
periodSeconds: 5
failureThreshold: 2
Step 3: Deploy green and smoke-test it directly#
Ship the new version to the green fleet, then hit it before it sees production traffic. Because MCP over Streamable HTTP is a single POST endpoint, a smoke test is one curl at a real method — no handshake needed anymore.
# Readiness gate
curl -fsS http://10.0.2.11:8080/healthz || { echo "green not ready"; exit 1; }
# JSON-RPC smoke test: call a real method
curl -fsS http://10.0.2.11:8080/mcp \
-H 'Content-Type: application/json' \
-H 'Accept: application/json' \
-d '{"jsonrpc":"2.0","id":1,"method":"tools/list",
"_meta":{"protocolVersion":"2026-07-28"}}' \
| grep -q '"result"' && echo "green OK"
If either check fails, you stop here — production never moved.
Step 4: Cut traffic at the load balancer#
You can flip all at once or ramp with weights. A weighted ramp lets you watch error rates on a slice of real traffic first.
# Weighted cutover: 90% blue, 10% green
upstream mcp_active {
server 10.0.1.11:8080 weight=9;
server 10.0.2.11:8080 weight=1;
}
Reload with zero dropped connections, then widen the weight to green as metrics stay clean:
nginx -t && nginx -s reload # graceful: in-flight requests finish on old workers
When you are confident, point mcp_active entirely at green and reload once more.
Step 5: Drain blue — which is nearly nothing#
Once the balancer stops routing to blue, drain is just waiting out in-flight requests. Size the wait to your longest tool call, then decommission.
# Give in-flight requests up to 120s to complete, then scale blue to zero
sleep 120
kubectl scale deploy/mcp-blue --replicas=0
That is the whole point of statelessness: nothing on blue is holding a session, so after the wait, blue has nothing left to lose.
Step 6: Roll back in seconds#
Blue is still deployed (until Step 5 finishes) and unchanged. Rollback is the same cutover in reverse — repoint mcp_active at blue and reload. Seconds, not a redeploy. This is why blue-green beats rolling when fast, safe rollback is the priority.
Do this now#
Add a /healthz endpoint to your MCP server today, strip any ip_hash or sticky-cookie directive from your balancer, and script Steps 3–4 as a single deploy command. Then run one weighted cutover in staging and time your rollback — if it is not under ten seconds, your blue fleet is being torn down too early. If your server still leans on server-side state, fix that first with explicit state handles before you rely on any of this.



