# Cloudflare Agents SDK now supports the new MCP spec — no more Durable Object required!

Hi, it's me! I'm super excited about this one!

Today there's a solid update to Cloudflare's Agents SDK, so let's dig into it right away!

## What was announced?

According to the Cloudflare Changelog, Agents SDK v0.20.0 (released July 27, 2026) adds client and server support for the MCP (Model Context Protocol) 2026-07-28 release candidate. With this, Workers can serve tools, prompts, resources, and elicitation without an MCP transport session or a Durable Object. Agents can also connect to both MCP 2026-07-28 servers and existing legacy servers.

## The story so far

Until now, running an MCP server on Workers usually meant using `McpAgent` and keeping session state in a Durable Object. The server-side `createMcpHandler` also took a v1 server directly, built around the assumption of persistent sessions. On the client side, you had to keep track of which protocol generation each server spoke.

## What changes

The big change is that `createMcpHandler` now accepts a server factory function. Since it creates an isolated server instance for every request, you can run a stateless MCP server on Workers without provisioning a Durable Object at all!

On top of that, the `agents/mcp/server` entry point keeps `McpAgent`, `WorkerTransport`, MCP client transports, and SDK v1 modules out of stateless server bundles, so your bundle stays smaller.

And a single route — `createMcpHandler(createServer)(request, env, ctx)` — can serve both MCP 2026-07-28 clients and legacy clients making stateless requests. For ordinary tools, prompts, and resources, you don't need separate routes or tool definitions for each protocol generation.

## Dive Deep

### How the client side works

The MCP client manager now uses `@modelcontextprotocol/client`. For each connection, it probes for MCP 2026-07-28 support with `server/discover`, and falls back to the legacy `initialize` handshake on the same connection if the server doesn't support it. Existing `addMcpServer` calls don't need a protocol-version setting or separate clients per generation.

For stateless requests, elicitation uses `input_required` through multi-round-trip requests (MRTR). The legacy path keeps using the same form and URL handlers for pushed requests. The SDK collects the input, retries the original operation, and resolves the original `callTool`, `getPrompt`, or `readResource` promise with the final result.

OAuth callbacks now validate issuer metadata through the v2 SDK, too. Discovery state and issuer-bound credentials persist across browser redirects and Durable Object hibernation.

### Writing a stateless server

Here's how simple it is to spin up a stateless server with a factory function.

```javascript
import { McpServer } from "@modelcontextprotocol/server";
import { createMcpHandler } from "agents/mcp/server";

function createServer() {
  return new McpServer({ name: "example", version: "1.0.0" });
}

export default {
  fetch(request, env, ctx) {
    return createMcpHandler(createServer)(request, env, ctx);
  },
};
```

The Workers wrapper validates the present browser Origin, supports explicit delegation to trusted Origin middleware, and exposes request handling plus typed change notifications.

### Migrating existing servers

If an existing `McpAgent` server still depends on sessionful features (protocol sessions, RPC, pushed server-to-client requests, standalone streams, or replay), you can run the stateless path side by side with the existing one. Use `isLegacyRequest()` to route only legacy traffic to the existing route.

```javascript
import { isLegacyRequest } from "@modelcontextprotocol/server";
import { createMcpHandler } from "agents/mcp/server";
import { MyMcpAgent } from "./legacy-server";
import { createServer } from "./server";

const stateless = createMcpHandler(createServer, {
  route: "/mcp",
  legacy: "reject",
});
const legacy = MyMcpAgent.serve("/mcp");

export default {
  async fetch(request, env, ctx) {
    if (await isLegacyRequest(request)) {
      return legacy.fetch(request, env, ctx);
    }
    return stateless(request, env, ctx);
  },
};
```

Once you've migrated the remaining sessionful features and let existing sessions drain, you can remove the legacy route. Upgrading is just `npm i agents@latest` (or the yarn/pnpm/bun equivalent).

### Deprecations in v0.20.0

Here's what's being deprecated in this release!

- `McpAgent` — replaced by an SDK v2 factory with `createMcpHandler`. Feature-frozen, with no removal version announced
- `createMcpHandler(v1Server, options)` — move the server to an SDK v2 factory and call `createMcpHandler(factory, options)` instead. Scheduled for removal in the next major version
- `MCPClientManager.callTool(params, resultSchema, options)` and the equivalent `withX402Client` overload — use `callTool(params, options)` or `callTool(confirm, params, options)` instead. A compatibility overload with no removal version announced

By the way, the MCP 2026-07-28 draft separately deprecates Roots, Sampling, Logging, the old HTTP+SSE transport, and Dynamic Client Registration.

## Wrap-up

- Agents SDK v0.20.0 adds client and server support for the MCP 2026-07-28 release candidate
- `createMcpHandler` now accepts a factory function, enabling stateless MCP servers without a Durable Object
- A single route can serve both MCP 2026-07-28 clients and legacy clients
- `McpAgent` is feature-frozen and deprecated; use `isLegacyRequest()` to run old and new routes side by side while migrating sessionful features

If you're running MCP servers on Workers, or want to spin up a lightweight one, this is definitely worth a look!
