Skip to main content

Cloudflare Deprecated McpAgent: What a Stateless MCP Handler Buys (and Costs) a Deploy-From-Chat Server

12 min readDora NodaDora Noda
Share
On this page

The reference implementation for remote MCP servers just deleted its default state layer. In Agents SDK v0.20.0, Cloudflare deprecated McpAgent — the Durable-Object-backed class every Workers-hosted MCP server extended for per-session state — and froze its features. New servers are supposed to use createMcpHandler, a stateless handler that builds a fresh server from a factory on every request and holds nothing between them.

This is not a routine API rename. It is the ecosystem's most-watched MCP hoster declaring, in code, where it thinks state belongs: not in the protocol session, but in explicit handles the client passes back plus a backing store the server owns. And it lands alongside the protocol change that makes that declaration possible — MCP specification 2026-07-28, published July 28, 2026, which retires the initialize handshake and the Mcp-Session-Id header and makes the protocol core stateless. AWS called it the largest MCP revision since launch.

This post is not a migration manual. It is the buy-vs-cost read a self-hosted platform needs before touching its own deploy/rollback MCP server: what giving up a stateful session buys you, what you must re-home somewhere else, and where in-flight build status and multi-step deploy context live once the session is gone.

The whole trade in one table

Here is the decision up front. Every row is unpacked below; the verdict line at the bottom is the migration guidance in one sentence.

ConcernStateful McpAgent (legacy)Stateless createMcpHandler (new default)
Session storeA Durable Object per session, SQLite-backed, with its own lifecycle and billingNone in the handler; state lives in a store you choose (D1, KV, Postgres) or not at all
Request routingSticky: requests must reach the instance that owns the sessionAny instance can answer any request; standard HTTP load balancing just works
First messageinitialize handshake, then notifications/initialized, then the real callThe first message can be the actual tool call
Protocol identityMcp-Session-Id header minted by the server, echoed by the clientPer-request _meta plus MCP-Protocol-Version, Mcp-Method, Mcp-Name headers
Multi-step contextKept in the session object across callsExplicit server-minted handles passed as ordinary tool arguments (SEP-2567)
Elicitation (asking the user mid-call)Server-pushed elicitation/create on a live sessionMulti-round-trip InputRequiredResult (MRTR) that pauses and resumes without a session
Progress updatesPushed over the session streamRequest-scoped SSE that dies with the request
Pushed requests, RPC, streams, replaySupported nativelyNot supported — redesign or keep a legacy lane

Verdict: if your tools are single-shot calls whose real state already lives in a database, migrate now — the session was pure overhead. If your server leans on pushed requests, live streams, or replay, keep a temporary legacy lane (createLegacyMcpHandler or McpAgent) beside the new stateless route and migrate in stages.

What actually changed

Three things shipped together, and they are easy to conflate. Untangle them:

1. The Agents SDK handler split (v0.20.0). agents/mcp/server now exports createMcpHandler(factory), which takes an MCP SDK v2 server factory — a function returning a fresh McpServer — and returns a callable Worker handler. The old sessionful path moved to agents/mcp as createLegacyMcpHandler, and McpAgent itself is deprecated and feature-frozen. Passing an SDK v1 server to the overloaded createMcpHandler still works but emits a deprecation warning, with removal slated for the next major.

2. The TypeScript SDK split (v2). The monolithic @modelcontextprotocol/sdk is replaced by scoped packages: @modelcontextprotocol/server@2.0.0 plus the matching client package. New servers install agents with the v2 server package; explicit legacy servers pin @modelcontextprotocol/sdk@1.30.0. Cloudflare's docs insist on exact versions per Agents release, which tells you how tightly the two are currently coupled.

3. The protocol revision (2026-07-28). The spec change is the load-bearing one. Streamable HTTP no longer opens with an initialize round trip; the server mints no Mcp-Session-Id; every JSON-RPC message arrives as a separate HTTP POST carrying protocol version, client identity, and capabilities in _meta. Servers reject requests whose Mcp-Method/Mcp-Name headers disagree with the body, so intermediaries can route without parsing payloads. Clients probe with server/discover and fall back to the legacy initialize handshake on the same connection when the server has not migrated — which is what makes the dual-lane rollout below safe rather than flag-day risky.

Note the direction of causation. The SDK deprecation is downstream of the spec: once the protocol no longer has a session, a Durable Object per session stops being infrastructure and starts being a cost center with nothing to guard. Cloudflare read that implication before most of its users did.

What you gain by deleting the session

Start with the buys, because they are real and they compound.

No per-session Durable Object lifecycle. Under McpAgent, each MCP session meant a Durable Object instance: SQLite-backed state, an alarm and hibernation model to reason about, and a billing line per session. Teams that skimped built a single global DO holding a Map of session IDs just to escape the per-session cost — an architecture that re-centralized everything the edge was supposed to distribute. Stateless removes the object, the map, and the bill. The handler is a function of (request, env): create server, dispatch, respond, drop.

Any-instance-can-answer. This is the operational headline. A session-based deployment needs sticky routing or a shared session store the moment it scales past one instance — the exact stateful-service tax the twelve-factor playbook has warned about for a decade. AWS's Well-Architected write-up of the 2026-07-28 revision makes the same point from the cloud side: sticky sessions, shared session stores, and custom observability plumbing built around session affinity are no longer necessary. A stateless MCP server scales like any HTTP service, because it is one.

The first message is the tool call. Dropping two handshake round trips (initialize, notifications/initialized) before any useful work is a latency win on every fresh client, and a simplicity win for every client author: no session establishment, no session resumption, no DELETE to terminate. Per-request bearer auth suffices because there is no session to bind the credential to beyond the request.

A smaller correctness surface. Sessions are where distributed-systems bugs breed: expired IDs, split-brain stores, replayed handshakes, resumption races. Deleting the session deletes the bug class. What remains is ordinary request handling with ordinary failure modes.

What you must re-home (and where it goes)

Now the costs — each one concrete, each one with a named stateless equivalent. This is the section that decides whether your migration is a weekend or a quarter.

Multi-step context becomes explicit handles. The session used to remember that deploy step 3 belongs to the same deployment as step 1. Stateless, the server mints a handle — a deploymentId, an orderId — returns it as an ordinary tool result, and the client passes it back as an ordinary tool argument on every follow-up (SEP-2567). The handle is opaque to the client and meaningful only as a key into your store. Design consequence: every tool in a multi-step flow gains a handle parameter, and every handle needs a lifecycle (expiry, revocation, not-found errors) you previously got from session teardown for free.

In-flight state moves to a backing store. The handle is a key; something must hold the value. On Cloudflare that is D1 for relational deploy records or KV for ephemeral build status; on a self-hosted fleet it is the Postgres or Redis you already run. The state did not disappear — it moved from an implicit per-session object to an explicit row you can inspect, query, back up, and expire. That is arguably better (debuggable beats invisible), but it is work: schema, TTLs, and a cleanup path for abandoned deploys.

Elicitation becomes multi-round-trip. If your deploy flow pauses mid-call to ask the human a question ("production is pinned to v41 — confirm rollback target?"), the old model pushed elicitation/create down a live session. The stateless equivalent is MRTR: the tool returns an InputRequiredResult, the client collects the answer, and the call resumes via per-round inputResponses — no session held open while the human thinks. Budget for client support here: both sides must speak MRTR, and older clients will not.

Progress becomes request-scoped. Streaming build logs over the session stream becomes SSE scoped to the single request (responseMode: "auto" in the handler options). Fine for "watch this build," unusable for "notify me when any build finishes" — that is a pushed request, and pushed requests are gone.

Pushed requests, RPC, streams, and replay stay behind. This is the honest boundary. If your server pushes notifications to clients unprompted, multiplexes RPC over the session, or depends on replaying session history, there is no stateless equivalent to migrate to — only a redesign (webhooks, polling, a separate realtime channel) or a deliberate decision to keep the legacy lane. Cloudflare's migration guide says this plainly: servers with those dependencies should serve stateless and legacy routes side by side during the transition, not force the migration.

Worked example: a deploy-from-chat server, stateless

Make it concrete. A deploy-from-chat MCP server exposes three tools: deploy (start a deploy of service X at ref Y), deploy_status (poll it), and rollback (revert to the previous release). Under McpAgent, the session remembered which deploy the conversation was about. Stateless, the tools look like this:

ts
// Factory: a fresh server per request, no session captured.
function createServer(env: Env) {
  const server = new McpServer({ name: "deploy", version: "2.0.0" });
 
  server.registerTool("deploy",
    { description: "Start a deploy; returns a deploymentId handle",
      inputSchema: { service: z.string(), ref: z.string() } },
    async ({ service, ref }) => {
      const id = crypto.randomUUID();            // server-minted handle
      await env.DB.insertDeploy({ id, service, ref, status: "queued" });
      await env.QUEUE.enqueueBuild(id);           // async worker does the work
      return { content: [{ type: "text", text: id }] };
    });
 
  server.registerTool("deploy_status",
    { description: "Poll a deploy by its handle",
      inputSchema: { deploymentId: z.string() } },
    async ({ deploymentId }) => {
      const row = await env.DB.getDeploy(deploymentId);
      if (!row) throw new Error(`unknown deployment: ${deploymentId}`);
      return { content: [{ type: "text", text: JSON.stringify(row) }] };
    });
 
  return server;
}
 
export default { fetch: (req, env, ctx) => createMcpHandler(createServer)(req, env, ctx) };

Three things to notice. First, the handler closes over nothing per client — env.DB is shared infrastructure, not session state. Second, the long-running work (the build) was never in the session anyway; it lives in a queue and a worker, and the MCP layer is just a typed window onto the database row. Most deploy servers will discover their session held less than they feared. Third, auth rides the request: whatever bearer check the Worker applies covers all three tools uniformly, because there is no session to bootstrap trust into.

rollback follows the same shape — it takes a deploymentId (or service name), reads current-vs-previous release from the store, and enqueues the revert. And the human-confirmation pause ("confirm rollback of payments-api to v41?") is where MRTR enters: rollback returns InputRequiredResult naming the question, the chat client renders it, and the resumed call carries the answer. No session idles while the human is away from keyboard.

The migration playbook

Cloudflare documents two paths, and the choice hinges on one question: does your server depend on sessionful features (session state, RPC, pushed requests, streams, replay)?

Direct migration — for servers that do not. Move the server definition into an SDK v2 factory, point createMcpHandler at it, pin @modelcontextprotocol/server@2.0.0, and ship. Because v2 clients probe server/discover and fall back to initialize against unmigrated servers, mixed-version fleets interoperate during rollout instead of breaking at the boundary.

Dual-lane migration — for servers that do. Keep the existing McpAgent (or createLegacyMcpHandler with WorkerTransport) on its route, add the new stateless handler on a second route, and migrate tool by tool: single-shot tools first, MRTR-able elicitation second, pushed-request features last or never. The legacy lane is explicitly temporary in the docs' framing, but "temporary" with no removal date for createLegacyMcpHandler reads more like "supported until the ecosystem finishes moving" — plan quarters, not sprints, if you serve third-party clients.

Either way, measure three things before and after: p50/p99 time-to-first-tool-result on cold clients (should drop by the handshake round trips), session-store cost and object churn (should go to zero on the migrated lane), and multi-step completion rate (must not regress — a dropped handle or an expired row is the new session bug, and it fails louder).

And know when not to migrate. A server whose core value is live collaboration — shared canvas state, pushed completions, streaming agent runs — is not a request/response service wearing a session costume. It genuinely needs server-initiated delivery, and the stateless core deliberately stopped providing it. For those, the legacy lane is not technical debt; it is the product.

What the reference move signals for self-hosted fleets

Step back from Cloudflare's primitives. The interesting signal is not "Durable Objects are bad" — they are the right tool for genuinely stateful edge workloads, and the Agents SDK's own Agent class keeps its per-instance SQLite, alarms, and workflows. The signal is that the protocol's center of gravity moved: MCP state now lives in explicit application constructs (handles plus stores), not in transport sessions. Every SDK, gateway, and hoster will converge there because the spec leaves them no session to build on.

For a self-hosted PaaS running its own deploy/rollback MCP server, that convergence is good news with a to-do list. The good news: you never had Durable Objects, so you never built the per-session habit — your deploy state was probably already a Postgres row keyed by something, which is exactly where the ecosystem just landed. The to-do list: audit whatever session map your HTTP transport kept (sticky cookies, in-memory session tables, resumption logic), replace continuation with handles, scope auth per request, and serve the 2026-07-28 shape next to the 2025 handshake until your clients catch up.

The deeper lesson is architectural. A deploy is a durable, auditable, multi-actor workflow that outlives any chat session: it starts in chat, continues in CI, pages someone at 3 AM, and gets rolled back from a different client a week later. Binding that workflow's state to an MCP session was always a category error — convenient while the protocol offered sessions, brittle the moment anything outlived them. Cloudflare deprecating McpAgent is the ecosystem correcting the category error in public. Build the handle-and-store shape now, and the next protocol revision is someone else's migration.

Bex.co is the open-source, AI-native Render alternative — push a git repo, get a running HTTPS service on machines you own. Agents are first-class operators there, and the deploy surface they drive is exactly the handle-and-store shape this post argues for: every deploy addressable, queryable, and resumable from any client. Star the repo on GitHub or deploy your first app today.

Related articles

Give your agents a chain backend

Autonomous agents hit RPC endpoints very differently than people do. See what bex router handles on their behalf.

Read the agents guide