diff --git a/src/content/changelog/agents/2026-01-12-mcp-sdk-upgrade.mdx b/src/content/changelog/agents/2026-01-12-mcp-sdk-upgrade.mdx new file mode 100644 index 000000000000000..93b8f15052d3c83 --- /dev/null +++ b/src/content/changelog/agents/2026-01-12-mcp-sdk-upgrade.mdx @@ -0,0 +1,107 @@ +--- +title: MCP SDK upgraded to v1.25.2 with enhanced error handling and new transport features +description: MCP SDK upgrade brings improved error handling for transport detection, new WorkerTransport options, and a simpler way to build MCP servers +products: + - agents + - workers +date: 2026-01-12 +--- + +The Agents SDK has been updated with [@modelcontextprotocol/sdk v1.25.2](https://github.com/modelcontextprotocol/typescript-sdk), bringing several improvements to MCP server development and transport handling. + +### Enhanced error handling + +The MCP SDK now uses `StreamableHTTPError` with structured error codes instead of message-only errors. The Agents SDK has been updated to handle these new error structures correctly: + +- **404/405 errors**: Properly detected for transport fallback +- **401 errors**: Correctly identified for authentication failures +- **Improved detection**: Error handling now checks both error codes and messages for better reliability + +This change improves the auto-transport selection feature, allowing the SDK to more reliably detect when a transport is not supported and fall back to alternatives. + +### New WorkerTransport options + +The `WorkerTransport` class now supports additional options for advanced use cases: + +#### Session lifecycle callback + +Track when sessions are closed: + +```ts +const transport = new WorkerTransport({ + onsessionclosed: (sessionId) => { + console.log(`Session ${sessionId} was closed`); + // Clean up session-specific resources + }, +}); +``` + +#### Event store for resumability + +Enable clients to reconnect and resume from their last received message: + +```ts +const transport = new WorkerTransport({ + eventStore: { + storeEvent: async (streamId, message) => { + const eventId = crypto.randomUUID(); + await env.KV.put(`event:${eventId}`, JSON.stringify({ streamId, message })); + return eventId; + }, + replayEventsAfter: async (lastEventId, { send }) => { + // Retrieve and replay events after lastEventId + const events = await getEventsAfter(lastEventId); + for (const event of events) { + await send(event.id, event.message); + } + return events[0]?.streamId ?? "_GET_stream"; + }, + }, +}); +``` + +#### Polling behavior support + +Control client reconnection timing and implement polling patterns: + +```ts +const transport = new WorkerTransport({ + retryInterval: 5000, // Client reconnects after 5 seconds +}); + +// Close stream to trigger reconnection +transport.closeSSEStream(requestId); +``` + +The new `closeSSEStream` method allows servers to close individual SSE streams, triggering client reconnection after the specified retry interval. This is useful for implementing polling patterns during long-running operations. + +### Simplified MCP server creation + +A new example demonstrates using `WebStandardStreamableHTTPServerTransport` from the MCP SDK directly, without the `agents` package. This provides the simplest way to create a stateless MCP server on Cloudflare Workers: + +```ts +import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js"; +import { WebStandardStreamableHTTPServerTransport } from "@modelcontextprotocol/sdk/server/webStandardStreamableHttp.js"; + +const server = new McpServer({ + name: "Hello MCP Server", + version: "1.0.0", +}); + +const transport = new WebStandardStreamableHTTPServerTransport(); +server.connect(transport); + +export default { + fetch: async (request: Request) => { + return transport.handleRequest(request); + }, +}; +``` + +This approach requires no additional dependencies beyond the MCP SDK and is ideal when you do not need state management or authentication. View the [complete example](https://github.com/cloudflare/agents/tree/main/examples/mcp-server) on GitHub. + +For more advanced use cases with state management and authentication, continue using [`createMcpHandler`](/agents/model-context-protocol/mcp-handler-api/) from the Agents SDK. + +### API documentation updates + +The [MCP Handler API reference](/agents/model-context-protocol/mcp-handler-api/) has been updated with documentation for all new WorkerTransport options and methods. The [Transport guide](/agents/model-context-protocol/transport/) now includes both approaches for building MCP servers. diff --git a/src/content/docs/agents/model-context-protocol/mcp-handler-api.mdx b/src/content/docs/agents/model-context-protocol/mcp-handler-api.mdx index 8e692c3ce774cd2..e9f63f5e75bd904 100644 --- a/src/content/docs/agents/model-context-protocol/mcp-handler-api.mdx +++ b/src/content/docs/agents/model-context-protocol/mcp-handler-api.mdx @@ -61,8 +61,11 @@ interface CreateMcpHandlerOptions extends WorkerTransportOptions { sessionIdGenerator?: () => string; enableJsonResponse?: boolean; onsessioninitialized?: (sessionId: string) => void; + onsessionclosed?: (sessionId: string) => void; corsOptions?: CORSOptions; storage?: MCPStorageApi; + eventStore?: EventStore; + retryInterval?: number; } ``` @@ -259,6 +262,7 @@ class WorkerTransport implements Transport { ): Promise; async start(): Promise; async close(): Promise; + closeSSEStream(requestId: RequestId): void; } ``` @@ -285,6 +289,12 @@ interface WorkerTransportOptions { */ onsessioninitialized?: (sessionId: string) => void; + /** + * Callback invoked when a session is closed via DELETE request. + * Receives the session ID that was closed. + */ + onsessionclosed?: (sessionId: string) => void; + /** * CORS configuration for cross-origin requests. * Configures Access-Control-* headers. @@ -297,6 +307,18 @@ interface WorkerTransportOptions { * so it survives hibernation/restart. */ storage?: MCPStorageApi; + + /** + * Event store for resumability support. + * If provided, enables clients to reconnect and resume messages using Last-Event-ID. + */ + eventStore?: EventStore; + + /** + * Retry interval in milliseconds to suggest to clients in SSE retry field. + * Controls client reconnection timing for polling behavior. + */ + retryInterval?: number; } ``` @@ -344,6 +366,23 @@ const transport = new WorkerTransport({ +#### onsessionclosed + +A callback that fires when a session is closed via a DELETE request. Use this to clean up resources or perform logging when a client explicitly terminates a session. + + + +```ts +const transport = new WorkerTransport({ + onsessionclosed: (sessionId) => { + console.log(`MCP session closed: ${sessionId}`); + // Clean up any session-specific resources + }, +}); +``` + + + #### corsOptions Configure CORS headers for cross-origin requests. @@ -409,6 +448,113 @@ const transport = new WorkerTransport({ +#### eventStore + +An optional event store for resumability support. When provided, the transport stores each message with a unique event ID, allowing clients to reconnect and resume from their last received message using the `Last-Event-ID` header. + +```ts +interface EventStore { + /** + * Stores an event for later retrieval. + * Returns the generated event ID. + */ + storeEvent(streamId: string, message: JSONRPCMessage): Promise; + + /** + * Replays events after the given event ID. + * Returns the stream ID that was replayed. + */ + replayEventsAfter( + lastEventId: string, + options: { + send: (eventId: string, message: JSONRPCMessage) => Promise; + }, + ): Promise; + + /** + * Optional: Get stream ID for a given event ID. + */ + getStreamIdForEventId?(eventId: string): Promise; +} +``` + + + +```ts +const transport = new WorkerTransport({ + eventStore: { + storeEvent: async (streamId, message) => { + const eventId = crypto.randomUUID(); + await env.KV.put(`event:${eventId}`, JSON.stringify({ streamId, message })); + return eventId; + }, + replayEventsAfter: async (lastEventId, { send }) => { + // Retrieve and replay events after lastEventId + const events = await getEventsAfter(lastEventId); + for (const event of events) { + await send(event.id, event.message); + } + return events[0]?.streamId ?? "_GET_stream"; + }, + }, +}); +``` + + + +#### retryInterval + +The retry interval in milliseconds to suggest to clients in the SSE `retry` field. This controls how quickly clients reconnect when the connection is closed, enabling polling behavior during long-running operations. + + + +```ts +const transport = new WorkerTransport({ + retryInterval: 5000, // Suggest clients reconnect after 5 seconds +}); +``` + + + +When combined with the `closeSSEStream` method, this enables implementing polling patterns where the server closes the connection and the client automatically reconnects after the specified interval. + +### Methods + +#### closeSSEStream + +Close an SSE stream for a specific request, triggering client reconnection. Use this to implement polling behavior during long-running operations where the client will automatically reconnect after the retry interval specified in the priming event. + +```ts +closeSSEStream(requestId: RequestId): void; +``` + + + +```ts +// In an MCP tool handler +server.tool("longRunningTask", "Start a long-running task", {}, async () => { + // Start the task asynchronously + startBackgroundTask(); + + // Close the SSE stream to trigger client reconnection + // The client will reconnect based on the retryInterval + transport.closeSSEStream(currentRequestId); + + return { + content: [ + { + type: "text", + text: "Task started. Client will reconnect to check status.", + }, + ], + }; +}); +``` + + + +This method is useful when implementing polling patterns where the server needs to perform work between client connections rather than keeping a long-lived connection open. + ## Authentication Context When using [OAuth authentication](/agents/model-context-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. diff --git a/src/content/docs/agents/model-context-protocol/transport.mdx b/src/content/docs/agents/model-context-protocol/transport.mdx index 160517c6e4fc0a2..50de2f78e63ad63 100644 --- a/src/content/docs/agents/model-context-protocol/transport.mdx +++ b/src/content/docs/agents/model-context-protocol/transport.mdx @@ -22,7 +22,83 @@ MCP servers built with the [Agents SDK](/agents) use [`createMcpHandler`](/agent ## Implementing remote MCP transport -Use [`createMcpHandler`](/agents/model-context-protocol/mcp-handler-api/) to create an MCP server that handles Streamable HTTP transport. This is the recommended approach for new MCP servers. +The Agents SDK provides two approaches for creating MCP servers: + +1. **Using the MCP SDK directly** — Use `WebStandardStreamableHTTPServerTransport` from `@modelcontextprotocol/sdk` for the simplest zero-configuration MCP server +2. **Using `createMcpHandler`** — Use the Agents SDK helper for additional features like session management and authentication + +### Using the MCP SDK directly + +For the simplest MCP server, use `WebStandardStreamableHTTPServerTransport` from the `@modelcontextprotocol/sdk` package directly. This requires no additional dependencies beyond the MCP SDK and runs on Cloudflare Workers without any configuration. + +View the [complete example on GitHub](https://github.com/cloudflare/agents/tree/main/examples/mcp-server). + + + +```ts +import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js"; +import { z } from "zod"; +import { WebStandardStreamableHTTPServerTransport } from "@modelcontextprotocol/sdk/server/webStandardStreamableHttp.js"; + +const server = new McpServer({ + name: "Hello MCP Server", + version: "1.0.0", +}); + +server.registerTool( + "hello", + { + description: "Returns a greeting message", + inputSchema: { name: z.string().optional() }, + }, + async ({ name }) => { + return { + content: [ + { + text: `Hello, ${name ?? "World"}!`, + type: "text", + }, + ], + }; + }, +); + +const transport = new WebStandardStreamableHTTPServerTransport(); +server.connect(transport); + +const corsHeaders = { + "Access-Control-Allow-Origin": "*", + "Access-Control-Allow-Methods": "GET, POST, DELETE, OPTIONS", + "Access-Control-Allow-Headers": + "Content-Type, Accept, mcp-session-id, mcp-protocol-version", + "Access-Control-Expose-Headers": "mcp-session-id", + "Access-Control-Max-Age": "86400", +}; + +function withCors(response: Response): Response { + for (const [key, value] of Object.entries(corsHeaders)) { + response.headers.set(key, value); + } + return response; +} + +export default { + fetch: async (request: Request, _env: Env, _ctx: ExecutionContext) => { + if (request.method === "OPTIONS") { + return new Response(null, { headers: corsHeaders }); + } + return withCors(await transport.handleRequest(request)); + }, +}; +``` + + + +This approach is ideal when you do not need state management or authentication. + +### Using createMcpHandler + +Use [`createMcpHandler`](/agents/model-context-protocol/mcp-handler-api/) to create an MCP server with additional features like session management, state persistence, and authentication support. #### Get started quickly