This guide explains the different transport options for connecting to MCP servers with the Agents SDK.
For a primer on MCP Servers and how they are implemented in the Agents SDK with McpAgenthere
The Streamable HTTP transport is the recommended way to connect to MCP servers.
When a client connects to your MCP server:
- The client makes an HTTP request to your Worker with a JSON-RPC message in the body
- Your Worker upgrades the connection to a WebSocket
- The WebSocket connects to your
McpAgentDurable Object which manages connection state - JSON-RPC messages flow bidirectionally over the WebSocket
- Your Worker streams responses back to the client using Server-Sent Events (SSE)
This is all handled automatically by the McpAgent.serve() method:
import { McpAgent } from "agents/mcp";
import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
export class MyMCP extends McpAgent {
server = new McpServer({ name: "Demo", version: "1.0.0" });
async init() {
// Define your tools, resources, prompts
}
}
// Serve with Streamable HTTP transport
export default MyMCP.serve("/mcp");The serve() method returns a Worker with a fetch handler that:
- Handles CORS preflight requests
- Manages WebSocket upgrades
- Routes messages to your Durable Object
Clients connect using the streamable-http transport:
await agent.addMcpServer("my-server", "https://your-worker.workers.dev/mcp");The auto transport serves both Streamable HTTP and legacy SSE on the same endpoint. Capable clients use Streamable HTTP automatically, while older SSE-only clients continue to work.
export default MyMCP.serve("/mcp", { transport: "auto" });The handler distinguishes between the two protocols based on the request shape — no configuration or content negotiation is required from clients. This is useful when migrating from SSE to Streamable HTTP without breaking existing clients.
We also support the legacy SSE (Server-Sent Events) transport, but it is deprecated in favor of Streamable HTTP.
If you need SSE transport for compatibility:
// Server
export default MyMCP.serveSSE("/sse");
// Client
await agent.addMcpServer("my-server", url);The RPC transport is a custom transport 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
- Simpler: No HTTP endpoints, no connection management
- Internal only: Perfect for agents calling MCP servers within the same Worker
Note: RPC transport does not support authentication. Use HTTP/SSE for external connections that require OAuth.
The RPC transport uses Durable Object bindings to connect your Agent (MCP client) directly to your McpAgent (MCP server).
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";
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 "agents/ai-chat-agent";
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.
Deduplication: For RPC transport, if addMcpServer is called with a name that already has an active connection, the existing connection is returned instead of creating a duplicate. For HTTP transport, deduplication matches on both server name and URL (see MCP Client API for details). This makes it safe to call addMcpServer in onStart() without worrying about creating multiple connections on restart.
In your wrangler.jsonc, define bindings for both Durable Objects:
Route requests to your Chat agent:
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 });
}
};Since RPC transport does not have an OAuth flow, you can pass user context (like userId, role, etc.) directly as props:
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<
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}` }]
};
});
}
}The props are:
- Type-safe: TypeScript extracts the Props type from your McpAgent generic
- Persistent: Stored in Durable Object storage via
updateProps() - Available immediately: Set before any tool calls are made
This is useful for:
- User authentication context
- Tenant/organization IDs
- Feature flags or permissions
- Any per-connection configuration
When you call addMcpServer() with a Durable Object binding, the SDK:
- Creates an
RPCClientTransportthat wraps the DO stub - Calls
handleMcpMessage()on theMcpAgentfor each JSON-RPC message - The
McpAgentroutes messages through itsRPCServerTransportto the MCP server - Responses flow back synchronously through the RPC call
This happens entirely within your Worker's execution context using Cloudflare's RPC mechanism - no HTTP, no WebSockets, no public internet.
The RPC transport fully supports:
- JSON-RPC 2.0 validation (via the MCP SDK's schema)
- Batch requests
- Notifications (messages without
idfield) - Automatic reconnection after Durable Object hibernation (when called from
onStart())
The RPC transport has a configurable timeout for waiting for tool responses. By default, the server will wait 60 seconds for a tool handler to respond. You can customize this by overriding getRpcTransportOptions() in your McpAgent:
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 |
| Auto | Migrating from SSE, mixed client support | Serves both protocols on one endpoint | Reserves {path}/message route |
| RPC | Internal agents | Fastest, simplest setup | No auth, Service Bindings only |
| SSE | Legacy compatibility | Backwards compatible | Deprecated, use Streamable HTTP |
- Streamable HTTP: See
examples/mcp - RPC Transport: See
examples/mcp-rpc-transport - MCP Client: See
examples/mcp-client
{ "durable_objects": { "bindings": [ { "name": "Chat", "class_name": "Chat" }, { "name": "MyMCP", "class_name": "MyMCP" } ] }, "migrations": [ { "new_sqlite_classes": ["MyMCP", "Chat"], "tag": "v1" } ] }