diff --git a/src/content/changelog/agents/2026-07-20-agents-sdk-v0.18.0-mcp-sdk-v2.mdx b/src/content/changelog/agents/2026-07-20-agents-sdk-v0.18.0-mcp-sdk-v2.mdx new file mode 100644 index 00000000000..0c7a85cea30 --- /dev/null +++ b/src/content/changelog/agents/2026-07-20-agents-sdk-v0.18.0-mcp-sdk-v2.mdx @@ -0,0 +1,72 @@ +--- +title: "Agents SDK v0.18.0: Stateless MCP SDK v2 support" +description: "Agents SDK v0.18.0 adds Stateless MCP SDK v2 clients and servers while retaining explicit Legacy support." +products: + - agents + - workers +date: 2026-07-20 +--- + +import { PackageManagers, TypeScriptExample } from "~/components"; + +Agents SDK v0.18.0 adds support for the split MCP TypeScript SDK v2 packages. Agents can serve Stateless MCP, fall back to Legacy servers, and handle Stateless Elicitation. + +Existing Legacy server deployments remain supported through `createLegacyMcpHandler`, `WorkerTransport`, and `McpAgent`. + +## 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 createMcpHandler(createServer); +``` + + + +The isolated `agents/mcp/server` entry keeps `McpAgent`, `WorkerTransport`, MCP client transports, and SDK v1 modules out of Stateless server bundles. Its Legacy compatibility lane supports ordinary tools, resources, and prompts. Session streams, replay, deletion, and pushed server-to-client requests still require a Legacy sessionful server. + +The Workers wrapper validates present browser Origins, supports explicit delegation to trusted Origin middleware, and exposes the upstream handler's `close`, `notify`, and `bus` controls. + +## Explicit Legacy support + +Existing SDK v1 servers can rename `createMcpHandler` to `createLegacyMcpHandler` without changing their transport behavior: + + + +```ts +import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js"; +import { createLegacyMcpHandler } from "agents/mcp"; + +export default createLegacyMcpHandler( + new McpServer({ name: "legacy", version: "1.0.0" }), +); +``` + + + +Passing an SDK v1 server directly to `createMcpHandler` is deprecated. `McpAgent` remains available for stateful Legacy deployments, but it is deprecated and feature-frozen. + +## Client negotiation and input requests + +The MCP client manager now uses `@modelcontextprotocol/client`. It probes Stateless servers with `server/discover` and falls back to the Legacy `initialize` handshake. + +Stateless Elicitation uses `input_required` through multi-round-trip requests (MRTR). Legacy Elicitation 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. + +### Upgrade + +To update the Agents SDK: + + + +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. 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..00f5fa61886 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 @@ -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 for Legacy servers that use stateful features. It is deprecated and feature-frozen. A server without those dependencies can migrate directly to [`createMcpHandler`](/agents/model-context-protocol/apis/handler-api/). + +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 +## Legacy 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 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. Legacy Elicitation 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 0d3b37f92e5..454e8d5447f 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.18.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 @@ -390,15 +392,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. Stateless Elicitation returns `input_required` and completes through multi-round-trip requests (MRTR). Legacy Elicitation 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() { @@ -429,7 +431,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. Legacy connections advertise them during `initialize`. Stateless requests 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. @@ -507,7 +509,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; @@ -567,9 +569,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 [Stateless Elicitation](/agents/model-context-protocol/apis/handler-api/#stateless-elicitation) and [Legacy Elicitation](/agents/model-context-protocol/apis/agent-api/#legacy-elicitation). ## Managing servers @@ -683,7 +685,7 @@ async addMcpServer( callbackHost?: string; callbackPath?: string; agentsPrefix?: string; - client?: ClientOptions; + client?: McpClientOptions; transport?: { headers?: HeadersInit; type?: "sse" | "streamable-http" | "auto"; @@ -702,7 +704,7 @@ async addMcpServer( options?: { id?: string; props?: Record; - client?: ClientOptions; + client?: McpClientOptions; retry?: RetryOptions; } ): Promise<{ id: string; state: "ready" }> @@ -717,7 +719,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"` @@ -867,7 +869,7 @@ 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?: { @@ -896,7 +898,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. @@ -908,7 +910,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..2f5a27b5efd 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,273 @@ 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` remains available for Legacy servers while Stateless equivalents are built. It is deprecated and feature-frozen. Refer to the [migration guide](/agents/model-context-protocol/guides/migrate-to-mcp-sdk-v2/) for staged rollout guidance. -- **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 stateless Worker handler from an MCP SDK v2 server factory. ```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); - }, -} satisfies ExportedHandler; +export default createMcpHandler(createServer); ``` -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. +Pass the factory itself. Do not create one global server instance or pass a constructed SDK v2 server directly. -## Stateful MCP Servers +### `CreateMcpHandlerOptions` -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. +The following options are available: -Provide a custom `WorkerTransport` with persistent storage. View the [complete example on GitHub](https://github.com/cloudflare/agents/tree/main/examples/mcp-elicitation). +| 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 | +| `bus` | `ServerEventBus` | In-memory bus | Event bus for Stateless subscriptions | +| `maxSubscriptions` | `number` | `1,024` | Maximum concurrent listen streams | +| `keepAliveMs` | `number` | `15,000` | Keepalive interval for listen streams | - +SDK v1 transport options do not apply to this handler. It rejects options such as `transport`, `storage`, `sessionIdGenerator`, `eventStore`, and `enableJsonResponse`. -```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"; +Use `responseMode: "json"` instead of `enableJsonResponse: true`. JSON mode drops notifications emitted before a final result. -const STATE_KEY = "mcp-transport-state"; +### Factory lifecycle -type State = { counter: number }; +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. -export class MyStatefulMcpAgent extends Agent { - server = new McpServer({ - name: "Stateful MCP Server", - version: "1.0.0", - }); +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. - 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); - }, - }, - }); +### Stateless Elicitation - async onRequest(request: Request) { - return createMcpHandler(this.server, { - transport: this.transport, - })(request, this.env, this.ctx as unknown as ExecutionContext); - } -} -``` +Stateless Elicitation returns `input_required` and completes through multi-round-trip requests (MRTR). The SDK carries `requestState` and `inputResponses` between requests. The Worker does not remain suspended while a user responds. - +Use `inputRequired(...)` to request input. Read accepted form content from `context.mcpReq.inputResponses` with `acceptedContent(...)`. -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: +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 [Legacy Elicitation](/agents/model-context-protocol/apis/agent-api/#legacy-elicitation). - +### Origin validation and CORS -```ts -import { getAgentByName } from "agents"; +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. -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(); +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. + +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: - // Get the Agent instance by name/session ID - const agent = await getAgentByName(env.MyStatefulMcpAgent, sessionId); + - // Route the MCP request to the agent - return await agent.onRequest(request); +```ts +export default createMcpHandler(createServer, { + allowedHostnames: ["mcp.example.com"], + corsOptions: { + origin: "https://app.example.com", }, -} satisfies ExportedHandler; +}); ``` -With persistent storage, the transport preserves: +Allowlist values are hostnames without a scheme or port. Origin matching ignores scheme and port. -- Session ID across reconnections -- Protocol version negotiation state -- Initialization status +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. -This allows MCP clients to reconnect and resume their session in the event of a connection loss. +CORS response headers are not authentication. Protect the MCP endpoint with OAuth or another authentication layer. -## Migration Guide for MCP SDK 1.26.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. -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. +### Legacy compatibility -### Who is affected? +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`. -| 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 | +Legacy compatibility does not provide a complete session transport: -### Why is this necessary? +- 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. -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. +Set `legacy: "reject"` for a Stateless-only endpoint. Use `createLegacyMcpHandler` or `McpAgent` when a Legacy client needs protocol sessions. -### Before (broken with SDK 1.26.0) +### Return value - +The returned handler is callable as a Worker fetch handler: ```ts -import { createMcpHandler } from "agents/mcp"; -import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js"; +const handler = createMcpHandler(createServer); -// INCORRECT: Global server instance -const server = new McpServer({ - name: "Hello MCP Server", - version: "1.0.0", -}); +const response = await handler(request, env, ctx); +``` -server.tool("hello", "Returns a greeting", {}, async () => { - return { - content: [{ text: "Hello, World!", type: "text" }], - }; -}); +It also exposes the upstream handler controls: -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); - }, -} satisfies ExportedHandler; +```ts +handler.fetch(request, { authInfo }); +handler.notify.toolsChanged(); +handler.bus.publish(event); +await handler.close(); ``` - +`close()` rejects new requests and closes active Stateless and Legacy compatibility work. -### After (correct) +## `createLegacyMcpHandler` - +`createLegacyMcpHandler` serves an SDK v1 server through `WorkerTransport`. ```ts -import { createMcpHandler } from "agents/mcp"; -import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js"; - -// CORRECT: Factory function to create server instance -function createServer() { - 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" }], - }; - }); - - return server; -} +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"; -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; +function createLegacyMcpHandler( + server: McpServer | Server, + options?: CreateLegacyMcpHandlerOptions, +): LegacyMcpHandler; ``` - - -### For raw SDK transport users - -If you are using the raw SDK transport directly (not via `createMcpHandler`), you must also create new transport instances per request: +Use this handler for Legacy sessions, transport storage, event replay, and pushed server-to-client requests. -```ts +```ts title="src/index.ts" import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js"; -import { WebStandardStreamableHTTPServerTransport } from "@modelcontextprotocol/sdk/server/webStandardStreamableHttp.js"; +import { createLegacyMcpHandler } from "agents/mcp"; function createServer() { - const server = new McpServer({ - name: "Hello MCP Server", - version: "1.0.0", - }); - - // Register tools... - - return server; + return new McpServer({ name: "legacy-server", version: "1.0.0" }); } 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); + async fetch(request: Request, env: Env, ctx: ExecutionContext) { + return createLegacyMcpHandler(createServer())(request, env, ctx); }, } satisfies ExportedHandler; ``` -### WorkerTransport - -The `WorkerTransport` class implements the MCP Transport interface, handling HTTP request/response cycles, Server-Sent Events (SSE) streaming, session management, and CORS. - -```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; -} -``` - -#### sessionIdGenerator - -Provides a custom session identifier. This session identifier is used to identify the session in the MCP Client. - - - -```ts -const transport = new WorkerTransport({ - sessionIdGenerator: () => `user-${Date.now()}-${Math.random()}`, -}); -``` - - - -#### enableJsonResponse - -Disables SSE streaming and returns responses as standard JSON. - - - -```ts -const transport = new WorkerTransport({ - enableJsonResponse: true, // Disable streaming, return JSON responses -}); -``` - - - -#### onsessioninitialized - -A callback that fires when a session is initialized, either by creating a new session or restoring from storage. - - - -```ts -const transport = new WorkerTransport({ - onsessioninitialized: (sessionId) => { - console.log(`MCP session initialized: ${sessionId}`); - }, -}); -``` - - - -#### corsOptions - -Configure CORS headers for cross-origin requests. - -```ts -interface CORSOptions { - origin?: string; - methods?: string; - headers?: string; - maxAge?: number; - exposeHeaders?: string; -} -``` - - +Passing an SDK v1 server to `createMcpHandler` still works but emits a deprecation warning. Change the call to `createLegacyMcpHandler` to keep the same behavior without the warning. -```ts -const transport = new WorkerTransport({ - corsOptions: { - origin: "https://example.com", - methods: "GET, POST, OPTIONS", - headers: "Content-Type, Authorization", - maxAge: 86400, - }, -}); -``` +`experimental_createMcpHandler` is also deprecated. Replace it with `createLegacyMcpHandler`. - - -#### storage +### `CreateLegacyMcpHandlerOptions` -Persist transport state to survive Durable Object hibernation or restarts. +`CreateLegacyMcpHandlerOptions` extends `WorkerTransportOptions` and adds these fields: -```ts -interface MCPStorageApi { - get(): Promise | TransportState | undefined; - set(state: TransportState): Promise | void; -} +| 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 | -interface TransportState { - sessionId?: string; - initialized: boolean; - protocolVersion?: ProtocolVersion; -} -``` +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 | -```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); - }, - }, -}); -``` +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 +284,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..5e26a9be682 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..b8825d2f558 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"; @@ -139,7 +141,7 @@ You need a Cloudflare Workers project, an OpenAPI 3.x document, and a host-side }, }); - 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..8e19acdf6e1 --- /dev/null +++ b/src/content/docs/agents/model-context-protocol/guides/migrate-to-mcp-sdk-v2.mdx @@ -0,0 +1,396 @@ +--- +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 upgrade in Agents SDK v0.18.0. It explains how to move Stateless servers to `@modelcontextprotocol/server`, keep existing servers on an explicit Legacy handler, and update MCP clients. + +## Choose a server path + +Use the following table to select a migration path: + +| Current server | Migration path | +| ------------------------------------------- | ---------------------------------------------------------------------------------------------------- | +| `createMcpHandler` with an SDK v1 server | Rename the call to `createLegacyMcpHandler` to keep the same behavior. | +| Server ready for Stateless request handling | Move to `@modelcontextprotocol/server` and pass a factory to `createMcpHandler`. | +| Server that must retain Legacy behavior | Keep SDK v1 and use `createLegacyMcpHandler`. | +| `McpAgent` without Legacy stateful features | Migrate directly to an SDK v2 factory and `createMcpHandler`. | +| `McpAgent` using Legacy stateful features | Design Stateless equivalents, serve Stateless and Legacy lanes together, then drain the Legacy lane. | + +The following APIs are deprecated: + +- Passing an SDK v1 server to `createMcpHandler` +- `experimental_createMcpHandler` +- `McpAgent` + +## 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 an explicit Legacy server: + + + +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. + +## Keep existing Legacy behavior + +Use this path when your server depends on any of these 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` + + + +1. Keep importing `McpServer` from `@modelcontextprotocol/sdk`. + +2. Replace `createMcpHandler` with `createLegacyMcpHandler`. + +3. Keep the existing `WorkerTransport` options. + +4. Test initialization, tool calls, reconnects, session deletion, OAuth, and server-to-client requests. + + + + + +```ts title="src/index.ts" +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; +``` + + + +Create a new SDK v1 server for each request unless you provide a persistent transport that is already connected to that server. Reconnecting one server instance to several transports is invalid. + +Refer to the [Legacy Elicitation example](https://github.com/cloudflare/agents/tree/main/examples/mcp-elicitation) for a sessionful server with Durable Object state and SSE replay. + +## 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. Pass the factory itself to `createMcpHandler`. + +5. Remove SDK v1 transport and session options. + +6. Test the endpoint with Stateless and Legacy clients. + + + + + +```ts title="src/index.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 createMcpHandler(createServer); +``` + + + +The handler creates one server for each request. Concurrent Worker requests never share a connected server instance. + +### Stateless handler options + +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. | +| `bus`, `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 createMcpHandler(createServer, { + allowedHostnames: ["mcp.example.com"], + allowedOriginHostnames: ["app.example.com"], + corsOptions: { origin: "https://app.example.com" }, +}); +``` + + + +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 Legacy compatibility + +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. + +Use `createLegacyMcpHandler` or `McpAgent` when a Legacy client needs those features. + +## Migrate an McpAgent server + +`McpAgent` remains an SDK v1 server. Do not change its server import 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: + +| Legacy stateful feature | Stateless design | +| ----------------------------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | +| 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. + +Stateless servers 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 Stateless Elicitation + +Stateless tools, prompts, and resources 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. + +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 + +Stateless MCP changes the transport and lifecycle model: + +| Area | Legacy behavior | Stateless 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; 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.18.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` + +Serve these results with `createLegacyMcpHandler`. The Code Mode MCP connector and `withX402Client` accept either client generation. + +## Plan the rollout + + + +1. Classify each endpoint as Stateless, explicit Legacy, or `McpAgent`. + +2. Pin the MCP SDK versions required by the Agents release. + +3. Rename SDK v1 handler calls before changing their behavior. + +4. Add Stateless handlers beside existing sessionful routes. + +5. Test Stateless and Legacy clients independently. + +6. Test required HTTP headers through every proxy and gateway. + +7. Test OAuth after a clean login and after Durable Object hibernation. + +8. Test cancellation, multiple input rounds, and transport loss. + +9. Verify valid Origins and reject invalid Origins with `403`. + +10. Remove Legacy routes only after existing sessions drain. + + + +Stored HTTP session IDs from Agents releases before v0.18.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..9733ec419a1 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,15 +21,14 @@ 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 | Existing `WorkerTransport` servers | +| [`McpAgent`](/agents/model-context-protocol/apis/agent-api/) | Yes | Legacy | Existing 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 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..36af057713b 100644 --- a/src/content/docs/agents/model-context-protocol/protocol/authorization.mdx +++ b/src/content/docs/agents/model-context-protocol/protocol/authorization.mdx @@ -202,29 +202,42 @@ 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 createMcpHandler(createServer); ``` +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..6859740e1d9 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. Existing Legacy `McpAgent` servers must keep using `@modelcontextprotocol/sdk`. :::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,32 @@ 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); - }, -} satisfies ExportedHandler; +export default createMcpHandler(createServer); ``` ## 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. +For Legacy 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. 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..d255477ef2b 100644 --- a/src/content/docs/agents/model-context-protocol/protocol/transport.mdx +++ b/src/content/docs/agents/model-context-protocol/protocol/transport.mdx @@ -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() { @@ -66,13 +66,7 @@ function createServer() { return server; } -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); - }, -} satisfies ExportedHandler; +export default createMcpHandler(createServer); ``` @@ -86,26 +80,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 +### Legacy servers -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). +Stateless MCP has no protocol-level session. 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. +Servers that require Legacy sessions can use `createLegacyMcpHandler` with `WorkerTransport`, or keep `McpAgent` while their Stateless equivalents are built. 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 @@ -203,15 +191,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 +261,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 +306,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** | Legacy compatibility | 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