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 8e692c3ce7..c95efa562c 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; } ``` @@ -112,9 +115,76 @@ const handler = createMcpHandler(server, { transport }); +## Using the MCP SDK Directly + +For the simplest possible stateless MCP server, you can use the `@modelcontextprotocol/sdk` package directly with `WebStandardStreamableHTTPServerTransport`. This approach does not use the `agents` package and provides zero-config MCP server functionality that works on Cloudflare Workers. View the [complete example on GitHub](https://github.com/cloudflare/agents/tree/main/examples/mcp-server). + + + +```ts title="src/index.ts" +import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js"; +import { WebStandardStreamableHTTPServerTransport } from "@modelcontextprotocol/sdk/server/webStandardStreamableHttp.js"; +import { z } from "zod"; + +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) => { + if (request.method === "OPTIONS") { + return new Response(null, { headers: corsHeaders }); + } + return withCors(await transport.handleRequest(request)); + }, +}; +``` + + + +This approach is recommended when you need a simple, stateless MCP server without additional features like authentication, state management, or agent capabilities. The server handles CORS automatically and works with any MCP client that supports the `streamable-http` transport. + ## Stateless MCP Servers -Many MCP Servers are stateless, meaning they don't 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). +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). @@ -259,6 +329,7 @@ class WorkerTransport implements Transport { ): Promise; async start(): Promise; async close(): Promise; + closeSSEStream(requestId: RequestId): void; } ``` @@ -285,6 +356,11 @@ interface WorkerTransportOptions { */ onsessioninitialized?: (sessionId: string) => void; + /** + * Callback fired when a session is closed via DELETE request. + */ + onsessionclosed?: (sessionId: string) => void; + /** * CORS configuration for cross-origin requests. * Configures Access-Control-* headers. @@ -297,6 +373,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 +432,23 @@ const transport = new WorkerTransport({ +#### onsessionclosed + +A callback that fires when a session is closed via DELETE request. Use this to clean up resources or log session closures. + + + +```ts +const transport = new WorkerTransport({ + onsessionclosed: (sessionId) => { + console.log(`MCP session closed: ${sessionId}`); + // Clean up any resources associated with this session + }, +}); +``` + + + #### corsOptions Configure CORS headers for cross-origin requests. @@ -409,6 +514,90 @@ const transport = new WorkerTransport({ +#### eventStore + +Optional event store for resumability support. When provided, enables clients to reconnect and resume receiving messages using the `Last-Event-ID` header. This is useful for implementing reliable delivery in long-running operations. + +```ts +interface EventStore { + storeEvent(streamId: StreamId, message: JSONRPCMessage): Promise; + replayEventsAfter( + lastEventId: EventId, + options: { + send: (eventId: EventId, message: JSONRPCMessage) => Promise; + }, + ): Promise; + getStreamIdForEventId?(eventId: EventId): Promise; +} +``` + + + +```ts +const transport = new WorkerTransport({ + eventStore: { + storeEvent: async (streamId, message) => { + const eventId = crypto.randomUUID(); + await env.EVENTS.put(`${streamId}:${eventId}`, JSON.stringify(message)); + return eventId; + }, + replayEventsAfter: async (lastEventId, { send }) => { + // Fetch and replay events after lastEventId + // Return the stream ID + }, + }, +}); +``` + + + +#### retryInterval + +Retry interval in milliseconds to suggest to clients in the SSE `retry` field. Controls how often clients should attempt to reconnect when connections are lost. Useful for implementing polling behavior during long-running operations. + + + +```ts +const transport = new WorkerTransport({ + retryInterval: 5000, // Clients will reconnect every 5 seconds +}); +``` + + + +#### Methods + +##### closeSSEStream + +Close an SSE stream for a specific request, triggering client reconnection. Use this to implement polling behavior during long-running operations. The client will reconnect after the retry interval specified in the priming event. + +```ts +closeSSEStream(requestId: RequestId): void; +``` + + + +```ts +const transport = new WorkerTransport({ + retryInterval: 3000, // Client will reconnect every 3 seconds +}); + +// In your MCP tool handler: +server.tool("longOperation", "Start a long-running operation", {}, async () => { + // Start async work + const requestId = getCurrentRequestId(); // Get from context + + // Close the stream to trigger client reconnection + transport.closeSSEStream(requestId); + + return { + content: [{ type: "text", text: "Operation started, polling for updates..." }], + }; +}); +``` + + + ## 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.