For most of its short life, the Model Context Protocol worked like a phone call. Before an AI client could do anything useful, it dialed in, waited for the server to pick up, exchanged pleasantries, and held the line open — the initialize/initialized handshake, plus an Mcp-Session-Id that identified the open line.
The 2026-07-28 revision hangs up the phone. No handshake, no session ID, no line to hold open. Every request now stands on its own, carries everything it needs, and can be answered by any server instance that happens to be free. If you run MCP servers in production, this is the most consequential change since the protocol launched — it reshapes how you deploy, scale, cache, and secure them.
New to the protocol? Start with What Is Model Context Protocol (MCP)? — the rest of this assumes you know what a tool call is.
The one-line version
The old transport was stateful: clients opened a session, the server tracked it, everything after depended on it existing. The new transport is stateless: each HTTP request is self-describing, independent, and disposable. That single shift unlocks plain load balancers, horizontal autoscaling, serverless, and aggressive caching — the whole production playbook that stateful sessions quietly blocked (sticky sessions, a Redis session store in the hot path, deep packet inspection at the gateway).
What the 2026-07-28 revision changes
Two proposals do the demolition:
- The
initialize/initializedhandshake is removed (SEP-2575). A client’s very first request can be atools/call. - The
Mcp-Session-Idheader and protocol-level session are removed (SEP-2567). There is no server-side session to create, store, or expire.
Two mechanisms replace the session: the MCP-Protocol-Version header travels on every request, and protocol version + client info ride in _meta on every request. Here’s the shape of a request under the new model:
POST /mcp HTTP/1.1Host: your-server.example.comContent-Type: application/jsonMCP-Protocol-Version: 2026-07-28Authorization: Bearer <token>{ "jsonrpc": "2.0", "id": 1, "method": "tools/call", "params": { "name": "search_orders", "arguments": { "email": "[email protected]" }, "_meta": { "client": { "name": "claude", "version": "1.4.0" } } }}
No prior handshake produced this. No session ID ties it to anything. Any instance behind your load balancer can read it top to bottom and answer it completely.
| Concern | Stateful (pre-2026) | Stateless (2026-07-28) |
|---|---|---|
| First request | Must be initialize | Can be any method |
| Session identity | Mcp-Session-Id header | None — nothing to track |
| Version negotiation | Once, during handshake | MCP-Protocol-Version header, every request |
| Load balancing | Sticky sessions required | Plain round-robin |
| Shared state | Session store (Redis) in hot path | None required |
| Gateway routing | Often needs body inspection | Route on the Mcp-Method header |
That last row is the delightful detail: because the method surfaces in an Mcp-Method header, your gateway can route expensive tools/call traffic to a beefier pool than cheap tools/list traffic without parsing JSON at all.
Architecture in production
Strip away the sessions and the deployment diagram collapses into something almost boringly simple — which is exactly the point.

Every instance is identical and interchangeable. There is no “the instance that owns this client.” Add a fourth during a spike and remove it afterward — no client notices. An instance can crash mid-flight and the client simply retries onto a healthy one that knows exactly as much as the dead one did: everything it needs from the request itself. Stateful servers you tend (session stores to monitor, affinity to debug, graceful-drain on deploy). Stateless servers you just run more of.
The request lifecycle
A single call from client to upstream and back, with nothing held between requests:

The server’s job on each request is uniform: read the protocol version, authenticate the caller, map the tool to an upstream operation, return the result. None of it depends on prior requests, so the whole thing parallelizes trivially.
Building one: a minimal stateless endpoint
Here’s the entire pattern in Node.js — JSON-RPC over HTTP POST, a per-request version check, per-request auth, dispatch on method, and a tools/list that carries cache metadata. There’s no session anywhere; that’s the whole demonstration.
import express from "express";const app = express();app.use(express.json());const SUPPORTED_VERSIONS = ["2026-07-28", "2025-11-25"];const TOOLS = [{ name: "search_orders", description: "Find orders by customer email.", inputSchema: { type: "object", properties: { email: { type: "string", format: "email" } }, required: ["email"], },}];app.post("/mcp", async (req, res) => { // Per-request version check — no handshake, no session. const version = req.get("MCP-Protocol-Version"); if (!version || !SUPPORTED_VERSIONS.includes(version)) { return res.status(400).json(rpcError(req.body?.id, -32000, "Unsupported MCP-Protocol-Version")); } // Per-request auth — the token is the only identity. const token = (req.get("Authorization") || "").replace(/^Bearer\s+/i, ""); if (!token) { return res.status(401).json(rpcError(req.body?.id, -32001, "Missing bearer token")); } const { id, method, params } = req.body; switch (method) { case "tools/list": return res.json({ jsonrpc: "2.0", id, result: { tools: TOOLS, ttlMs: 300000, cacheScope: "server" }, }); case "tools/call": { const result = await callTool(params.name, params.arguments, token); return res.json({ jsonrpc: "2.0", id, result }); } default: return res.status(404).json(rpcError(id, -32601, `Unknown method: ${method}`)); }});function rpcError(id, code, message) { return { jsonrpc: "2.0", id: id ?? null, error: { code, message } };}async function callTool(name, args, token) { if (name !== "search_orders") throw new Error(`Unknown tool: ${name}`); const upstream = await fetch( `https://api.example.com/orders?email=${encodeURIComponent(args.email)}`, { headers: { Authorization: `Bearer ${token}` } } ); return { content: [{ type: "text", text: JSON.stringify(await upstream.json()) }] };}app.listen(3000, () => console.log("Stateless MCP server on :3000"));
There’s no initialize case in the switch, and nowhere to store a session. Every instance running this code is identical and disposable — run one or run fifty. The same shape maps cleanly onto Python (FastAPI) or PHP; the only rule is no server-side session.
One detail that makes statelessness practical: the ttlMs and cacheScope on tools/list. With no session, a naive client would re-request the tool catalog constantly. Modeled on HTTP Cache-Control, ttlMs tells the client how long to cache the listing and cacheScope how widely to share it — so your fleet serves real tools/call work instead of answering tools/list over and over. Set it generously for stable catalogs.
If hand-writing and hosting this transport for every API you own sounds like a lot of undifferentiated work — it is. That’s the gap getMCP fills: point it at your REST API and it generates the tools and speaks the 2026-07-28 stateless transport for you, while still negotiating down to older session-based versions for legacy clients.
Deploying and securing it
Because there’s nothing to sync between instances, deployment reduces to “run N copies behind a round-robin balancer” — no session affinity, no sticky cookies. Serverless (Lambda, Cloud Run, Cloudflare Workers) fits perfectly: each invocation is independent by definition. Or, if your API already lives on WordPress, getMCP runs the whole thing as a plugin on infrastructure you already own — import from a cURL command, an OpenAPI spec, or Postman, and get a compliant MCP URL. See How to Add MCP to Your SaaS in 2026.
Removing sessions removes a whole class of problems but moves where you place your defenses:
- Authenticate per request, always. No session to trust after the fact — every request must prove its own token, validated fresh each time. No long-lived session cookie to steal or fixate.
- Honor the OAuth hardening. Validate the issuer (
iss) per RFC 9207, respect the OIDCapplication_type, handle refresh tokens as documented. See MCP Authentication Explained. - Don’t trust
_meta. Client info is client-supplied — fine for logging and routing, never for authorization. That decision belongs to the token. - Rate-limit at the edge on the credential/source, tightening limits on expensive methods via the
Mcp-Methodheader. - TLS everywhere, secrets in a vault. Credentials travel on every request; inject upstream secrets at runtime, never hardcode them.
The net posture is arguably better: fewer moving parts, no session-hijacking surface, and a token you validate every single time.
Common mistakes when going stateless
- Still expecting an
initializecall. That branch is dead code now — and requiring it breaks compliant 2026-07-28 clients on their first real request. - Reading
Mcp-Session-Id. It’s gone. Move identity logic to the token, non-authoritative context to_meta. - Negotiating version once and caching it. Read
MCP-Protocol-Versionon every call. - Leaving sticky sessions on. A leftover
ip_hashor affinity cookie silently defeats the entire benefit. Audit your balancer. - Skipping
ttlMs. Omit it and clients hammertools/list. Two lines, outsized payoff. - Forgetting older clients exist. Not everything speaks 2026-07-28 yet — negotiate down to recent prior versions rather than hard-fail.
The takeaway
The 2026-07-28 revision took MCP from a protocol that worked to one built for production. By hanging up the phone, it turned every MCP server into something you scale like a stateless web service: plain load balancers, effortless horizontal scaling, serverless-friendly, aggressively cacheable, smaller security surface.
Building new? Build stateless from day one. Migrating? The transport changes are mechanical and your tool logic mostly survives. And if you’d rather not hand-write and host the transport at all, that’s exactly what getMCP exists to solve.
Building on WordPress? getMCP turns any REST API into a compliant MCP server — no code required. Get started free.