diff --git a/src/content/changelog/agents/2026-07-27-agents-sdk-v0.20.0-mcp-sdk-v2.mdx b/src/content/changelog/agents/2026-07-27-agents-sdk-v0.20.0-mcp-sdk-v2.mdx
new file mode 100644
index 00000000000..a545550616e
--- /dev/null
+++ b/src/content/changelog/agents/2026-07-27-agents-sdk-v0.20.0-mcp-sdk-v2.mdx
@@ -0,0 +1,103 @@
+---
+title: "Agents SDK adds MCP Specification 2026-07-28 support"
+description: "Agents SDK v0.20.0 adds client and server support for MCP 2026-07-28, including stateless Workers and compatibility with legacy MCP servers."
+products:
+ - agents
+ - workers
+date: 2026-07-27
+---
+
+import { PackageManagers, TypeScriptExample } from "~/components";
+
+Agents SDK v0.20.0 adds client and server support for the [MCP 2026-07-28 release candidate](https://blog.modelcontextprotocol.io/posts/2026-07-28-release-candidate/). Workers can serve tools, prompts, resources, and elicitation without an MCP transport session or Durable Object. Agents can connect to both MCP 2026-07-28 servers and existing legacy servers.
+
+## Client support
+
+The MCP client manager now uses `@modelcontextprotocol/client`. For each connection, it probes for MCP 2026-07-28 support with `server/discover`. If the server does not support the stateless protocol, the client continues with the legacy `initialize` handshake on the same connection. Existing `addMcpServer` calls do not need a protocol-version setting or separate clients for each protocol generation.
+
+For stateless requests, elicitation uses `input_required` through multi-round-trip requests (MRTR). The legacy path uses the same form and URL handlers for pushed requests. The SDK collects input, retries the original operation, and resolves the original `callTool`, `getPrompt`, or `readResource` promise with its final result.
+
+OAuth callbacks now validate issuer metadata through the v2 SDK. Discovery state and issuer-bound credentials persist across browser redirects and Durable Object hibernation.
+
+## Run stateless servers
+
+`createMcpHandler` now accepts a factory that returns a server from `@modelcontextprotocol/server`. The factory creates an isolated server for each request.
+
+
+
+```ts
+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);
+ },
+} satisfies ExportedHandler;
+```
+
+
+
+The isolated `agents/mcp/server` entry keeps `McpAgent`, `WorkerTransport`, MCP client transports, and SDK v1 modules out of stateless server bundles.
+
+The Workers wrapper validates present browser Origins, supports explicit delegation to trusted Origin middleware, and exposes request handling plus typed change notifications.
+
+### Backward compatibility
+
+The same `createMcpHandler(createServer)(request, env, ctx)` route serves MCP 2026-07-28 clients and legacy clients that use stateless requests. You do not need separate routes or tool definitions for ordinary tools, prompts, and resources.
+
+`McpAgent` is deprecated and feature-frozen. Migrate existing `McpAgent` servers to the stateless handler at your earliest convenience. If a server depends on protocol sessions, RPC, pushed server-to-client requests, standalone streams, or replay, use the [migration guide](/agents/model-context-protocol/guides/migrate-to-mcp-sdk-v2/) to design stateless equivalents and run both routes while clients transition.
+
+## Migrate existing SDK v1 servers
+
+Upgrade the Agents SDK:
+
+
+
+Move ordinary SDK v1 server definitions into an SDK v2 factory and serve them with `createMcpHandler`. The handler's default legacy compatibility means most stateless deployments need only one route.
+
+If an existing `McpAgent` server still needs sessionful features, add the stateless path beside it. Use `isLegacyRequest()` to send only legacy traffic to the existing route:
+
+
+
+```ts
+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: Request, env: Env, ctx: ExecutionContext) {
+ if (await isLegacyRequest(request)) {
+ return legacy.fetch(request, env, ctx);
+ }
+ return stateless(request, env, ctx);
+ },
+} satisfies ExportedHandler;
+```
+
+
+
+Migrate the remaining sessionful features, allow existing sessions to drain, then remove the legacy route. Refer to [Migrate to MCP SDK v2](/agents/model-context-protocol/guides/migrate-to-mcp-sdk-v2/) for package changes, compatibility limits, and rollout steps.
+
+## Deprecations in v0.20.0
+
+This release deprecates the following Agents SDK APIs:
+
+| Deprecated API | Replacement | Status |
+| ------------------------------------------------------------------------------------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------- | -------------------------------------------------------- |
+| `McpAgent` | Use an SDK v2 factory with `createMcpHandler` for stateless servers. Use the migration guide to replace stateful features before removing a legacy route. | Feature-frozen. No removal version is announced. |
+| `createMcpHandler(v1Server, options)` | Move the server to an SDK v2 factory and call `createMcpHandler(factory, options)`. Use `createLegacyMcpHandler` only as a temporary bridge for sessionful features. | 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)`. | Compatibility overload. No removal version is announced. |
+
+The MCP 2026-07-28 draft separately deprecates Roots, Sampling, Logging, the old HTTP+SSE transport, and Dynamic Client Registration.
diff --git a/src/content/docs/agents/model-context-protocol/apis/agent-api.mdx b/src/content/docs/agents/model-context-protocol/apis/agent-api.mdx
index c4dbf2ccef4..9f140b05109 100644
--- a/src/content/docs/agents/model-context-protocol/apis/agent-api.mdx
+++ b/src/content/docs/agents/model-context-protocol/apis/agent-api.mdx
@@ -1,7 +1,7 @@
---
pcx_content_type: reference
title: McpAgent
-description: Build stateful MCP servers on Cloudflare by extending the McpAgent class with persistent storage and agent capabilities.
+description: Reference the deprecated, feature-frozen McpAgent class while migrating existing stateful MCP servers to stateless handlers.
tags:
- MCP
sidebar:
@@ -12,7 +12,19 @@ products:
import { TypeScriptExample, LinkCard } from "~/components";
-When you build MCP Servers on Cloudflare, you extend the [`McpAgent` class](https://github.com/cloudflare/agents/blob/main/packages/agents/src/mcp/index.ts#L32-L620), from the Agents SDK:
+`McpAgent` creates a stateful legacy MCP server backed by a Durable Object.
+
+:::caution[Deprecated]
+
+`McpAgent` remains available only for existing legacy servers while they migrate. It is deprecated and feature-frozen. Migrate to [`createMcpHandler`](/agents/model-context-protocol/apis/handler-api/) at your earliest convenience.
+
+A server that depends on MCP session state, RPC, pushed server-to-client requests, standalone streams, or replay needs a staged migration. Design stateless equivalents, add a stateless route, and serve both lanes until clients migrate and existing sessions drain.
+
+Keep importing the legacy server `McpServer` from `@modelcontextprotocol/sdk`. An SDK v2 server from `@modelcontextprotocol/server` cannot run inside `McpAgent`.
+
+Refer to [Migrate to MCP SDK v2](/agents/model-context-protocol/guides/migrate-to-mcp-sdk-v2/) for the feature mapping and dual-era rollout.
+
+:::
@@ -40,7 +52,7 @@ export class MyMCP extends McpAgent {
This means that each instance of your MCP server has its own durable state, backed by a [Durable Object](/durable-objects/), with its own [SQL database](/agents/runtime/lifecycle/state/).
-Your MCP server doesn't necessarily have to be an Agent. You can build MCP servers that are stateless, and just add [tools](/agents/model-context-protocol/protocol/tools/) to your MCP server using the `@modelcontextprotocol/sdk` package.
+A stateless server can define [tools](/agents/model-context-protocol/protocol/tools/) with `@modelcontextprotocol/server` and serve them through `createMcpHandler`.
But if you want your MCP server to:
@@ -257,9 +269,9 @@ export class MyMCP extends McpAgent {
-## Elicitation
+## Elicitation on legacy servers
-[MCP elicitation](https://modelcontextprotocol.io/specification/2025-11-25/client/elicitation) lets a server request user input while handling another request, such as a tool call. The current stable MCP specification defines two modes:
+[MCP elicitation](https://modelcontextprotocol.io/specification/2025-11-25/client/elicitation) lets a server request user input while handling another request, such as a tool call. The legacy path defines two modes:
- **Form mode** collects structured, non-sensitive data through the client.
- **URL mode** sends the user to an out-of-band interaction, such as third-party authorization or payment.
@@ -371,7 +383,7 @@ switch (result.action) {
:::note[MCP client support]
-Not all MCP clients implement elicitation. Check the client before depending on it and provide a fallback when appropriate. Agents acting as MCP clients can handle both modes through [MCP client elicitation handlers](/agents/model-context-protocol/apis/client-api/#elicitation).
+Not all MCP clients implement legacy elicitation. Check the client before depending on it and provide a fallback when appropriate. Agents acting as MCP clients can handle both modes through [MCP client elicitation handlers](/agents/model-context-protocol/apis/client-api/#elicitation).
:::
For more human-in-the-loop patterns, refer to [Human-in-the-loop patterns](/agents/concepts/agentic-patterns/human-in-the-loop/).
diff --git a/src/content/docs/agents/model-context-protocol/apis/client-api.mdx b/src/content/docs/agents/model-context-protocol/apis/client-api.mdx
index 8552f8b4ce9..2b061bc435e 100644
--- a/src/content/docs/agents/model-context-protocol/apis/client-api.mdx
+++ b/src/content/docs/agents/model-context-protocol/apis/client-api.mdx
@@ -12,7 +12,9 @@ products:
import { Render, TypeScriptExample, LinkCard } from "~/components";
-Connect your agent to external [Model Context Protocol (MCP)](/agents/model-context-protocol/) servers to use their tools, resources, and prompts. This enables your agent to interact with GitHub, Slack, databases, and other services through a standardized protocol.
+Connect your agent to external [Model Context Protocol (MCP)](/agents/model-context-protocol/) servers to use their tools, resources, and prompts. Agents SDK v0.20.0 uses `@modelcontextprotocol/client` and negotiates stateless or legacy behavior automatically.
+
+Refer to [Migrate to MCP SDK v2](/agents/model-context-protocol/guides/migrate-to-mcp-sdk-v2/) for package, type, OAuth provider, and rollout changes.
## Overview
@@ -393,15 +395,15 @@ for (const prompt of state.prompts) {
### Elicitation
-[MCP elicitation](https://modelcontextprotocol.io/specification/2025-11-25/client/elicitation) lets a server request user input while handling another request, such as a tool call. The current stable MCP specification defines form and URL modes.
+MCP servers can request user input while handling another operation. On the stateless path, elicitation returns `input_required` and completes through multi-round-trip requests (MRTR). On the legacy path, the server sends pushed `elicitation/create` requests. Both use form and URL modes.
-Register a handler for each mode your Agent supports in `onStart()`:
+Register a handler for each mode your Agent supports in `onStart()`. The same handlers serve both lanes:
```ts
import { Agent } from "agents";
-import type { ElicitRequest, ElicitResult } from "agents/mcp";
+import type { ElicitRequest, ElicitResult } from "agents/mcp/client";
class MyAgent extends Agent {
onStart() {
@@ -432,7 +434,7 @@ The `serverId` identifies the connection that sent the request. Use it to tell t
#### Capability negotiation and hibernation
-At the MCP `initialize` handshake, a connection advertises only the modes with configured handlers. A form-only handler advertises form mode. A URL-only handler advertises URL mode. A connection without handlers advertises no elicitation capability, which lets the server use its fallback.
+The SDK advertises only the modes with configured handlers. Connections on the legacy path advertise them during `initialize`. Requests on the stateless path carry them with request capabilities. A form-only handler advertises form mode. A URL-only handler advertises URL mode. A connection without handlers advertises no elicitation capability, which lets the server use its fallback.
The SDK stores the advertised modes with each server registration. A connection restored after Durable Object hibernation can therefore advertise the same modes when it reconnects. Callback functions remain in memory and reattach when `onStart()` runs.
@@ -510,7 +512,7 @@ A handler returns a promise, but the response often comes from a browser. Broadc
```ts
import { Agent, callable } from "agents";
-import type { ElicitRequest, ElicitResult } from "agents/mcp";
+import type { ElicitRequest, ElicitResult } from "agents/mcp/client";
type PendingResolver = {
resolve: (result: ElicitResult) => void;
@@ -570,9 +572,9 @@ class MyAgent extends Agent {
The example uses a 55-second timeout because MCP SDK requests default to 60 seconds. If your client call sets a longer request timeout, adjust this timeout to finish first.
-Refer to the [`mcp-client` example](https://github.com/cloudflare/agents/tree/main/examples/mcp-client) for the browser implementation. The [`mcp-elicitation` example](https://github.com/cloudflare/agents/tree/main/examples/mcp-elicitation) is a server that sends both modes.
+Refer to the [`mcp-client` example](https://github.com/cloudflare/agents/tree/main/examples/mcp-client) for the browser implementation. [`mcp-elicitation-mrtr`](https://github.com/cloudflare/agents/tree/main/examples/mcp-elicitation-mrtr) demonstrates stateless elicitation. [`mcp-elicitation`](https://github.com/cloudflare/agents/tree/main/examples/mcp-elicitation) demonstrates legacy elicitation.
-To send elicitation requests from an MCP server, refer to [`elicitInput`](/agents/model-context-protocol/apis/agent-api/#elicitinputoptions-context).
+For server-side patterns, refer to [Elicitation with a stateless handler](/agents/model-context-protocol/apis/handler-api/#elicitation-with-a-stateless-handler) and [Elicitation on legacy servers](/agents/model-context-protocol/apis/agent-api/#elicitation-on-legacy-servers).
## Managing servers
@@ -686,7 +688,7 @@ async addMcpServer(
callbackHost?: string;
callbackPath?: string;
agentsPrefix?: string;
- client?: ClientOptions;
+ client?: McpClientOptions;
transport?: {
headers?: HeadersInit;
type?: "sse" | "streamable-http" | "auto";
@@ -705,7 +707,7 @@ async addMcpServer(
options?: {
id?: string;
props?: Record;
- client?: ClientOptions;
+ client?: McpClientOptions;
retry?: RetryOptions;
}
): Promise<{ id: string; state: "ready" }>
@@ -720,7 +722,7 @@ async addMcpServer(
- `callbackHost` — Host for OAuth callback URL. Only needed for OAuth-authenticated servers. If omitted, automatically derived from the incoming request or WebSocket connection URI — you typically do not need to set this unless you are using a custom domain that differs from the Worker's hostname
- `callbackPath` — Custom callback URL path that bypasses the default `/agents/{class}/{name}/callback` construction. **Required when `sendIdentityOnConnect` is `false`** to prevent leaking the instance name. When set, the callback URL becomes `{callbackHost}/{callbackPath}`. You must route this path to the agent instance via `getAgentByName`
- `agentsPrefix` — URL prefix for OAuth callback path. Default: `"agents"`. Ignored when `callbackPath` is provided
- - `client` — MCP client configuration options (passed to `@modelcontextprotocol/sdk` Client constructor). By default, includes `CfWorkerJsonSchemaValidator` for validating tool parameters against JSON schemas
+ - `client` — The Agents-supported `McpClientOptions` subset from `@modelcontextprotocol/client`. The default validator supports JSON Schema 2020-12 and legacy draft-07 schemas in Workers
- `transport` — Transport layer configuration:
- `headers` — Custom HTTP headers for authentication
- `type` — Transport type: `"auto"` (default), `"streamable-http"`, or `"sse"`
@@ -870,17 +872,19 @@ export class MyAgent extends Agent {
### `configureElicitationHandlers()`
-Configure handlers for server-initiated `elicitation/create` requests. Add a handler for each elicitation mode your Agent supports.
+Configure handlers for stateless elicitation and legacy `elicitation/create` requests. Add a handler for each elicitation mode your Agent supports.
```txt
this.mcp.configureElicitationHandlers(handlers?: {
form?: (
request: ElicitRequest,
serverId: string,
+ signal?: AbortSignal,
) => Promise;
url?: (
request: ElicitRequest,
serverId: string,
+ signal?: AbortSignal,
) => Promise;
}): void
```
@@ -892,6 +896,7 @@ this.mcp.configureElicitationHandlers(handlers?: {
- `url` (function, optional) — Handles URL-mode requests for out-of-band interactions.
- `request` (`ElicitRequest`) — The MCP elicitation request. Inspect `request.params.mode` for the mode-specific fields.
- `serverId` (string) — The ID of the MCP server connection that sent the request.
+- `signal` (`AbortSignal`, optional) — Aborts when the originating MCP operation is cancelled.
Each handler returns a promise containing an `ElicitResult`. Return `accept`, `decline`, or `cancel`. Accepted form responses include `content` that matches `requestedSchema`. URL responses omit `content`.
@@ -899,7 +904,7 @@ Passing `undefined` clears all configured handlers.
#### Capability behavior
-The client advertises only modes with configured handlers during the MCP `initialize` handshake. Handler changes apply immediately to live connections, but servers receive updated advertised modes after those connections reconnect.
+The client advertises only modes with configured handlers during legacy negotiation and on stateless requests. Handler changes apply immediately to live connections, but servers receive updated advertised modes after those connections reconnect.
The SDK stores the handler-derived modes with each MCP server registration. Restored connections advertise those modes after Durable Object hibernation, and callbacks reattach when `onStart()` runs.
@@ -911,7 +916,7 @@ Configure handlers in `onStart()`:
```ts
import { Agent } from "agents";
-import type { ElicitRequest, ElicitResult } from "agents/mcp";
+import type { ElicitRequest, ElicitResult } from "agents/mcp/client";
export class MyAgent extends Agent {
onStart() {
diff --git a/src/content/docs/agents/model-context-protocol/apis/handler-api.mdx b/src/content/docs/agents/model-context-protocol/apis/handler-api.mdx
index 18db6e245d8..e4664e7a455 100644
--- a/src/content/docs/agents/model-context-protocol/apis/handler-api.mdx
+++ b/src/content/docs/agents/model-context-protocol/apis/handler-api.mdx
@@ -1,7 +1,7 @@
---
pcx_content_type: reference
-title: createMcpHandler
-description: Create a stateless MCP server fetch handler for a plain Worker using createMcpHandler and streamable HTTP transport.
+title: MCP handler APIs
+description: Create stateless or legacy MCP server handlers for Cloudflare Workers with the Agents SDK.
tags:
- MCP
sidebar:
@@ -10,543 +10,342 @@ products:
- agents
---
-import { TypeScriptExample, LinkCard } from "~/components";
+import { LinkCard, PackageManagers, TypeScriptExample } from "~/components";
-The `createMcpHandler` function creates a fetch handler to serve your [MCP server](/agents/model-context-protocol/). Use it when you want a stateless MCP server that runs in a plain Worker (no Durable Object). For stateful MCP servers that persist state across requests, use the [`McpAgent`](/agents/model-context-protocol/apis/agent-api/) class instead.
+The Agents SDK provides two server handler paths:
-It uses an implementation of the MCP Transport interface, `WorkerTransport`, built on top of web standards, which conforms to the [streamable-http](https://modelcontextprotocol.io/specification/draft/basic/transports/#streamable-http) transport specification.
+| API | Import path | MCP server package | Behavior |
+| ------------------------ | ------------------- | ------------------------------ | ---------------------------------------------- |
+| `createMcpHandler` | `agents/mcp/server` | `@modelcontextprotocol/server` | stateless with legacy compatibility by default |
+| `createLegacyMcpHandler` | `agents/mcp` | `@modelcontextprotocol/sdk` | legacy sessions through `WorkerTransport` |
-```ts
-import { createMcpHandler, type CreateMcpHandlerOptions } from "agents/mcp";
-import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
-
-function createMcpHandler(
- server: McpServer,
- options?: CreateMcpHandlerOptions,
-): (request: Request, env: Env, ctx: ExecutionContext) => Promise;
-```
-
-#### Parameters
+`McpAgent` is deprecated and feature-frozen. Migrate existing `McpAgent` servers to a stateless handler. Refer to the [migration guide](/agents/model-context-protocol/guides/migrate-to-mcp-sdk-v2/) when sessionful features require a staged rollout.
-- **server** — An instance of [`McpServer`](https://modelcontextprotocol.io/docs/develop/build-server#node) from the `@modelcontextprotocol/sdk` package
-- **options** — Optional configuration object (see [`CreateMcpHandlerOptions`](#createmcphandleroptions))
+## Install dependencies
-#### Returns
+For a stateless server:
-A Worker fetch handler function with the signature `(request: Request, env: unknown, ctx: ExecutionContext) => Promise`.
-
-### CreateMcpHandlerOptions
-
-Configuration options for creating an MCP handler.
-
-```ts
-interface CreateMcpHandlerOptions extends WorkerTransportOptions {
- /**
- * The route path that this MCP handler should respond to.
- * If specified, the handler will only process requests that match this route.
- * @default "/mcp"
- */
- route?: string;
-
- /**
- * An optional auth context to use for handling MCP requests.
- * If not provided, the handler will look for props in the execution context.
- */
- authContext?: McpAuthContext;
-
- /**
- * An optional transport to use for handling MCP requests.
- * If not provided, a WorkerTransport will be created with the provided WorkerTransportOptions.
- */
- transport?: WorkerTransport;
-
- // Inherited from WorkerTransportOptions:
- sessionIdGenerator?: () => string;
- enableJsonResponse?: boolean;
- onsessioninitialized?: (sessionId: string) => void;
- corsOptions?: CORSOptions;
- storage?: MCPStorageApi;
-}
-```
+
-#### Options
+For an explicit legacy server:
-##### route
+
-The URL path where the MCP handler responds. Requests to other paths return a 404 response.
+Use the exact MCP versions required by your installed Agents release while the v2 SDK remains in beta.
-**Default:** `"/mcp"`
+## `createMcpHandler`
-
+`createMcpHandler` creates a callable stateless MCP request handler from an MCP SDK v2 server factory. Invoke it from a Worker's object `fetch()` export or compose it inside another handler.
```ts
-const handler = createMcpHandler(server, {
- route: "/api/mcp", // Only respond to requests at /api/mcp
-});
-```
-
-
-
-#### authContext
-
-An authentication context object that will be available to MCP tools via [`getMcpAuthContext()`](/agents/model-context-protocol/apis/handler-api/#authentication-context).
+import {
+ createMcpHandler,
+ type CreateMcpHandlerOptions,
+ type StatelessMcpHandler,
+} from "agents/mcp/server";
+import type { McpServerFactory } from "@modelcontextprotocol/server";
-When using the [`OAuthProvider`](/agents/model-context-protocol/protocol/authorization/) from `@cloudflare/workers-oauth-provider`, the authentication context is automatically populated with information from the OAuth flow. You typically don't need to set this manually.
+function createMcpHandler(
+ factory: McpServerFactory,
+ options?: CreateMcpHandlerOptions,
+): StatelessMcpHandler;
+```
-#### transport
+### Parameters
-A custom `WorkerTransport` instance. If not provided, a new transport is created on every request.
+- `factory` creates a fresh `McpServer` or `Server` from `@modelcontextprotocol/server`. It can be synchronous or asynchronous.
+- `options` combines Agents Worker options with supported upstream SDK v2 handler options.
-
+The factory receives this request context:
```ts
-import { createMcpHandler, WorkerTransport } from "agents/mcp";
-
-const transport = new WorkerTransport({
- sessionIdGenerator: () => `session-${crypto.randomUUID()}`,
- storage: {
- get: () => myStorage.get("transport-state"),
- set: (state) => myStorage.put("transport-state", state),
- },
-});
-
-const handler = createMcpHandler(server, { transport });
+interface McpRequestContext {
+ era: "modern" | "legacy";
+ authInfo?: AuthInfo;
+ requestInfo?: Request;
+}
```
-
-
-## Stateless MCP Servers
+A zero-argument factory remains valid.
-Many MCP Servers are stateless, meaning they do not maintain any session state between requests. The `createMcpHandler` function is a lightweight alternative to the `McpAgent` class that can be used to serve an MCP server straight from a Worker. View the [complete example on GitHub](https://github.com/cloudflare/agents/tree/main/examples/mcp-worker).
-
-:::caution[Breaking change in MCP SDK 1.26.0]
-
-**Important:** If you are upgrading from MCP SDK versions before 1.26.0, you must update how you create `McpServer` instances in stateless servers.
-
-MCP SDK 1.26.0 introduces a guard that prevents connecting to a server instance that has already been connected to a transport. This fixes a security vulnerability ([CVE](https://github.com/modelcontextprotocol/typescript-sdk/security/advisories/GHSA-345p-7cg4-v4c7)) where sharing server or transport instances could leak cross-client response data.
-
-**If your stateless MCP server declares `McpServer` or transport instances in the global scope, you must create new instances per request.**
-
-See the [migration guide](/agents/model-context-protocol/apis/handler-api/#migration-guide-for-mcp-sdk-1260) below for details.
-:::
+### Example
```ts title="src/index.ts"
-import { createMcpHandler } from "agents/mcp";
-import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
+import { McpServer } from "@modelcontextprotocol/server";
+import { createMcpHandler } from "agents/mcp/server";
import { z } from "zod";
function createServer() {
const server = new McpServer({
- name: "Hello MCP Server",
+ name: "hello-server",
version: "1.0.0",
});
- server.tool(
+ server.registerTool(
"hello",
- "Returns a greeting message",
- { name: z.string().optional() },
- async ({ name }) => {
- return {
- content: [
- {
- text: `Hello, ${name ?? "World"}!`,
- type: "text",
- },
- ],
- };
+ {
+ description: "Return a greeting",
+ inputSchema: { name: z.string().optional() },
},
+ async ({ name }) => ({
+ content: [{ type: "text", text: `Hello, ${name ?? "World"}!` }],
+ }),
);
return server;
}
export default {
- fetch: async (request: Request, env: Env, ctx: ExecutionContext) => {
- // Create new server instance per request
- const server = createServer();
- return createMcpHandler(server)(request, env, ctx);
+ fetch(request, env, ctx) {
+ return createMcpHandler(createServer)(request, env, ctx);
},
-} satisfies ExportedHandler;
-```
-
-
-
-Each request to this MCP server creates a new session and server instance. The server does not maintain state between requests. This is the simplest way to implement an MCP server.
-
-## Stateful MCP Servers
-
-For stateful MCP servers that need to maintain session state across multiple requests, you can use the `createMcpHandler` function with a `WorkerTransport` instance directly in an `Agent`. This is useful if you want to make use of advanced client features like elicitation and sampling.
-
-Provide a custom `WorkerTransport` with persistent storage. View the [complete example on GitHub](https://github.com/cloudflare/agents/tree/main/examples/mcp-elicitation).
-
-
-
-```ts title="src/index.ts"
-import { Agent } from "agents";
-import {
- createMcpHandler,
- WorkerTransport,
- type TransportState,
-} from "agents/mcp";
-import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
-
-const STATE_KEY = "mcp-transport-state";
-
-type State = { counter: number };
-
-export class MyStatefulMcpAgent extends Agent {
- server = new McpServer({
- name: "Stateful MCP Server",
- version: "1.0.0",
- });
-
- transport = new WorkerTransport({
- sessionIdGenerator: () => this.name,
- storage: {
- get: () => {
- return this.ctx.storage.get(STATE_KEY);
- },
- set: (state: TransportState) => {
- this.ctx.storage.put(STATE_KEY, state);
- },
- },
- });
-
- async onRequest(request: Request) {
- return createMcpHandler(this.server, {
- transport: this.transport,
- })(request, this.env, this.ctx as unknown as ExecutionContext);
- }
-}
+} satisfies ExportedHandler;
```
-In this case we are defining the `sessionIdGenerator` to return the Agent name as the session ID. To make sure we route to the correct Agent we can use `getAgentByName` in the Worker handler:
+Pass the factory itself. Do not create one global server instance or pass a constructed SDK v2 server directly.
-
+### `CreateMcpHandlerOptions`
-```ts
-import { getAgentByName } from "agents";
+The following options are available:
-export default {
- async fetch(request: Request, env: Env, ctx: ExecutionContext) {
- // Extract session ID from header or generate a new one
- const sessionId =
- request.headers.get("mcp-session-id") ?? crypto.randomUUID();
+| Option | Type | Default | Description |
+| ------------------------ | --------------------------- | ------------------------------------------------- | ------------------------------------------------------------- |
+| `route` | `string` | `"/mcp"` | Exact path handled by the Worker wrapper |
+| `corsOptions` | `CORSOptions \| false` | Wildcard CORS | CORS response headers, or `false` to remove them |
+| `allowedHostnames` | `string[]` | Localhost or `workers.dev` route | Optional Host restriction for custom domains |
+| `allowedOriginHostnames` | `string[] \| "*"` | Localhost, `workers.dev`, or concrete CORS Origin | Browser Origin restriction, or explicit middleware delegation |
+| `authContext` | `McpAuthContext` | Execution context props | Application props returned by `getMcpAuthContext()` |
+| `legacy` | `"stateless" \| "reject"` | `"stateless"` | legacy compatibility or stateless-only rejection |
+| `responseMode` | `"auto" \| "json" \| "sse"` | `"auto"` | stateless request response shaping |
+| `onerror` | `(error: Error) => void` | None | Out-of-band error reporting |
+| `maxSubscriptions` | `number` | `1,024` | Maximum concurrent listen streams |
+| `keepAliveMs` | `number` | `15,000` | Keepalive interval for listen streams |
- // Get the Agent instance by name/session ID
- const agent = await getAgentByName(env.MyStatefulMcpAgent, sessionId);
+SDK v1 transport options do not apply to this handler. It rejects options such as `transport`, `storage`, `sessionIdGenerator`, `eventStore`, and `enableJsonResponse`.
- // Route the MCP request to the agent
- return await agent.onRequest(request);
- },
-} satisfies ExportedHandler;
-```
+Use `responseMode: "json"` instead of `enableJsonResponse: true`. JSON mode drops notifications emitted before a final result.
-
+### Factory lifecycle
-With persistent storage, the transport preserves:
+The handler creates one MCP server for each request. This follows the draft protocol model, where version, identity, and capabilities travel with every request rather than through a protocol session.
-- Session ID across reconnections
-- Protocol version negotiation state
-- Initialization status
+Application data can still be durable. Store cross-request data behind an authenticated handle in a Durable Object, D1, KV, or R2 rather than an MCP session ID.
-This allows MCP clients to reconnect and resume their session in the event of a connection loss.
+### Elicitation with a stateless handler
-## Migration Guide for MCP SDK 1.26.0
+Elicitation through a stateless handler returns `input_required` and completes through multi-round-trip requests (MRTR). On each retry, the SDK echoes the latest `requestState` and sends responses for the immediately preceding input round. It does not accumulate earlier `inputResponses`. The Worker does not remain suspended while a user responds.
-The MCP SDK 1.26.0 introduces a breaking change for stateless MCP servers that addresses a critical security vulnerability where responses from one client could leak to another client when using shared server or transport instances.
+Use `inputRequired(...)` to request input. Read that round's accepted form content from `context.mcpReq.inputResponses` with `acceptedContent(...)`. Seal trusted intermediate values needed by later rounds into integrity-protected `requestState`.
-### Who is affected?
+Refer to the [stateless elicitation example](https://github.com/cloudflare/agents/tree/main/examples/mcp-elicitation-mrtr) for a two-round tool flow. For stateful pushed requests, refer to [Elicitation on legacy servers](/agents/model-context-protocol/apis/agent-api/#elicitation-on-legacy-servers).
-| Server Type | Affected? | Action Required |
-| --------------------------------------------- | --------- | ------------------------------------------------ |
-| Stateful servers using `Agent`/Durable Object | No | No changes needed |
-| Stateless servers using `createMcpHandler` | Yes | Create new `McpServer` per request |
-| Stateless servers using raw SDK transport | Yes | Create new `McpServer` and transport per request |
+### Origin validation and CORS
-### Why is this necessary?
+The Workers wrapper validates every present browser Origin. It rejects malformed, opaque, and non-HTTP Origins with `403`. Origin-less non-browser MCP clients remain valid.
-The previous pattern of declaring `McpServer` instances in the global scope allowed responses from one client to leak to another client. This is a security vulnerability. The new SDK version prevents this by throwing an error if you try to connect a server that is already connected.
+The default allowlist includes localhost-class Origins, the endpoint's `workers.dev` hostname, and a concrete hostname from `corsOptions.origin`. The handler also applies matching Host checks to localhost and `workers.dev` endpoints. This keeps local DNS rebinding protection without requiring a separate Origin list for the common Workers routes.
-### Before (broken with SDK 1.26.0)
+For a custom domain with wildcard CORS, set `allowedHostnames` and `allowedOriginHostnames` explicitly. If `corsOptions.origin` is a concrete URL, the handler derives its Origin hostname automatically:
```ts
-import { createMcpHandler } from "agents/mcp";
-import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
-
-// INCORRECT: Global server instance
-const server = new McpServer({
- name: "Hello MCP Server",
- version: "1.0.0",
-});
-
-server.tool("hello", "Returns a greeting", {}, async () => {
- return {
- content: [{ text: "Hello, World!", type: "text" }],
- };
-});
-
export default {
- fetch: async (request: Request, env: Env, ctx: ExecutionContext) => {
- // This will fail on second request with MCP SDK 1.26.0+
- return createMcpHandler(server)(request, env, ctx);
+ fetch(request, env, ctx) {
+ return createMcpHandler(createServer, {
+ allowedHostnames: ["mcp.example.com"],
+ corsOptions: {
+ origin: "https://app.example.com",
+ },
+ })(request, env, ctx);
},
-} satisfies ExportedHandler;
+} satisfies ExportedHandler;
```
-### After (correct)
+Allowlist values are hostnames without a scheme or port. Origin matching ignores scheme and port.
-
+Set `allowedOriginHostnames: "*"` only when trusted middleware validates Origins before calling the handler. This value turns off the handler Origin check, including malformed and opaque Origin rejection. MCP HTTP servers must validate browser Origins.
-```ts
-import { createMcpHandler } from "agents/mcp";
-import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
+CORS response headers are not authentication. Protect the MCP endpoint with OAuth or another authentication layer.
-// CORRECT: Factory function to create server instance
-function createServer() {
- const server = new McpServer({
- name: "Hello MCP Server",
- version: "1.0.0",
- });
+The handler does not infer a Host allowlist from `request.url`. If a deployment accepts arbitrary Host values, validate them before calling the handler. Local servers outside Cloudflare Workers should follow the upstream SDK DNS rebinding guidance.
- server.tool("hello", "Returns a greeting", {}, async () => {
- return {
- content: [{ text: "Hello, World!", type: "text" }],
- };
- });
+### Compatibility with legacy clients
- return server;
-}
+The default `legacy: "stateless"` setting accepts ordinary legacy tools, prompts, and resources. This lane uses the SDK v2 web-standard transport and does not import `WorkerTransport`.
-export default {
- fetch: async (request: Request, env: Env, ctx: ExecutionContext) => {
- // Create new server instance per request
- const server = createServer();
- return createMcpHandler(server)(request, env, ctx);
- },
-} satisfies ExportedHandler;
-```
+This compatibility path does not provide a complete session transport:
-
+- Each POST creates a new server and transport.
+- HTTP GET and DELETE return `405`.
+- No MCP session ID persists.
+- Pushed elicitation, sampling, and roots requests fail immediately.
+- Standalone streams, resumability, replay, and session deletion are unavailable.
+- Published experimental tasks are not supported through this path.
-### For raw SDK transport users
+Set `legacy: "reject"` for a stateless-only endpoint. During migration, route legacy clients that still require protocol sessions to a temporary `createLegacyMcpHandler` or `McpAgent` lane.
-If you are using the raw SDK transport directly (not via `createMcpHandler`), you must also create new transport instances per request:
+### Return value
-
+`createMcpHandler` returns a `StatelessMcpHandler`. It is callable and exposes request and notification controls:
```ts
-import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
-import { WebStandardStreamableHTTPServerTransport } from "@modelcontextprotocol/sdk/server/webStandardStreamableHttp.js";
+interface StatelessMcpHandler {
+ (request: Request, env: unknown, ctx: ExecutionContext): Promise;
-function createServer() {
- const server = new McpServer({
- name: "Hello MCP Server",
- version: "1.0.0",
- });
-
- // Register tools...
+ fetch(
+ request: Request,
+ options?: McpHandlerRequestOptions,
+ ): Promise;
- return server;
+ notify: {
+ toolsChanged(): void;
+ promptsChanged(): void;
+ resourcesChanged(): void;
+ resourceUpdated(uri: string): void;
+ };
}
-export default {
- async fetch(request: Request) {
- // Create new transport and server per request
- const transport = new WebStandardStreamableHTTPServerTransport();
- const server = createServer();
- server.connect(transport);
- return transport.handleRequest(request);
- },
-} satisfies ExportedHandler;
+type McpHandlerRequestOptions = {
+ authInfo?: AuthInfo;
+ parsedBody?: unknown;
+};
```
-
-
-### WorkerTransport
+#### Invoke the handler
-The `WorkerTransport` class implements the MCP Transport interface, handling HTTP request/response cycles, Server-Sent Events (SSE) streaming, session management, and CORS.
+Call the handler from a Worker's object `fetch()` export:
```ts
-class WorkerTransport implements Transport {
- sessionId?: string;
- started: boolean;
- onclose?: () => void;
- onerror?: (error: Error) => void;
- onmessage?: (message: JSONRPCMessage, extra?: MessageExtraInfo) => void;
-
- constructor(options?: WorkerTransportOptions);
-
- async handleRequest(
- request: Request,
- parsedBody?: unknown,
- ): Promise;
- async send(
- message: JSONRPCMessage,
- options?: TransportSendOptions,
- ): Promise;
- async start(): Promise;
- async close(): Promise;
-}
-```
-
-#### Constructor Options
-
-```ts
-interface WorkerTransportOptions {
- /**
- * Function that generates a unique session ID.
- * Called when a new session is initialized.
- */
- sessionIdGenerator?: () => string;
-
- /**
- * Enable traditional Request/Response mode, disabling streaming.
- * When true, responses are returned as JSON instead of SSE streams.
- * @default false
- */
- enableJsonResponse?: boolean;
-
- /**
- * Callback invoked when a session is initialized.
- * Receives the generated or restored session ID.
- */
- onsessioninitialized?: (sessionId: string) => void;
-
- /**
- * CORS configuration for cross-origin requests.
- * Configures Access-Control-* headers.
- */
- corsOptions?: CORSOptions;
-
- /**
- * Optional storage API for persisting transport state.
- * Use this to store session state in Durable Object/Agent storage
- * so it survives hibernation/restart.
- */
- storage?: MCPStorageApi;
-}
+export default {
+ fetch(request, env, ctx) {
+ return createMcpHandler(createServer)(request, env, ctx);
+ },
+} satisfies ExportedHandler;
```
-#### sessionIdGenerator
-
-Provides a custom session identifier. This session identifier is used to identify the session in the MCP Client.
+Do not export the callable directly as a Worker's default export. Wrangler treats function default exports as `WorkerEntrypoint` classes.
-
+Use `fetch()` when another framework or authentication layer has already parsed or validated request data:
```ts
-const transport = new WorkerTransport({
- sessionIdGenerator: () => `user-${Date.now()}-${Math.random()}`,
+const response = await handler.fetch(request, {
+ authInfo,
+ parsedBody,
});
```
-
+`authInfo` is passed to the server factory and request handlers. The handler does not derive it from request headers or verify access tokens. `parsedBody` avoids reparsing a JSON body that upstream middleware already consumed.
-#### enableJsonResponse
+#### Publish list and resource changes
-Disables SSE streaming and returns responses as standard JSON.
+The `notify` methods publish typed change events to matching open `subscriptions/listen` streams:
-
+| Method | MCP notification |
+| ----------------------------- | -------------------------------------- |
+| `notify.toolsChanged()` | `notifications/tools/list_changed` |
+| `notify.promptsChanged()` | `notifications/prompts/list_changed` |
+| `notify.resourcesChanged()` | `notifications/resources/list_changed` |
+| `notify.resourceUpdated(uri)` | `notifications/resources/updated` |
-```ts
-const transport = new WorkerTransport({
- enableJsonResponse: true, // Disable streaming, return JSON responses
-});
-```
-
-
+Calling a notifier when no matching subscription is open is a no-op.
-#### onsessioninitialized
+#### Keep one handler for notifications
-A callback that fires when a session is initialized, either by creating a new session or restoring from storage.
+Notification routing belongs to the handler instance. Constructing a new handler inside every Worker `fetch()` call is suitable for ordinary tools, prompts, resources, and MRTR elicitation. It cannot notify a `subscriptions/listen` stream owned by an earlier handler instance.
-
+Create the handler once at module scope when using `notify` or `subscriptions/listen`, then invoke it from the Worker object export:
```ts
-const transport = new WorkerTransport({
- onsessioninitialized: (sessionId) => {
- console.log(`MCP session initialized: ${sessionId}`);
+const handler = createMcpHandler(createServer);
+
+export default {
+ fetch(request, env, ctx) {
+ return handler(request, env, ctx);
},
-});
+} satisfies ExportedHandler;
```
-
+Notifications are isolate-local. A notification published in one Worker isolate does not reach a subscription stream running in another isolate.
-#### corsOptions
+## `createLegacyMcpHandler`
-Configure CORS headers for cross-origin requests.
+`createLegacyMcpHandler` serves an SDK v1 server through `WorkerTransport`.
```ts
-interface CORSOptions {
- origin?: string;
- methods?: string;
- headers?: string;
- maxAge?: number;
- exposeHeaders?: string;
-}
+import {
+ createLegacyMcpHandler,
+ type CreateLegacyMcpHandlerOptions,
+ type LegacyMcpHandler,
+} from "agents/mcp";
+import type { Server } from "@modelcontextprotocol/sdk/server/index.js";
+import type { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
+
+function createLegacyMcpHandler(
+ server: McpServer | Server,
+ options?: CreateLegacyMcpHandlerOptions,
+): LegacyMcpHandler;
```
-
+Use this handler only as a temporary migration bridge when an existing SDK v1 endpoint still requires legacy sessions, transport storage, event replay, or pushed server-to-client requests.
+
+
```ts
-const transport = new WorkerTransport({
- corsOptions: {
- origin: "https://example.com",
- methods: "GET, POST, OPTIONS",
- headers: "Content-Type, Authorization",
- maxAge: 86400,
+import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
+import { createLegacyMcpHandler } from "agents/mcp";
+
+function createServer() {
+ return new McpServer({ name: "legacy-server", version: "1.0.0" });
+}
+
+export default {
+ async fetch(request: Request, env: Env, ctx: ExecutionContext) {
+ return createLegacyMcpHandler(createServer())(request, env, ctx);
},
-});
+} satisfies ExportedHandler;
```
-#### storage
+Passing an SDK v1 server to `createMcpHandler` still works but emits a deprecation warning. Move the server to an SDK v2 factory and pass the factory to `createMcpHandler`. If sessionful behavior prevents an immediate migration, use `createLegacyMcpHandler` only on the temporary legacy lane.
-Persist transport state to survive Durable Object hibernation or restarts.
+`experimental_createMcpHandler` is also deprecated. Move its SDK v1 server to an SDK v2 factory. Use `createLegacyMcpHandler` only as a temporary bridge for sessionful behavior.
-```ts
-interface MCPStorageApi {
- get(): Promise | TransportState | undefined;
- set(state: TransportState): Promise | void;
-}
+### `CreateLegacyMcpHandlerOptions`
-interface TransportState {
- sessionId?: string;
- initialized: boolean;
- protocolVersion?: ProtocolVersion;
-}
-```
+`CreateLegacyMcpHandlerOptions` extends `WorkerTransportOptions` and adds these fields:
-
+| Option | Type | Default | Description |
+| ------------- | ----------------- | ----------------------- | ------------------------------------- |
+| `route` | `string` | `"/mcp"` | Exact path handled by the handler |
+| `authContext` | `McpAuthContext` | Execution context props | Application props for tool handlers |
+| `transport` | `WorkerTransport` | New transport | Persistent or preconfigured transport |
-```ts
-// Inside an Agent or Durable Object class method:
-const transport = new WorkerTransport({
- storage: {
- get: async () => {
- return await this.ctx.storage.get("mcp-state");
- },
- set: async (state) => {
- await this.ctx.storage.put("mcp-state", state);
- },
- },
-});
-```
+Common `WorkerTransportOptions` include:
-
+| Option | Description |
+| ----------------------------------------- | ---------------------------------------------------------- |
+| `sessionIdGenerator` | Creates protocol session IDs |
+| `enableJsonResponse` | Returns JSON instead of SSE where supported |
+| `storage` | Persists transport state through an `{ get, set }` adapter |
+| `eventStore` | Persists events for replay and stream recovery |
+| `corsOptions` | Adds CORS response and preflight headers |
+| `onsessioninitialized`, `onsessionclosed` | Observe session lifecycle changes |
+
+Create a fresh SDK v1 server for each request unless you provide a persistent transport already connected to that server. One server cannot reconnect to several transports.
+
+## Authentication context
-## Authentication Context
+A compatible `@cloudflare/workers-oauth-provider` supplies verified standard `AuthInfo` to SDK v2 callbacks at `context.http.authInfo`.
-When using [OAuth authentication](/agents/model-context-protocol/protocol/authorization/) with `createMcpHandler`, user information is made available to your MCP tools through `getMcpAuthContext()`. Under the hood this uses `AsyncLocalStorage` to pass the request to the tool handler, keeping the authentication context available.
+The existing `getMcpAuthContext()` helper continues to return application props:
```ts
interface McpAuthContext {
@@ -554,101 +353,57 @@ interface McpAuthContext {
}
```
-### getMcpAuthContext
-
-Retrieve the current authentication context within an MCP tool handler. This returns user information that was populated by the OAuth provider. Note that if using `McpAgent`, this information is accessible directly on `this.props` instead.
-
-```ts
-import { getMcpAuthContext } from "agents/mcp";
-
-function getMcpAuthContext(): McpAuthContext | undefined;
-```
-
```ts
-import { getMcpAuthContext } from "agents/mcp";
-import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
-
-function createServer() {
- const server = new McpServer({ name: "Auth Server", version: "1.0.0" });
+import { getMcpAuthContext } from "agents/mcp/server";
- server.tool("getProfile", "Get the current user's profile", {}, async () => {
+server.registerTool(
+ "whoami",
+ { description: "Return the current identity", inputSchema: {} },
+ async (_args, context) => {
const auth = getMcpAuthContext();
- const username = auth?.props?.username as string | undefined;
- const email = auth?.props?.email as string | undefined;
return {
content: [
{
type: "text",
- text: `User: ${username ?? "anonymous"}, Email: ${email ?? "none"}`,
+ text: JSON.stringify({
+ clientId: context.http?.authInfo?.clientId,
+ scopes: context.http?.authInfo?.scopes,
+ userId: auth?.props.userId,
+ }),
},
],
};
- });
-
- return server;
-}
+ },
+);
```
-:::note
-For a complete guide on setting up OAuth authentication with MCP servers, see the [MCP Authorization documentation](/agents/model-context-protocol/protocol/authorization/). View the [complete authenticated MCP server in a Worker example on GitHub](https://github.com/cloudflare/agents/tree/main/examples/mcp-worker-authenticated).
-:::
-
-## Error Handling
+Do not log or return `authInfo.token` or `authInfo.extra.props`.
-The `createMcpHandler` automatically catches errors and returns JSON-RPC error responses with code `-32603` (Internal error).
+## Migration
-
+Refer to [Migrate to MCP SDK v2](/agents/model-context-protocol/guides/migrate-to-mcp-sdk-v2/) before changing an existing server. The migration guide covers dual-era routing, stateful servers, client changes, and rollout checks.
-```ts
-server.tool("riskyOperation", "An operation that might fail", {}, async () => {
- if (Math.random() > 0.5) {
- throw new Error("Random failure occurred");
- }
- return {
- content: [{ type: "text", text: "Success!" }],
- };
-});
-
-// Errors are automatically caught and returned as:
-// {
-// "jsonrpc": "2.0",
-// "error": {
-// "code": -32603,
-// "message": "Random failure occurred"
-// },
-// "id":
-// }
-```
-
-
-
-## Related Resources
-
-
+## Related resources
diff --git a/src/content/docs/agents/model-context-protocol/guides/build-codemode-mcp-server.mdx b/src/content/docs/agents/model-context-protocol/guides/build-codemode-mcp-server.mdx
index fd5772e279c..72c604db238 100644
--- a/src/content/docs/agents/model-context-protocol/guides/build-codemode-mcp-server.mdx
+++ b/src/content/docs/agents/model-context-protocol/guides/build-codemode-mcp-server.mdx
@@ -30,6 +30,8 @@ Code Mode is experimental and may have breaking changes. Use caution in producti
You need a Cloudflare Workers project and an existing `McpServer`.
+`codeMcpServer()` currently returns an SDK v1 server. Serve it through the explicit legacy `createLegacyMcpHandler` API.
+
## Wrap the server
@@ -62,7 +64,7 @@ You need a Cloudflare Workers project and an existing `McpServer`.
import { DynamicWorkerExecutor } from "@cloudflare/codemode";
import { codeMcpServer } from "@cloudflare/codemode/mcp";
import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
- import { createMcpHandler } from "agents/mcp";
+ import { createLegacyMcpHandler } from "agents/mcp";
import { z } from "zod";
function createOrderServer() {
@@ -105,7 +107,7 @@ You need a Cloudflare Workers project and an existing `McpServer`.
executor,
});
- return createMcpHandler(server, { route: "/mcp" })(
+ return createLegacyMcpHandler(server, { route: "/mcp" })(
request,
env,
ctx,
diff --git a/src/content/docs/agents/model-context-protocol/guides/build-codemode-openapi-mcp-server.mdx b/src/content/docs/agents/model-context-protocol/guides/build-codemode-openapi-mcp-server.mdx
index da690a54fc2..e5fd57bf884 100644
--- a/src/content/docs/agents/model-context-protocol/guides/build-codemode-openapi-mcp-server.mdx
+++ b/src/content/docs/agents/model-context-protocol/guides/build-codemode-openapi-mcp-server.mdx
@@ -33,6 +33,8 @@ Code Mode is experimental and may have breaking changes. Use caution in producti
You need a Cloudflare Workers project, an OpenAPI 3.x document, and a host-side method for authenticating API requests.
+`openApiMcpServer()` currently returns an SDK v1 server. Serve it through the explicit legacy `createLegacyMcpHandler` API.
+
## Publish the service
@@ -64,7 +66,7 @@ You need a Cloudflare Workers project, an OpenAPI 3.x document, and a host-side
```ts
import { DynamicWorkerExecutor } from "@cloudflare/codemode";
import { openApiMcpServer } from "@cloudflare/codemode/mcp";
- import { createMcpHandler } from "agents/mcp";
+ import { createLegacyMcpHandler } from "agents/mcp";
const SPEC_URL = "https://api.example.com/openapi.json";
const API_ORIGIN = "https://api.example.com";
@@ -125,21 +127,19 @@ You need a Cloudflare Workers project, an OpenAPI 3.x document, and a host-side
: JSON.stringify(options.body),
});
+ if (!response.ok) {
+ throw new Error(`API request failed: ${response.status}`);
+ }
if (response.status === 204) return null;
const responseType = response.headers.get("Content-Type") ?? "";
- const result = responseType.includes("application/json")
+ return responseType.includes("application/json")
? await response.json()
: await response.text();
-
- if (!response.ok) {
- throw new Error(`API request failed: ${response.status}`);
- }
- return result;
},
});
- return createMcpHandler(server, { route: "/mcp" })(
+ return createLegacyMcpHandler(server, { route: "/mcp" })(
request,
env,
ctx,
diff --git a/src/content/docs/agents/model-context-protocol/guides/migrate-to-mcp-sdk-v2.mdx b/src/content/docs/agents/model-context-protocol/guides/migrate-to-mcp-sdk-v2.mdx
new file mode 100644
index 00000000000..f84746c7c4c
--- /dev/null
+++ b/src/content/docs/agents/model-context-protocol/guides/migrate-to-mcp-sdk-v2.mdx
@@ -0,0 +1,392 @@
+---
+title: Migrate to MCP SDK v2
+description: Migrate Agents SDK MCP servers and clients to the split MCP TypeScript SDK v2 packages while retaining legacy compatibility.
+pcx_content_type: how-to
+sidebar:
+ order: 13
+products:
+ - agents
+---
+
+import { PackageManagers, Steps, TypeScriptExample } from "~/components";
+
+This guide covers the [MCP SDK v2](https://github.com/modelcontextprotocol/typescript-sdk) 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 beta version exact.
+
+For a stateless server:
+
+
+
+For a temporary legacy lane:
+
+
+
+For an Agent that connects to MCP servers:
+
+
+
+Follow peer dependency instructions from your package manager. The exact v2 pin will change with later Agents releases while the MCP SDK remains in beta.
+
+## 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](#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](https://github.com/modelcontextprotocol/typescript-sdk/blob/main/docs/migration/upgrade-to-v2.md) 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.
+
+
+
+
+
+```ts
+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:
+
+```ts
+// 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:
+
+
+
+```ts
+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. |
+
+Do not remove the legacy route as soon as the stateless implementation exists. Serve both lanes while clients migrate and existing sessions drain.
+
+### Run stateless and legacy lanes together
+
+A single URL can route stateless requests to SDK v2 and legacy requests to the existing sessionful server.
+
+
+
+```ts
+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: Request, env: Env, ctx: ExecutionContext) {
+ if (await isLegacyRequest(request)) {
+ return legacy.fetch(request, env, ctx);
+ }
+ return stateless(request, env, ctx);
+ },
+} satisfies ExportedHandler;
+```
+
+
+
+Keep `legacy: "reject"` on the stateless handler. Otherwise, its legacy compatibility lane consumes requests before the sessionful route receives them.
+
+Deploy both routes before moving clients. Monitor the legacy lane and let existing sessions drain. Remove the legacy route and its protocol-only Durable Object binding only after no clients depend on them. Handle Durable Object migration configuration as a separate deployment step.
+
+## Update MCP clients
+
+Agents now uses `@modelcontextprotocol/client` internally. Existing `addMcpServer` calls negotiate the protocol era automatically.
+
+Servers on the stateless path use `server/discover`. Agents falls back to `initialize` for legacy Streamable HTTP, SSE, and RPC servers.
+
+The client API includes these changes:
+
+- `callTool(params, options)` is the preferred signature.
+- `callTool(params, resultSchema, options)` remains available but is deprecated.
+- MCP client types now come from `@modelcontextprotocol/client`.
+- Required stateless HTTP headers are handled by the SDK.
+- List changes use stateless subscriptions or legacy notifications based on the negotiated lane.
+
+### Configure elicitation for stateless requests
+
+Tools, prompts, and resources on the stateless path can return `input_required` through multi-round-trip requests (MRTR). The SDK calls the configured elicitation handler and retries the original operation. Your original `callTool`, `getPrompt`, or `readResource` promise remains pending.
+
+Each retry contains responses for the immediately preceding input round, not every earlier response. The client also echoes the latest opaque `requestState`. Seal trusted intermediate values needed by later rounds into integrity-protected `requestState`; do not expect `inputResponses` to accumulate across rounds.
+
+Refer to the [stateless elicitation example](https://github.com/cloudflare/agents/tree/main/examples/mcp-elicitation-mrtr) for a two-round tool flow.
+
+
+
+```ts
+export class MyAgent extends Agent {
+ onStart() {
+ this.mcp.configureElicitationHandlers({
+ form: async (request, serverId, signal) => {
+ return collectInput(request, serverId, signal);
+ },
+ url: async (request, serverId, signal) => {
+ return openExternalFlow(request, serverId, signal);
+ },
+ });
+ }
+}
+```
+
+
+
+Handlers and in-flight calls remain in memory. Hibernation, isolate restart, transport loss, or connection reconstruction rejects an active interactive call. Retry the operation after the connection recovers.
+
+Treat manually handled `requestState` as untrusted input. Bind it to the authenticated user and operation, protect its integrity, and set a short expiry.
+
+### Update custom OAuth providers
+
+A custom `AgentMcpOAuthProvider` must implement the v2 `OAuthClientProvider` contract:
+
+- Import OAuth types from `@modelcontextprotocol/client`.
+- Store `StoredOAuthClientInformation` and `StoredOAuthTokens`.
+- Preserve the SDK issuer stamp on credentials.
+- Persist `OAuthDiscoveryState` across browser redirects.
+- Accept `"discovery"` in `invalidateCredentials`.
+- Keep credentials separate when authorization issuers differ.
+
+SDK v2 validates OAuth metadata issuers by default. A trusted legacy server with known mismatched metadata can use `skipIssuerMetadataValidation: true`. This weakens OAuth mix-up protection and should not be a general fallback.
+
+## Review protocol differences
+
+MCP's stateless model changes the transport and lifecycle:
+
+| Area | Previous behavior | New behavior |
+| --------------------- | --------------------------------------------------- | ------------------------------------------------------------------------ |
+| Startup | `initialize` handshake | No handshake. `server/discover` is optional for clients |
+| Request metadata | Connection-scoped negotiation | Version, client capabilities, and identity metadata on each request |
+| Sessions | Optional `Mcp-Session-Id` | No protocol session |
+| Server input requests | Server sends JSON-RPC requests | Server returns `input_required`. Client retries the original operation |
+| Change notifications | Standalone GET stream and list-change notifications | `subscriptions/listen` POST with an SSE response |
+| Stream recovery | `Last-Event-ID` can resume configured streams | Listen streams reopen after failure. There is no `Last-Event-ID` replay. |
+
+Custom transports, proxies, and gateways must preserve the draft request headers:
+
+- `MCP-Protocol-Version`
+- `Mcp-Method`
+- `Mcp-Name` for tool, prompt, and resource operations
+- Declared `Mcp-Param-*` tool headers
+
+The exact beta used by Agents is a draft snapshot. Beta.5 makes `clientInfo` optional and places server identity in result `_meta`. Use high-level SDK APIs and update MCP packages with the Agents release that supports each snapshot. Raw stateless results must include `resultType`.
+
+The draft deprecates Roots, Sampling, Logging, the old HTTP+SSE transport, and Dynamic Client Registration. The types remain available during the deprecation window for legacy compatibility. The published experimental task methods become the `io.modelcontextprotocol/tasks` extension. Agents SDK v0.20.0 does not add that extension.
+
+### Integration compatibility
+
+The following integrations retain SDK v1 server output in this release:
+
+- Current Code Mode `codeMcpServer` and `openApiMcpServer` helpers
+- The server-side `withX402` helper
+- Existing OpenAI Apps examples that import an SDK v1 `McpServer`
+
+Until these integrations produce SDK v2 servers, isolate their output behind a temporary `createLegacyMcpHandler` route. The Code Mode MCP connector and `withX402Client` accept either client generation.
+
+## Plan the rollout
+
+
+
+1. Classify each endpoint by its sessionful dependencies.
+
+2. Pin the MCP SDK versions required by the Agents release.
+
+3. Move each server definition that can be stateless to an SDK v2 factory.
+
+4. Add stateless handlers beside endpoints that still need a temporary legacy lane.
+
+5. Route stateless and legacy requests with `isLegacyRequest()`.
+
+6. Test stateless and legacy clients independently.
+
+7. Test required HTTP headers through every proxy and gateway.
+
+8. Test OAuth after a clean login and after Durable Object hibernation.
+
+9. Test cancellation, multiple input rounds, and transport loss.
+
+10. Verify valid Origins and reject invalid Origins with `403`.
+
+11. Remove legacy routes only after existing sessions drain.
+
+
+
+Stored HTTP session IDs from Agents releases before v0.20.0 do not include the negotiated protocol version. The upgraded client discards those IDs and reconnects instead of sending an unsafe resumed request. Existing in-flight work tied to an old remote session does not resume.
+
+For API details, refer to [`createMcpHandler`](/agents/model-context-protocol/apis/handler-api/) and [`McpClient`](/agents/model-context-protocol/apis/client-api/).
diff --git a/src/content/docs/agents/model-context-protocol/guides/remote-mcp-server.mdx b/src/content/docs/agents/model-context-protocol/guides/remote-mcp-server.mdx
index 1e78e89fc21..f18148b2611 100644
--- a/src/content/docs/agents/model-context-protocol/guides/remote-mcp-server.mdx
+++ b/src/content/docs/agents/model-context-protocol/guides/remote-mcp-server.mdx
@@ -12,7 +12,7 @@ products:
import { Details, Render, PackageManagers, LinkCard } from "~/components";
-This guide will show you how to deploy your own remote MCP server on Cloudflare using [Streamable HTTP transport](/agents/model-context-protocol/protocol/transport/), the current MCP specification standard. You have two options:
+This guide shows how to deploy a remote MCP server on Cloudflare using [Streamable HTTP transport](/agents/model-context-protocol/protocol/transport/). You have two options:
- **Without authentication** — anyone can connect and use the server (no login required).
- **With [authentication and authorization](/agents/model-context-protocol/guides/remote-mcp-server/#add-authentication)** — users sign in before accessing tools, and you can control which tools an agent can call based on the user's permissions.
@@ -21,18 +21,21 @@ This guide will show you how to deploy your own remote MCP server on Cloudflare
The Agents SDK provides multiple ways to create MCP servers. Choose the approach that fits your use case:
-| Approach | Stateful? | Requires Durable Objects? | Best for |
-| -------------------------------------------------------------- | --------- | ------------------------- | ---------------------------------------------- |
-| [`createMcpHandler()`](/agents/model-context-protocol/apis/handler-api/) | No | No | Stateless tools, simplest setup |
-| [`McpAgent`](/agents/model-context-protocol/apis/agent-api/) | Yes | Yes | Stateful tools, per-session state, elicitation |
-| Raw `WebStandardStreamableHTTPServerTransport` | No | No | Full control, no SDK dependency |
+| Approach | Stateful? | Protocol path | Best for |
+| ----------------------------------------------------------------------------------------------------- | -------------------- | ----------------------------------- | ------------------------------------------- |
+| [`createMcpHandler()`](/agents/model-context-protocol/apis/handler-api/) | No | stateless with legacy compatibility | New stateless tools |
+| [`createLegacyMcpHandler()`](/agents/model-context-protocol/apis/handler-api/#createlegacymcphandler) | Optional | legacy | Temporary existing `WorkerTransport` routes |
+| [`McpAgent`](/agents/model-context-protocol/apis/agent-api/) | Yes | legacy | Deprecated Durable Object and RPC servers |
+| Raw SDK transport | Depends on transport | Depends on SDK package | Custom transport ownership |
-- **`createMcpHandler()`** is the fastest way to get a stateless MCP server running. Use it when your tools do not need per-session state.
-- **`McpAgent`** gives you a Durable Object per session with built-in state management, elicitation support, and both SSE and Streamable HTTP transports.
-- **Raw transport** gives you full control if you want to use the `@modelcontextprotocol/sdk` directly without the Agents SDK helpers.
+Use `createMcpHandler` for a new stateless server. An existing `McpAgent` without legacy stateful dependencies can migrate directly. If it uses MCP session state, RPC, pushed requests, streams, or replay, plan the stateless equivalents and serve stateless and legacy lanes during the transition. Refer to [Migrate to MCP SDK v2](/agents/model-context-protocol/guides/migrate-to-mcp-sdk-v2/) for the staged rollout.
## Deploy your first MCP server
+:::note[Template protocol path]
+The quick-deploy templates in this section still use the deprecated `McpAgent` path. Do not use that path for a new server. Start with the [`mcp-worker` example](https://github.com/cloudflare/agents/tree/main/examples/mcp-worker). If an existing template deployment needs sessionful behavior, add a stateless route and follow the [migration guide](/agents/model-context-protocol/guides/migrate-to-mcp-sdk-v2/).
+:::
+
You can start by deploying a [public MCP server](https://github.com/cloudflare/ai/tree/main/demos/remote-mcp-authless) without authentication, then add user authentication and scoped authorization later. If you already know your server will require authentication, you can skip ahead to the [next section](/agents/model-context-protocol/guides/remote-mcp-server/#add-authentication).
### Via the dashboard
diff --git a/src/content/docs/agents/model-context-protocol/protocol/authorization.mdx b/src/content/docs/agents/model-context-protocol/protocol/authorization.mdx
index 586c15644f1..34023d9cda3 100644
--- a/src/content/docs/agents/model-context-protocol/protocol/authorization.mdx
+++ b/src/content/docs/agents/model-context-protocol/protocol/authorization.mdx
@@ -178,9 +178,9 @@ Remember — [authentication is different from authorization](https://www.cloud
When a user authenticates through the OAuth Provider, their identity information is available inside your tools. How you access it depends on whether you use `McpAgent` or `createMcpHandler`.
-### With McpAgent
+### With `McpAgent` during migration
-The third type parameter on `McpAgent` defines the shape of the authentication context. Access it via `this.props` inside `init()` and tool handlers.
+This pattern applies only to existing deprecated `McpAgent` routes. The third type parameter defines the authentication context shape. Access it through `this.props` inside `init()` and tool handlers.
```ts
import { McpAgent } from "agents/mcp";
@@ -202,29 +202,46 @@ export class MyMCP extends McpAgent {
}
```
-### With createMcpHandler
+### With stateless `createMcpHandler`
-Use `getMcpAuthContext()` to access the same information from within a tool handler. This uses `AsyncLocalStorage` under the hood.
+A compatible Workers OAuth Provider supplies standard token metadata at `context.http.authInfo`. Use `getMcpAuthContext()` for existing application props.
```ts
-import { createMcpHandler, getMcpAuthContext } from "agents/mcp";
-import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
+import { createMcpHandler, getMcpAuthContext } from "agents/mcp/server";
+import { McpServer } from "@modelcontextprotocol/server";
function createServer() {
const server = new McpServer({ name: "Auth Demo", version: "1.0.0" });
- server.tool("whoami", "Get the current user", {}, async () => {
- const auth = getMcpAuthContext();
- const name = (auth?.props?.name as string) ?? "anonymous";
- return {
- content: [{ type: "text", text: `Hello, ${name}!` }],
- };
- });
+ server.registerTool(
+ "whoami",
+ { description: "Get the current user", inputSchema: {} },
+ async (_args, context) => {
+ const auth = getMcpAuthContext();
+ const name = (auth?.props.name as string) ?? "anonymous";
+ return {
+ content: [
+ {
+ type: "text",
+ text: `${name}: ${context.http?.authInfo?.clientId}`,
+ },
+ ],
+ };
+ },
+ );
return server;
}
+
+export default {
+ fetch(request, env, ctx) {
+ return createMcpHandler(createServer)(request, env, ctx);
+ },
+} satisfies ExportedHandler;
```
+Do not log or return the raw access token.
+
## Permission-based tool access
You can control which tools are available based on user permissions. There are two approaches: check permissions inside the tool handler, or conditionally register tools.
diff --git a/src/content/docs/agents/model-context-protocol/protocol/tools.mdx b/src/content/docs/agents/model-context-protocol/protocol/tools.mdx
index 507c18060cb..3d6942b7a97 100644
--- a/src/content/docs/agents/model-context-protocol/protocol/tools.mdx
+++ b/src/content/docs/agents/model-context-protocol/protocol/tools.mdx
@@ -12,9 +12,9 @@ products:
import { TypeScriptExample, LinkCard } from "~/components";
-MCP tools are functions that an [MCP server](/agents/model-context-protocol/) exposes for clients to call. When an LLM decides it needs to take an action — look up data, run a calculation, call an API — it invokes a tool. The MCP server executes the tool and returns the result.
+MCP tools are functions that an [MCP server](/agents/model-context-protocol/) exposes for clients to call. An LLM can invoke a tool to look up data, run a calculation, or call an API. The MCP server executes the tool and returns its result.
-Tools are defined using the `@modelcontextprotocol/sdk` package. The Agents SDK handles transport and lifecycle; the tool definitions are the same regardless of whether you use [`createMcpHandler`](/agents/model-context-protocol/apis/handler-api/) or [`McpAgent`](/agents/model-context-protocol/apis/agent-api/).
+Use `@modelcontextprotocol/server` for a stateless `createMcpHandler` server. `McpAgent` is deprecated and feature-frozen. Existing `McpAgent` routes must keep using `@modelcontextprotocol/sdk` only while they migrate.
:::note[Experimental WebMCP adapter]
@@ -30,21 +30,23 @@ The Agents SDK also includes the experimental `agents/experimental/webmcp` adapt
## Defining tools
-Use `server.tool()` to register a tool on an `McpServer` instance. Each tool has a name, a description (used by the LLM to decide when to call it), an input schema defined with [Zod](https://zod.dev), and a handler function.
+Use `server.registerTool()` to register a tool on a stateless `McpServer` instance. Each tool has a name, a description, an input schema defined with [Zod](https://zod.dev), and a handler function.
```ts
-import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
+import { McpServer } from "@modelcontextprotocol/server";
import { z } from "zod";
function createServer() {
const server = new McpServer({ name: "Math", version: "1.0.0" });
- server.tool(
+ server.registerTool(
"add",
- "Add two numbers together",
- { a: z.number(), b: z.number() },
+ {
+ description: "Add two numbers together",
+ inputSchema: { a: z.number(), b: z.number() },
+ },
async ({ a, b }) => ({
content: [{ type: "text", text: String(a + b) }],
}),
@@ -65,10 +67,12 @@ Tool results are returned as an array of content parts. The most common type is
```ts
-server.tool(
+server.registerTool(
"lookup",
- "Look up a user by ID",
- { userId: z.string() },
+ {
+ description: "Look up a user by ID",
+ inputSchema: { userId: z.string() },
+ },
async ({ userId }) => {
const user = await db.getUser(userId);
@@ -105,21 +109,23 @@ Tool inputs are defined as Zod schemas and validated automatically before the ha
```ts
-server.tool(
+server.registerTool(
"search",
- "Search for documents by query",
{
- query: z.string().describe("The search query"),
- limit: z
- .number()
- .min(1)
- .max(100)
- .default(10)
- .describe("Maximum number of results to return"),
- category: z
- .enum(["docs", "blog", "api"])
- .optional()
- .describe("Filter by content category"),
+ description: "Search for documents by query",
+ inputSchema: {
+ query: z.string().describe("The search query"),
+ limit: z
+ .number()
+ .min(1)
+ .max(100)
+ .default(10)
+ .describe("Maximum number of results to return"),
+ category: z
+ .enum(["docs", "blog", "api"])
+ .optional()
+ .describe("Filter by content category"),
+ },
},
async ({ query, limit, category }) => {
const results = await searchIndex(query, { limit, category });
@@ -139,33 +145,36 @@ For stateless MCP servers, define tools inside a factory function and pass the s
```ts
-import { createMcpHandler } from "agents/mcp";
-import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
+import { createMcpHandler } from "agents/mcp/server";
+import { McpServer } from "@modelcontextprotocol/server";
import { z } from "zod";
function createServer() {
const server = new McpServer({ name: "My Tools", version: "1.0.0" });
- server.tool("ping", "Check if the server is alive", {}, async () => ({
- content: [{ type: "text", text: "pong" }],
- }));
+ server.registerTool(
+ "ping",
+ { description: "Check if the server is alive", inputSchema: {} },
+ async () => ({
+ content: [{ type: "text", text: "pong" }],
+ }),
+ );
return server;
}
export default {
- fetch: (request: Request, env: Env, ctx: ExecutionContext) => {
- const server = createServer();
- return createMcpHandler(server)(request, env, ctx);
+ fetch(request, env, ctx) {
+ return createMcpHandler(createServer)(request, env, ctx);
},
-} satisfies ExportedHandler;
+} satisfies ExportedHandler;
```
## Using tools with `McpAgent`
-For stateful MCP servers, define tools in the `init()` method of an [`McpAgent`](/agents/model-context-protocol/apis/agent-api/). Tools have access to the agent instance via `this`, which means they can read and write state.
+This section applies only to existing legacy routes during migration. Define their tools in the `init()` method of an [`McpAgent`](/agents/model-context-protocol/apis/agent-api/). Tools have access to the agent instance through `this`, so they can read and write state.
diff --git a/src/content/docs/agents/model-context-protocol/protocol/transport.mdx b/src/content/docs/agents/model-context-protocol/protocol/transport.mdx
index c657f14078e..05ea1fc93a0 100644
--- a/src/content/docs/agents/model-context-protocol/protocol/transport.mdx
+++ b/src/content/docs/agents/model-context-protocol/protocol/transport.mdx
@@ -18,7 +18,7 @@ The Model Context Protocol (MCP) specification defines two standard [transport m
2. **Streamable HTTP** — The standard transport method for remote MCP connections, [introduced](https://modelcontextprotocol.io/specification/2025-03-26/basic/transports#streamable-http) in March 2025. It uses a single HTTP endpoint for bidirectional messaging.
:::note
-Server-Sent Events (SSE) was previously used for remote MCP connections but has been deprecated in favor of Streamable HTTP. If you need SSE support for legacy clients, use the [`McpAgent`](/agents/model-context-protocol/apis/agent-api/) class.
+Server-Sent Events (SSE) was previously used for remote MCP connections but has been deprecated in favor of Streamable HTTP. Existing `McpAgent` deployments can retain SSE temporarily while they migrate, but new servers should use the stateless Streamable HTTP handler.
:::
MCP servers built with the [Agents SDK](/agents) use [`createMcpHandler`](/agents/model-context-protocol/apis/handler-api/) to handle Streamable HTTP transport.
@@ -40,8 +40,8 @@ Create an MCP server using `createMcpHandler`. View the [complete example on Git
```ts
-import { createMcpHandler } from "agents/mcp";
-import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
+import { createMcpHandler } from "agents/mcp/server";
+import { McpServer } from "@modelcontextprotocol/server";
import { z } from "zod";
function createServer() {
@@ -67,12 +67,10 @@ function createServer() {
}
export default {
- fetch: (request: Request, env: Env, ctx: ExecutionContext) => {
- // Create a new server instance per request
- const server = createServer();
- return createMcpHandler(server)(request, env, ctx);
+ fetch(request, env, ctx) {
+ return createMcpHandler(createServer)(request, env, ctx);
},
-} satisfies ExportedHandler;
+} satisfies ExportedHandler;
```
@@ -86,26 +84,20 @@ If your MCP server implements authentication & authorization using the [Workers
```ts
export default new OAuthProvider({
apiRoute: "/mcp",
- apiHandler: {
- fetch: (request: Request, env: Env, ctx: ExecutionContext) => {
- // Create a new server instance per request
- const server = createServer();
- return createMcpHandler(server)(request, env, ctx);
- },
- },
+ apiHandler: createMcpHandler(createServer),
// ... other OAuth configuration
});
```
-### Stateful MCP servers
+### Servers that need protocol sessions
-If your MCP server needs to maintain state across requests, use `createMcpHandler` with a `WorkerTransport` inside an [Agent](/agents/) class. This allows you to persist session state in Durable Object storage and use advanced MCP features like [elicitation](https://modelcontextprotocol.io/specification/draft/client/elicitation) and [sampling](https://modelcontextprotocol.io/specification/draft/client/sampling).
+MCP has no protocol-level session on the stateless path. Applications can store durable business data behind a separate storage boundary.
-See [Stateful MCP Servers](/agents/model-context-protocol/apis/handler-api/#stateful-mcp-servers) for implementation details.
+While migrating legacy sessions, existing servers can keep a temporary `createLegacyMcpHandler` with `WorkerTransport` or `McpAgent` route beside the new stateless route. These APIs support transport state, event replay, pushed elicitation, sampling, and roots requests. `McpAgent` is deprecated and feature-frozen.
-Streamable HTTP streams are resumable: configure an `EventStore` so clients can reconnect with a `Last-Event-ID` header and replay missed events, keeping in-flight tool calls alive across the edge idle-stream watchdog. `DurableObjectEventStore` is exported from `agents/mcp` for stateful `WorkerTransport` callers. Refer to [`McpAgent`: Stream resumability](/agents/model-context-protocol/apis/agent-api/#stream-resumability).
+Add the stateless route before moving clients, and keep both lanes until existing sessions drain. Refer to [Migrate to MCP SDK v2](/agents/model-context-protocol/guides/migrate-to-mcp-sdk-v2/) for the staged migration. Refer to [`McpAgent`: Stream resumability](/agents/model-context-protocol/apis/agent-api/#stream-resumability) for existing stream behavior.
## RPC transport
@@ -117,7 +109,13 @@ The **RPC transport** is designed for internal applications where your MCP serve
RPC transport does not support authentication. Use Streamable HTTP for external connections that require OAuth.
-### Connecting an Agent to an McpAgent via RPC
+### Connect an Agent to an existing `McpAgent` through RPC
+
+:::caution[Deprecated server path]
+
+This section applies only to existing `McpAgent` deployments during migration. Do not create a new `McpAgent` server. Use a stateless `createMcpHandler` server instead.
+
+:::
#### 1. Define your MCP server
@@ -203,15 +201,15 @@ In your `wrangler.jsonc`, define bindings for both Durable Objects:
"durable_objects": {
"bindings": [
{ "name": "Chat", "class_name": "Chat" },
- { "name": "MyMCP", "class_name": "MyMCP" }
- ]
+ { "name": "MyMCP", "class_name": "MyMCP" },
+ ],
},
"migrations": [
{
"new_sqlite_classes": ["MyMCP", "Chat"],
- "tag": "v1"
- }
- ]
+ "tag": "v1",
+ },
+ ],
}
```
@@ -273,9 +271,7 @@ export class MyMCP extends McpAgent<
const role = this.props?.role || "guest";
return {
- content: [
- { type: "text", text: `User ID: ${userId}, Role: ${role}` },
- ],
+ content: [{ type: "text", text: `User ID: ${userId}, Role: ${role}` }],
};
});
}
@@ -320,19 +316,19 @@ export class MyMCP extends McpAgent {
## Choosing a transport
-| Transport | Use when | Pros | Cons |
-| ------------------- | ------------------------------------- | ---------------------------------------- | ------------------------------- |
-| **Streamable HTTP** | External MCP servers, production apps | Standard protocol, secure, supports auth | Slight network overhead |
+| Transport | Use when | Pros | Cons |
+| ------------------- | ------------------------------------- | ---------------------------------------- | ------------------------------------- |
+| **Streamable HTTP** | External MCP servers, production apps | Standard protocol, secure, supports auth | Slight network overhead |
| **RPC** | Internal agents on Cloudflare | Fastest, simplest setup | No auth, Durable Object bindings only |
-| **SSE** | Legacy compatibility | Backwards compatible | Deprecated, use Streamable HTTP |
+| **SSE** | Compatibility with older clients | Backwards compatible | Deprecated, use Streamable HTTP |
+
+### Migrate from McpAgent
-### Migrating from McpAgent
+If the endpoint does not use legacy stateful features, migrate directly to a stateless server factory from `@modelcontextprotocol/server` and pass it to `createMcpHandler`.
-If you have an existing MCP server using the `McpAgent` class:
+If it depends on MCP session state, RPC, pushed server-to-client requests, standalone streams, or replay, first design stateless equivalents. For example, move business state behind explicit application storage and replace pushed input requests with stateless elicitation. Serve stateless and legacy lanes together while clients migrate and existing sessions drain.
-- **Not using state?** Replace your `McpAgent` class with `McpServer` from `@modelcontextprotocol/sdk` and use `createMcpHandler(server)` in a Worker `fetch` handler.
-- **Using state?** Use `createMcpHandler` with a `WorkerTransport` inside an [Agent](/agents/) class. See [Stateful MCP Servers](/agents/model-context-protocol/apis/handler-api/#stateful-mcp-servers) for details.
-- **Need SSE support?** Continue using `McpAgent` with `serveSSE()` for legacy client compatibility. See the [McpAgent API reference](/agents/model-context-protocol/apis/agent-api/).
+Refer to [Migrate to MCP SDK v2](/agents/model-context-protocol/guides/migrate-to-mcp-sdk-v2/) for the feature mapping, dual-era routing, and rollout steps.
### Testing with MCP clients