Migrate to MCP SDK v2

Last updated Jul 28, 2026 This guide covers the MCP SDK v2 ↗︎ upgrade in Agents SDK v0.20.0. It explains how to move servers to @modelcontextprotocol/server, use a temporary legacy lane only when sessionful features require it, and update MCP clients.

Choose a server path

Use the following table to select a migration path:

Current server Migration path
SDK v1 server without sessionful dependencies Move the server definition to an SDK v2 factory and pass the factory to createMcpHandler.
SDK v1 server with sessionful dependencies Add an SDK v2 route. Keep createLegacyMcpHandler only on a temporary legacy lane while replacing those dependencies.
McpAgent without legacy stateful features Migrate directly to an SDK v2 factory and createMcpHandler.
McpAgent that uses legacy stateful features Design stateless equivalents, serve stateless and legacy lanes together, then drain the legacy lane.

Agents SDK v0.20.0 deprecates these APIs:

  • Passing an SDK v1 server to createMcpHandler. Move the server to an SDK v2 factory. Use createLegacyMcpHandler only as a temporary bridge for sessionful behavior. This overload is scheduled for removal in the next major version.
  • McpAgent. It is deprecated and feature-frozen. Migrate at your earliest convenience. No removal version is announced.
  • MCPClientManager.callTool(params, resultSchema, options) and withX402Client(...).callTool(confirm, params, resultSchema, options). Use callTool(params, options) or callTool(confirm, params, options) instead. No removal version is announced.

experimental_createMcpHandler was already deprecated and remains scheduled for removal in the next major version. Move its SDK v1 server to an SDK v2 factory. Use createLegacyMcpHandler only on a temporary sessionful lane while migrating.

Install the MCP packages

Install only the MCP package generations that your application imports. Keep the v2 version exact.

For a stateless server:

npmyarnpnpmbun

npm i agents @modelcontextprotocol/server@2.0.0 zod
yarn add agents @modelcontextprotocol/server@2.0.0 zod
pnpm add agents @modelcontextprotocol/server@2.0.0 zod
bun add agents @modelcontextprotocol/server@2.0.0 zod

For a temporary legacy lane:

npmyarnpnpmbun

npm i agents @modelcontextprotocol/sdk@1.30.0 zod
yarn add agents @modelcontextprotocol/sdk@1.30.0 zod
pnpm add agents @modelcontextprotocol/sdk@1.30.0 zod
bun add agents @modelcontextprotocol/sdk@1.30.0 zod

For an Agent that connects to MCP servers:

npmyarnpnpmbun

npm i agents @modelcontextprotocol/client@2.0.0
yarn add agents @modelcontextprotocol/client@2.0.0
pnpm add agents @modelcontextprotocol/client@2.0.0
bun add agents @modelcontextprotocol/client@2.0.0

Follow peer dependency instructions from your package manager. Update the exact MCP versions with the Agents release that supports them.

Decide whether a temporary legacy lane is required

Do not keep SDK v1 only because the server currently imports it. Move directly to an SDK v2 factory unless the endpoint depends on one of these sessionful features:

  • Protocol sessions or a supplied WorkerTransport
  • Transport storage or event replay
  • Standalone GET streams
  • Pushed elicitation, sampling, or roots requests
  • Session deletion with HTTP DELETE

If the endpoint uses one of these features, deploy the stateless route first. Keep the SDK v1 route only while you replace the sessionful dependency. Route requests with isLegacyRequest() as shown in Run stateless and legacy lanes together.

For an SDK v1 endpoint that does not use McpAgent, use createLegacyMcpHandler only on that temporary legacy branch. Remove it after clients migrate and existing sessions drain.

Move a stateless server to SDK v2

The stateless createMcpHandler accepts a factory. The factory returns McpServer or Server from @modelcontextprotocol/server.

  1. Follow the upstream TypeScript SDK v2 migration guide ↗︎ for server registration changes.
  2. Import the server from @modelcontextprotocol/server.
  3. Move server construction and registration into a factory.
  4. Keep the Worker object default export. Inside its fetch() method, pass the factory to createMcpHandler and invoke the returned callable as before. Use the handler's fetch(request, options?) method only for lower-level request integration.
  5. Do not default-export the callable returned by the Agents handler. Wrangler interprets function default exports as WorkerEntrypoint classes.
  6. Remove SDK v1 transport and session options.
  7. Test the endpoint with stateless and legacy clients.

src/index.jsjs

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);
    },
};

src/index.tsts

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);
    },
} satisfies ExportedHandler;

The Worker entrypoint remains an object. Only the handler call changes:

// SDK v1: pass a fresh constructed server.
return createMcpHandler(createServer())(request, env, ctx);

// SDK v2: pass the factory itself.
return createMcpHandler(createServer)(request, env, ctx);

Do not simplify this to export default createMcpHandler(createServer). The Agents handler is callable for composition inside another handler, but Wrangler treats any function default export as a WorkerEntrypoint class.

The SDK v2 handler creates one server for each MCP request. Concurrent Worker requests never share a connected server instance.

Handler options for stateless servers

The Agents wrapper adds route, corsOptions, allowedHostnames, allowedOriginHostnames, and authContext. It also passes supported SDK v2 options through to the upstream handler.

Common options include:

Option Behavior
route Sets the exact request path. The default is /mcp.
legacy Uses legacy compatibility by default. Set "reject" for a stateless-only endpoint.
responseMode Selects automatic, JSON, or SSE response handling.
allowedHostnames Restricts Host headers to specific hostnames.
allowedOriginHostnames Restricts browser Origins, or accepts "*" when trusted middleware validates them.
corsOptions Controls CORS response headers. Set false to remove them.
onerror Reports handler errors without changing the response.
maxSubscriptions, keepAliveMs Configure subscriptions/listen delivery.

The stateless handler rejects these SDK v1 options:

  • transport
  • storage
  • sessionIdGenerator
  • onsessioninitialized and onsessionclosed
  • enableJsonResponse
  • eventStore
  • allowedHosts and allowedOrigins
  • enableDnsRebindingProtection
  • retryInterval

Use responseMode: "json" instead of enableJsonResponse: true. JSON mode drops notifications emitted before the final result.

Origin validation on Workers

The Workers wrapper validates every present Origin. It rejects malformed, opaque, and non-HTTP Origins with 403.

Its default allowlist includes localhost-class Origins and the endpoint's workers.dev hostname. A concrete corsOptions.origin also adds its hostname automatically. The handler applies matching Host checks to localhost and workers.dev endpoints.

For a custom domain with wildcard CORS, configure both Host and Origin restrictions explicitly:

export default {
    fetch(request, env, ctx) {
        return createMcpHandler(createServer, {
            allowedHostnames: ["mcp.example.com"],
            allowedOriginHostnames: ["app.example.com"],
            corsOptions: { origin: "https://app.example.com" },
        })(request, env, ctx);
    },
};
export default {
    fetch(request, env, ctx) {
        return createMcpHandler(createServer, {
            allowedHostnames: ["mcp.example.com"],
            allowedOriginHostnames: ["app.example.com"],
            corsOptions: { origin: "https://app.example.com" },
        })(request, env, ctx);
    },
} satisfies ExportedHandler;

Set allowedOriginHostnames: "*" only when trusted middleware validates Origins before calling the handler. This value turns off the handler Origin check. MCP HTTP servers must validate browser Origins.

CORS headers do not authenticate a request. Protect the endpoint with OAuth or another authentication layer.

The handler does not infer a trusted Host allowlist from request.url. If your deployment accepts arbitrary Host values, validate them before calling the handler. For local servers outside Cloudflare Workers, follow the upstream SDK Host and Origin validation guidance.

Understand compatibility with legacy clients

The default legacy: "stateless" setting supports ordinary legacy tools, resources, and prompts. This lane uses the SDK v2 web-standard transport. It does not import WorkerTransport and is not a complete sessionful transport.

The fallback has these limits:

  • Each POST receives a new server and transport.
  • HTTP GET and DELETE return 405.
  • No MCP session ID or protocol session state persists.
  • Pushed sampling, elicitation, and roots requests fail immediately.
  • Standalone streams, event replay, and session deletion are unavailable.
  • Published experimental tasks are not supported through this fallback.

While migrating these features, route affected legacy clients to a temporary createLegacyMcpHandler or McpAgent lane.

Migrate an McpAgent server

During migration, McpAgent remains an SDK v1 server. Do not change the server import inside the legacy route to @modelcontextprotocol/server.

Migrate directly without legacy stateful features

If the server does not depend on MCP session state, RPC, pushed server-to-client requests, standalone streams, or event replay, move its tools to an SDK v2 factory and serve it with createMcpHandler.

Plan stateless equivalents for stateful features

If the server uses legacy stateful features, keep the existing McpAgent route while you design and deploy stateless equivalents:

Stateful feature on the legacy path Design for the stateless path
Application data keyed by an MCP session Store data behind an explicit application boundary such as a Durable Object, D1, KV, or R2. Address it with an authenticated, server-issued handle instead of an MCP session ID.
Multi-step interaction state Return integrity-protected requestState with input_required. Bind it to the authenticated user, original method and parameters, and an expiry.
Pushed elicitation, sampling, or roots requests Return inputRequired(...). The client fulfils the embedded requests and retries the original operation.
Standalone list-change stream Publish changes through subscriptions/listen. Clients reopen the subscription if its stream ends.
Session replay or transport recovery Make each stateless request independently recoverable. Persist business progress in application storage rather than the MCP transport.
Agent-to-McpAgent RPC Replace the protocol-session dependency with an explicit application RPC or HTTP boundary, then expose the stateless MCP tools separately.