Transport
The Model Context Protocol (MCP) specification defines two standard transport mechanisms ↗ for communication between clients and servers:
- stdio — Communication over standard in and standard out, designed for local MCP connections.
- Streamable HTTP — The standard transport method for remote MCP connections, introduced ↗ in March 2025. It uses a single HTTP endpoint for bidirectional messaging.
MCP servers built with the Agents SDK use createMcpHandler to handle Streamable HTTP transport.
Use createMcpHandler to create an MCP server that handles Streamable HTTP transport. This is the recommended approach for new MCP servers.
You can use the "Deploy to Cloudflare" button to create a remote MCP server.
Create an MCP server using createMcpHandler. 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: "My 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" }], }; }, );
return server;}
export default { fetch: (request, env, ctx) => { // Create a 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: "My 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" }], }; }, );
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<Env>;If your MCP server implements authentication & authorization using the Workers OAuth Provider ↗ library, use createMcpHandler with the apiRoute and apiHandler properties. View the complete example on GitHub ↗.
export default new OAuthProvider({ apiRoute: "/mcp", apiHandler: { fetch: (request, env, ctx) => { // Create a new server instance per request const server = createServer(); return createMcpHandler(server)(request, env, ctx); }, }, // ... other OAuth configuration});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); }, }, // ... other OAuth configuration});If your MCP server needs to maintain state across requests, use createMcpHandler with a WorkerTransport inside an Agent class. This allows you to persist session state in Durable Object storage and use advanced MCP features like elicitation ↗ and sampling ↗.
See Stateful MCP Servers for implementation details.
The RPC transport is designed for internal applications where your MCP server and agent are both running on Cloudflare — they can even run in the same Worker. It sends JSON-RPC messages directly over Cloudflare's RPC bindings without going over the public internet.
- Faster — no network overhead, direct function calls between Durable Objects
- Simpler — no HTTP endpoints, no connection management
- Internal only — perfect for agents calling MCP servers within the same Worker
RPC transport does not support authentication. Use Streamable HTTP for external connections that require OAuth.
Create your McpAgent with the tools you want to expose:
import { McpAgent } from "agents/mcp";import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";import { z } from "zod";
export class MyMCP extends McpAgent { server = new McpServer({ name: "MyMCP", version: "1.0.0" }); initialState = { counter: 0 };
async init() { this.server.tool( "add", "Add to the counter", { amount: z.number() }, async ({ amount }) => { this.setState({ counter: this.state.counter + amount }); return { content: [ { type: "text", text: `Added ${amount}, total is now ${this.state.counter}`, }, ], }; }, ); }}import { McpAgent } from "agents/mcp";import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";import { z } from "zod";
type State = { counter: number };
export class MyMCP extends McpAgent<Env, State> { server = new McpServer({ name: "MyMCP", version: "1.0.0" }); initialState: State = { counter: 0 };
async init() { this.server.tool( "add", "Add to the counter", { amount: z.number() }, async ({ amount }) => { this.setState({ counter: this.state.counter + amount }); return { content: [ { type: "text", text: `Added ${amount}, total is now ${this.state.counter}`, }, ], }; }, ); }}In your Agent, call addMcpServer() with the Durable Object binding in onStart():
import { AIChatAgent } from "@cloudflare/ai-chat";
export class Chat extends AIChatAgent { async onStart() { // Pass the DO namespace binding directly await this.addMcpServer("my-mcp", this.env.MyMCP); }
async onChatMessage(onFinish) { const allTools = this.mcp.getAITools();
const result = streamText({ model, tools: allTools, // ... });
return createUIMessageStreamResponse({ stream: result }); }}import { AIChatAgent } from "@cloudflare/ai-chat";
export class Chat extends AIChatAgent<Env> { async onStart(): Promise<void> { // Pass the DO namespace binding directly await this.addMcpServer("my-mcp", this.env.MyMCP); }
async onChatMessage(onFinish) { const allTools = this.mcp.getAITools();
const result = streamText({ model, tools: allTools, // ... });
return createUIMessageStreamResponse({ stream: result }); }}RPC connections are automatically restored after Durable Object hibernation, just like HTTP connections. The binding name and props are persisted to storage so the connection can be re-established without any extra code.
If addMcpServer is called with a name that already has an active connection, the existing connection is returned instead of creating a duplicate. This makes it safe to call in onStart().
In your wrangler.jsonc, define bindings for both Durable Objects:
{ "durable_objects": { "bindings": [ { "name": "Chat", "class_name": "Chat" }, { "name": "MyMCP", "class_name": "MyMCP" } ] }, "migrations": [ { "new_sqlite_classes": ["MyMCP", "Chat"], "tag": "v1" } ]}Route requests to your Chat agent:
import { routeAgentRequest } from "agents";
export default { async fetch(request, env, ctx) { const url = new URL(request.url);
// Optionally expose the MCP server via HTTP as well if (url.pathname.startsWith("/mcp")) { return MyMCP.serve("/mcp").fetch(request, env, ctx); }
const response = await routeAgentRequest(request, env); if (response) return response;
return new Response("Not found", { status: 404 }); },};import { routeAgentRequest } from "agents";
export default { async fetch(request: Request, env: Env, ctx: ExecutionContext) { const url = new URL(request.url);
// Optionally expose the MCP server via HTTP as well if (url.pathname.startsWith("/mcp")) { return MyMCP.serve("/mcp").fetch(request, env, ctx); }
const response = await routeAgentRequest(request, env); if (response) return response;
return new Response("Not found", { status: 404 }); },} satisfies ExportedHandler<Env>;Since RPC transport does not have an OAuth flow, you can pass user context directly as props:
await this.addMcpServer("my-mcp", this.env.MyMCP, { props: { userId: "user-123", role: "admin" },});await this.addMcpServer("my-mcp", this.env.MyMCP, { props: { userId: "user-123", role: "admin" },});Your McpAgent can then access these props:
export class MyMCP extends McpAgent { async init() { this.server.tool("whoami", "Get current user info", {}, async () => { const userId = this.props?.userId || "anonymous"; const role = this.props?.role || "guest";
return { content: [{ type: "text", text: `User ID: ${userId}, Role: ${role}` }], }; }); }}export class MyMCP extends McpAgent< Env, State, { userId?: string; role?: string }> { async init() { this.server.tool("whoami", "Get current user info", {}, async () => { const userId = this.props?.userId || "anonymous"; const role = this.props?.role || "guest";
return { content: [ { type: "text", text: `User ID: ${userId}, Role: ${role}` }, ], }; }); }}Props are type-safe (TypeScript extracts the Props type from your McpAgent generic), persistent (stored in Durable Object storage), and available immediately before any tool calls are made.
The RPC transport has a configurable timeout for waiting for tool responses. By default, the server waits 60 seconds for a tool handler to respond. You can customize this by overriding getRpcTransportOptions() in your McpAgent:
export class MyMCP extends McpAgent { server = new McpServer({ name: "MyMCP", version: "1.0.0" });
getRpcTransportOptions() { return { timeout: 120000 }; // 2 minutes }
async init() { this.server.tool( "long-running-task", "A tool that takes a while", { input: z.string() }, async ({ input }) => { await longRunningOperation(input); return { content: [{ type: "text", text: "Task completed" }], }; }, ); }}export class MyMCP extends McpAgent<Env, State> { server = new McpServer({ name: "MyMCP", version: "1.0.0" });
protected getRpcTransportOptions() { return { timeout: 120000 }; // 2 minutes }
async init() { this.server.tool( "long-running-task", "A tool that takes a while", { input: z.string() }, async ({ input }) => { await longRunningOperation(input); return { content: [{ type: "text", text: "Task completed" }], }; }, ); }}| 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 |
If you have an existing MCP server using the McpAgent class:
- Not using state? Replace your
McpAgentclass withMcpServerfrom@modelcontextprotocol/sdkand usecreateMcpHandler(server)in a Workerfetchhandler. - Using state? Use
createMcpHandlerwith aWorkerTransportinside an Agent class. See Stateful MCP Servers for details. - Need SSE support? Continue using
McpAgentwithserveSSE()for legacy client compatibility. See the McpAgent API reference.
You can test your MCP server using an MCP client that supports remote connections, or use mcp-remote ↗, an adapter that lets MCP clients that only support local connections work with remote MCP servers.
Follow this guide for instructions on how to connect to your remote MCP server to Claude Desktop, Cursor, Windsurf, and other MCP clients.