Cloudflare Agents MCP SDK v2 Migration: McpAgent to createMcpHandler
For a stateless Cloudflare MCP server, migrate from McpAgent or an SDK v1 server to an SDK v2 server factory and call createMcpHandler(createServer) inside the Worker fetch() method. Install the package generation that matches the route:
Do not migrate every endpoint the same way. First decide whether the server depends on protocol sessions, transport storage, event replay, standalone GET streams, pushed server-to-client requests, or session deletion. Keep a temporary legacy lane for those sessionful features; move ordinary tools to the stateless handler and drain the old route deliberately.
Choose the migration path from the server’s behavior
The package import alone does not tell you whether an endpoint is stateless:
| Current server | Recommended path |
|---|---|
| SDK v1 server with ordinary tools, resources, or prompts | Move registration into an SDK v2 factory and use createMcpHandler |
McpAgent without legacy stateful behavior | Migrate directly to the SDK v2 factory |
Any server using protocol sessions or WorkerTransport | Keep a temporary legacy handler while replacing the session dependency |
Server using transport storage, event replay, standalone GET streams, pushed elicitation/sampling/roots, or DELETE session handling | Serve stateless and legacy lanes together, then remove the legacy lane after clients migrate |
Cloudflare’s Agents SDK v0.20.0 migration announcement marks McpAgent as deprecated and feature-frozen. The migration guide also deprecates passing an SDK v1 server to createMcpHandler; that overload is scheduled for removal in the next major version.
Move server construction into a factory
An SDK v2 stateless handler expects a factory. The Worker remains the default-exported object, while the factory creates a fresh McpServer for each request:
import { McpServer } from '@modelcontextprotocol/server';import { createMcpHandler } from 'agents/mcp/server';import { z } from 'zod';
function createServer() { const server = new McpServer({ name: 'example-server', version: '1.0.0', });
server.registerTool( 'hello', { description: 'Return a greeting', inputSchema: { name: z.string().optional() }, }, async ({ name }) => ({ content: [{ type: 'text', text: `Hello, ${name ?? 'World'}!` }], }), );
return server;}
export default { fetch(request, env, ctx) { return createMcpHandler(createServer)(request, env, ctx); },};The important change is the argument: SDK v2 passes the factory createServer, not a previously constructed createServer() instance. Do not default-export the callable returned by createMcpHandler; Wrangler interprets a function default export as a WorkerEntrypoint class. Keep the Worker object as the entrypoint and compose the handler inside fetch().
The SDK v2 handler creates a server for each MCP request, so concurrent Worker requests do not share one connected server instance. Put durable application state behind an explicit boundary such as a Durable Object, D1, KV, or R2 rather than relying on an MCP protocol session that the stateless route does not preserve.
Pin the right package generation
Cloudflare’s migration guide gives separate install commands. For a stateless server, use the v2 server package:
For a temporary legacy lane, keep the SDK v1 server package instead:
Install only the generation that the code imports. Do not change a legacy McpAgent route’s server import to @modelcontextprotocol/server without also migrating its server registration and transport assumptions. Pin the exact MCP package version required by the Agents SDK release, then test the lockfile in a clean install.
Reject legacy traffic on a stateless-only route
The stateless handler supports ordinary legacy requests by default. If an endpoint is intentionally stateless, make that decision explicit:
const stateless = createMcpHandler(createServer, { route: '/mcp', legacy: 'reject',});
export default { fetch(request, env, ctx) { return stateless(request, env, ctx); },};This prevents a legacy request from being consumed by the compatibility lane before your sessionful route can handle it. The stateless route does not provide a protocol session ID, persistent session state, standalone GET or DELETE behavior, event replay, or the legacy server-to-client request flow. Check those assumptions with the clients that will actually call the endpoint.
Run stateless and legacy lanes together during migration
If existing clients still need sessionful behavior, deploy both paths and route requests with isLegacyRequest():
import { isLegacyRequest } from '@modelcontextprotocol/server';import { createMcpHandler } from 'agents/mcp/server';
const stateless = createMcpHandler(createStatelessServer, { 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); },};Use createLegacyMcpHandler for a non-McpAgent SDK v1 server that needs a temporary legacy branch. Keep the old branch only while its sessionful dependencies are being replaced. Monitor which clients still use it, migrate those clients, let existing sessions drain, and remove the legacy handler and any protocol-only Durable Object binding in a separate cleanup deployment.
Do not route by a user-controlled query parameter or an unvalidated header. Let the SDK’s request classification decide the protocol lane, and preserve authentication and Origin/Host validation around both handlers. CORS headers alone do not authenticate an MCP endpoint.
Test the migration at the protocol boundary
A build that type-checks is not enough. Test both lanes with a clean client and the real proxy path:
- Confirm an ordinary tool call reaches
createMcpHandlerand does not reuse a server instance across requests. - Confirm legacy clients are classified correctly when both handlers share
/mcp. - Confirm a sessionful operation that the stateless route cannot support is routed to the temporary legacy lane or rejected intentionally.
- Verify the
MCP-Protocol-Version,Mcp-Method,Mcp-Name, and any declaredMcp-Param-*headers survive a proxy or gateway. - Test authentication, allowed Host values, allowed Origins, cancellation, and transport loss.
- Check that a clean deploy contains the v2 server package and does not accidentally bundle an unused legacy transport.
The MCP stateless HTTP session migration guide explains the protocol-level session decision in more detail. For a repeatable Cloudflare Worker check, pair this migration with the Cloudflare Workers test harness guide and run a real request against the deployed handler.
The practical rule is to migrate the server’s behavior, not just its imports: stateless tools belong in an SDK v2 factory and createMcpHandler, while sessionful features need a visible legacy lane with a removal plan.
FAQ
Is McpAgent deprecated in Cloudflare Agents SDK?
Yes. Cloudflare marks McpAgent as deprecated and feature-frozen. Servers without legacy stateful dependencies should move to an SDK v2 factory and createMcpHandler; stateful servers need a staged migration.
What package replaces @modelcontextprotocol/sdk for a stateless server?
Use @modelcontextprotocol/[email protected] with the Agents SDK and zod, then import McpServer from the v2 server package. Keep @modelcontextprotocol/[email protected] only for a temporary legacy route that still needs SDK v1 behavior.
Why must createMcpHandler receive a factory?
The SDK v2 stateless handler creates a server for each MCP request. Pass createServer, not createServer(), so the handler can construct the server per request and avoid sharing a connected instance across concurrent Worker requests.
Can a stateless MCP handler keep session state in a Durable Object?
It can keep application state in a Durable Object or another explicit storage boundary, but that is different from relying on an MCP protocol session. Design an authenticated application handle and persist the business state independently of the transport session.
References:
Cloudflare Agents: Migrate to MCP SDK v2
Report a typo or broken link, or suggest a related topic.