MCP handler APIs
Last updated Oct 18, 2026 The Agents SDK provides two server handler paths:
| 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 |
McpAgent is deprecated and feature-frozen. Migrate existing McpAgent servers to a stateless handler. Refer to the migration guide when sessionful features require a staged rollout.
Install dependencies
For a stateless server:
npmyarnpnpmbun
npm i agents @modelcontextprotocol/server@2.0.0 zod
yarn add agents @modelcontextprotocol/server@2.0.0 zod
pnpm add agents @modelcontextprotocol/server@2.0.0 zod
bun add agents @modelcontextprotocol/server@2.0.0 zod
For an explicit legacy server:
npmyarnpnpmbun
npm i agents @modelcontextprotocol/sdk@1.30.0 zod
yarn add agents @modelcontextprotocol/sdk@1.30.0 zod
pnpm add agents @modelcontextprotocol/sdk@1.30.0 zod
bun add agents @modelcontextprotocol/sdk@1.30.0 zod
Use the exact MCP versions required by your installed Agents release.
createMcpHandler
createMcpHandler creates a callable stateless MCP request handler from an MCP SDK v2 server factory. Invoke it from a Worker's object fetch() export or compose it inside another handler.
import {
createMcpHandler,
type CreateMcpHandlerOptions,
type StatelessMcpHandler,
} from "agents/mcp/server";
import type { McpServerFactory } from "@modelcontextprotocol/server";
function createMcpHandler(
factory: McpServerFactory,
options?: CreateMcpHandlerOptions,
): StatelessMcpHandler;
Parameters
factorycreates a freshMcpServerorServerfrom@modelcontextprotocol/server. It can be synchronous or asynchronous.optionscombines Agents Worker options with supported upstream SDK v2 handler options.
The factory receives this request context:
interface McpRequestContext {
era: "modern" | "legacy";
authInfo?: AuthInfo;
requestInfo?: Request;
}
A zero-argument factory remains valid.
Example
import { McpServer } from "@modelcontextprotocol/server";
import { createMcpHandler } from "agents/mcp/server";
import { z } from "zod";
function createServer() {
const server = new McpServer({
name: "hello-server",
version: "1.0.0",
});
server.registerTool(
"hello",
{
description: "Return a greeting",
inputSchema: z.object({ name: z.string().optional() }),
},
async ({ name }) => ({
content: [{ type: "text", text: `Hello, ${name ?? "World"}!` }],
}),
);
return server;
}
export default {
fetch(request, env, ctx) {
return createMcpHandler(createServer)(request, env, ctx);
},
};
src/index.tsts
import { McpServer } from "@modelcontextprotocol/server";
import { createMcpHandler } from "agents/mcp/server";
import { z } from "zod";
function createServer() {
const server = new McpServer({
name: "hello-server",
version: "1.0.0",
});
server.registerTool(
"hello",
{
description: "Return a greeting",
inputSchema: z.object({ name: z.string().optional() }),
},
async ({ name }) => ({
content: [{ type: "text", text: `Hello, ${name ?? "World"}!` }],
}),
);
return server;
}
export default {
fetch(request, env, ctx) {
return createMcpHandler(createServer)(request, env, ctx);
},
} satisfies ExportedHandler;
Pass the factory itself. Do not create one global server instance or pass a constructed SDK v2 server directly.
CreateMcpHandlerOptions
The following options are available:
| Option | Type | Default | Description |
|---|---|---|---|
route |
string |
"/mcp" |
Exact path handled by the Worker wrapper |
corsOptions |
CORSOptions \| false |
Wildcard CORS | CORS response headers, or false to remove them |
allowedHostnames |
string[] |
Localhost or workers.dev route |
Optional Host restriction for custom domains |
allowedOriginHostnames |
string[] \| "*" |
Localhost, workers.dev, or concrete CORS Origin |
Browser Origin restriction, or explicit middleware delegation |
authContext |
McpAuthContext |
Execution context props | Application props returned by getMcpAuthContext() |
legacy |
"stateless" \| "reject" |
"stateless" |
legacy compatibility or stateless-only rejection |
responseMode |
"auto" \| "json" \| "sse" |
"auto" |
stateless request response shaping |
onerror |
(error: Error) => void |
None | Out-of-band error reporting |
maxSubscriptions |
number |
1,024 |
Maximum concurrent listen streams |
keepAliveMs |
number |
15,000 |
Keepalive interval for listen streams |
SDK v1 transport options do not apply to this handler. It rejects options such as transport, storage, sessionIdGenerator, eventStore, and enableJsonResponse.
Use responseMode: "json" instead of enableJsonResponse: true. JSON mode drops notifications emitted before a final result.
Factory lifecycle
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.
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.
Elicitation with a stateless handler
Elicitation through a stateless handler returns input_required and completes through multi-round-trip requests (MRTR). On each retry, the SDK echoes the latest requestState and sends responses for the immediately preceding input round. It does not accumulate earlier inputResponses. The Worker does not remain suspended while a user responds.
Use inputRequired(...) to request input. Read that round's accepted form content from context.mcpReq.inputResponses with acceptedContent(...). Seal trusted intermediate values needed by later rounds into integrity-protected requestState.
Refer to the stateless elicitation example ↗︎ for a two-round tool flow. For stateful pushed requests, refer to Elicitation on legacy servers.
Origin validation and CORS
The Workers wrapper validates every present browser Origin. It rejects malformed, opaque, and non-HTTP Origins with 403. Origin-less non-browser MCP clients remain valid.
The 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:
export default {
fetch(request, env, ctx) {
return createMcpHandler(createServer, {
allowedHostnames: ["mcp.example.com"],
corsOptions: {
origin: "https://app.example.com",
},
})(request, env, ctx);
},
};
export default {
fetch(request, env, ctx) {
return createMcpHandler(createServer, {
allowedHostnames: ["mcp.example.com"],
corsOptions: {
origin: "https://app.example.com",
},
})(request, env, ctx);
},
} satisfies ExportedHandler;
Allowlist values are hostnames without a scheme or port. Origin matching ignores scheme and port.
Set allowedOriginHostnames: "*" only when trusted middleware validates Origins before calling the handler. This value turns off the handler Origin check, including malformed and opaque Origin rejection. MCP HTTP servers must validate browser Origins.
CORS response headers are not authentication. Protect the MCP endpoint with OAuth or another authentication layer.
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.
Compatibility with legacy clients
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.
This compatibility path does not provide a complete session transport:
- Each POST creates a new server and transport.
- HTTP GET and DELETE return
405. - No MCP session ID persists.
- Pushed elicitation, sampling, and roots requests fail immediately.
- Standalone streams, resumability, replay, and session deletion are unavailable.
- Published experimental tasks are not supported through this path.
Set legacy: "reject" for a stateless-only endpoint. During migration, route legacy clients that still require protocol sessions to a temporary createLegacyMcpHandler or McpAgent lane.
Return value
createMcpHandler returns a StatelessMcpHandler. It is callable and exposes request and notification controls:
```ts interface StatelessMcpHandler { (request: Request, env: unknown, ctx: ExecutionContext): Promise<Response>;
fetch( request: Request, options?: McpHandlerRequestOptions, ): Promise<Response>;
notify: { toolsChanged(): void; promptsChanged(): void; resourcesChanged(): void; resourceUpdated(uri: string): void; }; }
