GetMCP/Blog/MCP Guides

Stateless MCP Servers — why the 2026 protocol changes everything for production AI infrastructure

InfiWebs · · 7 min read

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/initialized handshake is removed (SEP-2575). A client’s very first request can be a tools/call.
  • The Mcp-Session-Id header 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.1
Host: your-server.example.com
Content-Type: application/json
MCP-Protocol-Version: 2026-07-28
Authorization: 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.

ConcernStateful (pre-2026)Stateless (2026-07-28)
First requestMust be initializeCan be any method
Session identityMcp-Session-Id headerNone — nothing to track
Version negotiationOnce, during handshakeMCP-Protocol-Version header, every request
Load balancingSticky sessions requiredPlain round-robin
Shared stateSession store (Redis) in hot pathNone required
Gateway routingOften needs body inspectionRoute 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.

Stateless MCP production architecture: AI client to round-robin load balancer, fanning out to three stateless instances, all reaching one upstream API or database

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:

Stateless MCP request lifecycle sequence: client to load balancer to MCP instance to upstream API and back, with nothing retained 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 OIDC application_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-Method header.
  • 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

  1. Still expecting an initialize call. That branch is dead code now — and requiring it breaks compliant 2026-07-28 clients on their first real request.
  2. Reading Mcp-Session-Id. It’s gone. Move identity logic to the token, non-authoritative context to _meta.
  3. Negotiating version once and caching it. Read MCP-Protocol-Version on every call.
  4. Leaving sticky sessions on. A leftover ip_hash or affinity cookie silently defeats the entire benefit. Audit your balancer.
  5. Skipping ttlMs. Omit it and clients hammer tools/list. Two lines, outsized payoff.
  6. 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.

InfiWebs
← All posts

Leave a reply

Try GetMCP

Ship MCP for your product, in days.

Install the plugin, import your API, share one URL. Your users will start calling your tools from Claude tonight.