When the Model Context Protocol (MCP) first came out in late 2024, it was designed for a very specific environment: a desktop app like Claude Desktop or an IDE spawning a local tool server on the same machine over STDIO. When both the client and the server live on your laptop as local processes, keeping a stateful session open from start to finish makes complete sense.
The friction started as soon as developers tried moving MCP servers onto the web.
Because the early network transports carried over that local stateful session model, running a remote MCP server over HTTP meant juggling two separate endpoints (/sse and /message), forcing every client through an initialize and initializedhandshake before doing any real work, tracking Mcp-Session-Idheaders across requests, and holding long-lived streams open. If you wanted to deploy an MCP server on Cloudflare Workers, a plain stateless Worker wasn't enough—you had to wire up Durable Objects (McpAgent) just to keep the protocol session state alive in memory between calls.
With the 2026-07-28 MCP specification and the MCP v2 rollout on Cloudflare, the protocol finally dropped mandatory sessions and became stateless by default.
The 2026-07-28 release is a clean-break update toStreamable HTTP so MCP behaves like normal web traffic rather than a persistent socket tunnel.
1. No More Handshakes or Mcp-Session-Id
In MCP v2, the protocol-level initialize handshake, transport sessions, Mcp-Session-Id tracking, and explicitDELETE session teardown are gone from the default flow.
Every HTTP request is now self-contained. Instead of negotiating capabilities upfront in a separate stateful handshake, the client passes its protocol version, identity, and capabilities directly inside the request's_meta field (and if a client needs to inspect what a server supports ahead of time, it can make a single statelessserver/discover RPC call). Any Worker isolate or container behind a round-robin load balancer can handle any request cold.
2. HTTP Headers That API Gateways Can Actually Read
In v1, every MCP call was a generic JSON-RPC POST body. If you wanted to put rate limiting, caching, or WAF rules in front of your MCP server, your gateway had to parse the JSON payload just to figure out whether an agent was listing tools or calling a destructive database mutation.
The 2026-07-28 spec moves that routing metadata into standard HTTP headers:
MCP-Protocol-Version: 2026-07-28Mcp-Method: tools/call(ortools/list,resources/read,prompts/get)Mcp-Name: <tool_or_resource_name>
Because the method and tool name sit right in the request headers, edge gateways (like Cloudflare Gateway) can rate-limit expensive tools, block unauthorized tool names, or cache tools/list responses at the CDN layer without touching the JSON body.
3. Multi Round-Trip Requests (MRTR) Without Open Streams
One reason MCP v1 held connections open was to let a server ask the client for extra input or human confirmation mid-execution (elicitation and sampling).
MCP v2 replaces persistent back-channels with Multi Round-Trip Requests (SEP-2322). When a tool needs human approval or additional parameters, the server returns an embedded input request inside the standard HTTP response and exits cleanly. Once the user approves or supplies the value, the client sends a fresh stateless HTTP request to finish the run.
4. Simpler OAuth via Client Metadata Documents (CIMD)
Instead of forcing servers to implement Dynamic Client Registration (DCR)—which opened up spam and state-storage headaches on serverless setups—the 2026-07-28 spec switches to Client Metadata Documents (CIMD) paired with RFC 9207 issuer validation, and moves long-running async jobs into an optional Tasks extension.
| Feature | MCP v1 (2024-11-05 / 2025-03-26) | MCP v2 (2026-07-28) |
|---|---|---|
| Session Model | Stateful (initialize handshake + Mcp-Session-Id) | Stateless by default (self-contained _meta per request) |
| Transport Endpoints | Dual /sse + /message (or stateful Streamable HTTP) | Single stateless /mcp endpoint |
| Gateway Visibility | Opaque JSON-RPC body | Explicit Mcp-Method and Mcp-Name HTTP headers |
| Human-in-the-Loop | Requires holding an open server-to-client stream | Stateless Multi Round-Trip Requests (MRTR) |
| Cloudflare Deployment | Required Durable Objects (McpAgent) for session state | Runs in a plain stateless Cloudflare Worker (createMcpHandler) |
To match the 2026-07-28 spec, the official Tier 1 SDKs all moved to major version 2.x with stateless HTTP enabled out of the box:
- TypeScript (
@modelcontextprotocol/serverv2):The old monolithic@modelcontextprotocol/sdkpackage was split into dedicated@modelcontextprotocol/serverand@modelcontextprotocol/clientpackages. It ships withcreateMcpHandler, which speaks Web StandardRequestandResponseobjects natively—meaning it drops straight into Cloudflare Workers, Hono, Bun, or Deno without adapters. - Python (
mcpv2): Installed viauv add "mcp[cli]", it uses Python 3.11+ type annotations and docstrings on@mcp.tool()functions to build JSON schemas automatically and runs statelessly behind FastAPI/ASGI or AWS Lambda adapters. - Go (
github.com/modelcontextprotocol/go-sdk/mcp):Co-maintained with Google, mapping tool inputs directly to Go structs for compiled, low-memory binaries. - C# / .NET (
ModelContextProtocol.AspNetCorev2):Co-maintained with Microsoft, whereHttpServerTransportOptions.Statelessnow defaults totrueand[McpHeader]attributes bind directly toMcp-Param-*headers.
The best practical result of MCP v2 is how much boilerplate disappears when deploying to the edge. Unless your tool itself needs stateful coordination, you can drop Durable Objects completely and run your MCP server inside a standard Cloudflare Worker that scales to zero.
First, install the v2 server package and Zod:
npm install @modelcontextprotocol/server zodIn src/index.ts, use createMcpHandler with a factory function that creates a fresh McpServer instance per request (passing a factory function instead of a global singleton prevents concurrent requests inside the same Worker isolate from leaking state across clients):
import { McpServer, createMcpHandler } from "@modelcontextprotocol/server";
import * as z from "zod";
// Factory function: creates a clean server instance per incoming request
const createServer = () => {
const server = new McpServer({
name: "edge-utility-mcp",
version: "2.0.0",
});
server.tool(
"check_dns_latency",
"Measure HTTP response time to a target URL from the closest Cloudflare edge POP",
{
url: z.string().url().describe("Target HTTPS URL to probe"),
},
async ({ url }) => {
const start = performance.now();
const res = await fetch(url, { method: "HEAD" });
const elapsedMs = Math.round(performance.now() - start);
return {
content: [
{
type: "text",
text: JSON.stringify({
url,
status: res.status,
edgeLatencyMs: elapsedMs,
}),
},
],
};
}
);
return server;
};
const mcpHandler = createMcpHandler(createServer);
export default {
async fetch(request: Request, env: Env, ctx: ExecutionContext): Promise<Response> {
const url = new URL(request.url);
if (url.pathname === "/mcp") {
return mcpHandler(request, env, ctx);
}
return new Response("MCP v2 Server Running. Connect to /mcp", { status: 200 });
},
};Your wrangler.jsonc doesn't need any durable_objectsbindings or SQLite migrations anymore—just a standard Worker config:
{
"name": "edge-utility-mcp",
"main": "src/index.ts",
"compatibility_date": "2026-10-01",
"compatibility_flags": ["nodejs_compat"]
}Deploy it with a single command:
npx wrangler deployWhen do you still need the Cloudflare Agents SDK (agents package) and Durable Objects? Only when the workload itself is stateful—for example, if your MCP server needs per-user embedded SQLite (this.sql), scheduled background cron tasks (this.schedule()), or durable multi-step workflows (AgentWorkflow). For the other 90% of MCP servers that just wrap APIs, query D1/KV, or run stateless transformations, a plain Worker with createMcpHandler is all you need.