createMcpHandler — API Reference
The createMcpHandler function creates a fetch handler to serve your MCP server. You can use it as an alternative to the McpAgent class when you don't need the deprecated SSE transport.
It uses an implementation of the MCP Transport interface, WorkerTransport, built on top of web standards, which conforms to the streamable-http ↗ transport specification.
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<Response>;- server — An instance of
McpServer↗ from the@modelcontextprotocol/sdkpackage - options — Optional configuration object (see
CreateMcpHandlerOptions)
A Worker fetch handler function with the signature (request: Request, env: unknown, ctx: ExecutionContext) => Promise<Response>.
Configuration options for creating an MCP handler.
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;}The URL path where the MCP handler responds. Requests to other paths return a 404 response.
Default: "/mcp"
const handler = createMcpHandler(server, { route: "/api/mcp", // Only respond to requests at /api/mcp});const handler = createMcpHandler(server, { route: "/api/mcp", // Only respond to requests at /api/mcp});An authentication context object that will be available to MCP tools via getMcpAuthContext().
When using the OAuthProvider 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.
A custom WorkerTransport instance. If not provided, a new transport is created on every request.
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 });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 });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 ↗.
import { createMcpHandler } from "agents/mcp";import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";import { z } from "zod";
function createServer() { const server = new McpServer({ name: "Hello MCP Server", version: "1.0.0", });
server.tool( "hello", "Returns a greeting message", { name: z.string().optional() }, async ({ name }) => { return { content: [ { text: `Hello, ${name ?? "World"}!`, type: "text", }, ], }; }, );
return server;}
export default { fetch: async (request, env, ctx) => { // Create new server instance per request const server = createServer(); return createMcpHandler(server)(request, env, ctx); },};import { createMcpHandler } from "agents/mcp";import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";import { z } from "zod";
function createServer() { const server = new McpServer({ name: "Hello MCP Server", version: "1.0.0", });
server.tool( "hello", "Returns a greeting message", { name: z.string().optional() }, async ({ name }) => { return { content: [ { text: `Hello, ${name ?? "World"}!`, type: "text", }, ], }; }, );
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); },};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.
For stateful MCP servers that need to maintain session state across multiple requests, you can use the createMcpHandler function with a WorkerTransport instance directly in an Agent. This is useful if you want to make use of advanced client features like elicitation and sampling.
Provide a custom WorkerTransport with persistent storage. View the complete example on GitHub ↗.
import { Agent } from "agents";import { createMcpHandler, WorkerTransport } from "agents/mcp";import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
const STATE_KEY = "mcp-transport-state";
export class MyStatefulMcpAgent extends Agent { server = new McpServer({ name: "Stateful MCP Server", version: "1.0.0", });
transport = new WorkerTransport({ sessionIdGenerator: () => this.name, storage: { get: () => { return this.ctx.storage.kv.get(STATE_KEY); }, set: (state) => { this.ctx.storage.kv.put(STATE_KEY, state); }, }, });
async onMcpRequest(request) { return createMcpHandler(this.server, { transport: this.transport, })(request, this.env, {}); }}import { Agent } from "agents";import { createMcpHandler, WorkerTransport, type TransportState,} from "agents/mcp";import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
const STATE_KEY = "mcp-transport-state";
export class MyStatefulMcpAgent extends Agent<Env, State> { server = new McpServer({ name: "Stateful MCP Server", version: "1.0.0", });
transport = new WorkerTransport({ sessionIdGenerator: () => this.name, storage: { get: () => { return this.ctx.storage.kv.get<TransportState>(STATE_KEY); }, set: (state: TransportState) => { this.ctx.storage.kv.put<TransportState>(STATE_KEY, state); }, }, });
async onMcpRequest(request: Request) { return createMcpHandler(this.server, { transport: this.transport, })(request, this.env, {} as ExecutionContext); }}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:
import { getAgentByName } from "agents";
export default { async fetch(request, env, ctx) { // Extract session ID from header or generate a new one const sessionId = request.headers.get("mcp-session-id") ?? crypto.randomUUID();
// 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.onMcpRequest(request); },};import { getAgentByName } from "agents";
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();
// 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.onMcpRequest(request); },};With persistent storage, the transport preserves:
- Session ID across reconnections
- Protocol version negotiation state
- Initialization status
This allows MCP clients to reconnect and resume their session in the event of a connection loss.
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.
| 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 |
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.
import { createMcpHandler } from "agents/mcp";import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
// INCORRECT: Global server instanceconst server = new McpServer({ name: "Hello MCP Server", version: "1.0.0",});
server.tool("hello", "Returns a greeting", {}, async () => { return { content: [{ text: "Hello, World!", type: "text" }], };});
export default { fetch: async (request, env, ctx) => { // This will fail on second request with MCP SDK 1.26.0+ return createMcpHandler(server)(request, env, ctx); },};import { createMcpHandler } from "agents/mcp";import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
// INCORRECT: Global server instanceconst server = new McpServer({ name: "Hello MCP Server", version: "1.0.0",});
server.tool("hello", "Returns a greeting", {}, async () => { return { content: [{ text: "Hello, World!", type: "text" }], };});
export default { fetch: async (request: Request, env: Env, ctx: ExecutionContext) => { // This will fail on second request with MCP SDK 1.26.0+ return createMcpHandler(server)(request, env, ctx); },};import { createMcpHandler } from "agents/mcp";import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
// CORRECT: Factory function to create server instancefunction 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;}
export default { fetch: async (request, env, ctx) => { // Create new server instance per request const server = createServer(); return createMcpHandler(server)(request, env, ctx); },};import { createMcpHandler } from "agents/mcp";import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
// CORRECT: Factory function to create server instancefunction 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;}
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); },};If you are using the raw SDK transport directly (not via createMcpHandler), you must also create new transport instances per request:
import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";import { WebStandardStreamableHTTPServerTransport } from "@modelcontextprotocol/sdk/server/webStandardStreamableHttp.js";
function createServer() { const server = new McpServer({ name: "Hello MCP Server", version: "1.0.0", });
// Register tools...
return server;}
export default { async fetch(request) { // Create new transport and server per request const transport = new WebStandardStreamableHTTPServerTransport(); const server = createServer(); server.connect(transport); return transport.handleRequest(request); },};import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";import { WebStandardStreamableHTTPServerTransport } from "@modelcontextprotocol/sdk/server/webStandardStreamableHttp.js";
function createServer() { const server = new McpServer({ name: "Hello MCP Server", version: "1.0.0", });
// Register tools...
return server;}
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); },};The WorkerTransport class implements the MCP Transport interface, handling HTTP request/response cycles, Server-Sent Events (SSE) streaming, session management, and CORS.
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<Response>; async send( message: JSONRPCMessage, options?: TransportSendOptions, ): Promise<void>; async start(): Promise<void>; async close(): Promise<void>;}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;}Provides a custom session identifier. This session identifier is used to identify the session in the MCP Client.
const transport = new WorkerTransport({ sessionIdGenerator: () => `user-${Date.now()}-${Math.random()}`,});const transport = new WorkerTransport({ sessionIdGenerator: () => `user-${Date.now()}-${Math.random()}`,});Disables SSE streaming and returns responses as standard JSON.
const transport = new WorkerTransport({ enableJsonResponse: true, // Disable streaming, return JSON responses});const transport = new WorkerTransport({ enableJsonResponse: true, // Disable streaming, return JSON responses});A callback that fires when a session is initialized, either by creating a new session or restoring from storage.
const transport = new WorkerTransport({ onsessioninitialized: (sessionId) => { console.log(`MCP session initialized: ${sessionId}`); },});const transport = new WorkerTransport({ onsessioninitialized: (sessionId) => { console.log(`MCP session initialized: ${sessionId}`); },});Configure CORS headers for cross-origin requests.
interface CORSOptions { origin?: string; methods?: string; headers?: string; maxAge?: number; exposeHeaders?: string;}const transport = new WorkerTransport({ corsOptions: { origin: "https://example.com", methods: "GET, POST, OPTIONS", headers: "Content-Type, Authorization", maxAge: 86400, },});const transport = new WorkerTransport({ corsOptions: { origin: "https://example.com", methods: "GET, POST, OPTIONS", headers: "Content-Type, Authorization", maxAge: 86400, },});Persist transport state to survive Durable Object hibernation or restarts.
interface MCPStorageApi { get(): Promise<TransportState | undefined> | TransportState | undefined; set(state: TransportState): Promise<void> | void;}
interface TransportState { sessionId?: string; initialized: boolean; protocolVersion?: ProtocolVersion;}const transport = new WorkerTransport({ storage: { get: async () => { // Retrieve state from Durable Object storage return await this.ctx.storage.get("mcp-state"); }, set: async (state) => { // Persist state to Durable Object storage await this.ctx.storage.put("mcp-state", state); }, },});const transport = new WorkerTransport({ storage: { get: async () => { // Retrieve state from Durable Object storage return await this.ctx.storage.get<TransportState>("mcp-state"); }, set: async (state) => { // Persist state to Durable Object storage await this.ctx.storage.put("mcp-state", state); }, },});When using OAuth authentication 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.
interface McpAuthContext { props: Record<string, unknown>;}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 accessable directly on this.props instead.
import { getMcpAuthContext } from "agents/mcp";
function getMcpAuthContext(): McpAuthContext | undefined;import { getMcpAuthContext } from "agents/mcp";import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
const server = new McpServer({ name: "Auth Server", version: "1.0.0" });
server.tool("getProfile", "Get the current user's profile", {}, async () => { // Access user info automatically populated by OAuth provider const auth = getMcpAuthContext(); const username = auth?.props?.username; const email = auth?.props?.email;
return { content: [ { type: "text", text: `User: ${username ?? "anonymous"}, Email: ${email ?? "none"}`, }, ], };});import { getMcpAuthContext } from "agents/mcp";import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
const server = new McpServer({ name: "Auth Server", version: "1.0.0" });
server.tool("getProfile", "Get the current user's profile", {}, async () => { // Access user info automatically populated by OAuth provider 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"}`, }, ], };});The createMcpHandler automatically catches errors and returns JSON-RPC error responses with code -32603 (Internal error).
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": <request_id>// }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": <request_id>// }