branch:
mcp-transports.md
9588 bytesRaw
# MCP Transports

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 `McpAgent`[here](docs/mcp-servers.md)

## Streamable HTTP Transport (Recommended)

The **Streamable HTTP** transport is the recommended way to connect to MCP servers.

### How it works

When a client connects to your MCP server:

1. The client makes an HTTP request to your Worker with a JSON-RPC message in the body
2. Your Worker upgrades the connection to a WebSocket
3. The WebSocket connects to your `McpAgent` Durable Object which manages connection state
4. JSON-RPC messages flow bidirectionally over the WebSocket
5. Your Worker streams responses back to the client using Server-Sent Events (SSE)

This is all handled automatically by the `McpAgent.serve()` method:

```typescript
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

### Connection from clients

Clients connect using the `streamable-http` transport:

```typescript
await agent.addMcpServer("my-server", "https://your-worker.workers.dev/mcp");
```

## SSE Transport (Deprecated)

We also support the legacy **SSE (Server-Sent Events)** transport, but it's deprecated in favor of Streamable HTTP.

If you need SSE transport for compatibility:

```typescript
// Server
export default MyMCP.serveSSE("/sse");

// Client
await agent.addMcpServer("my-server", url, callbackHost);
```

## RPC Transport (Experimental)

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.

### Why use RPC transport?

- **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.

### Connecting an Agent to an McpAgent via RPC

The RPC transport uses Durable Object bindings to connect your `Agent` (MCP client) directly to your `McpAgent` (MCP server).

#### Step 1: Define your MCP server

Create your `McpAgent` with the tools you want to expose:

```typescript
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}`
            }
          ]
        };
      }
    );
  }
}
```

#### Step 2: Connect your Agent to the MCP server

In your `Agent`, call `addMcpServer()` with the Durable Object binding in `onStart()`:

```typescript
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](./mcp-client.md) for details). This makes it safe to call `addMcpServer` in `onStart()` without worrying about creating multiple connections on restart.

#### Step 3: Configure Durable Object bindings

In your `wrangler.jsonc`, define bindings for both Durable Objects:

```jsonc
{
  "durable_objects": {
    "bindings": [
      { "name": "Chat", "class_name": "Chat" },
      { "name": "MyMCP", "class_name": "MyMCP" }
    ]
  },
  "migrations": [
    {
      "new_sqlite_classes": ["MyMCP", "Chat"],
      "tag": "v1"
    }
  ]
}
```

#### Step 4: Set up your Worker fetch handler

Route requests to your Chat agent:

```typescript
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 });
  }
};
```

### Passing props from client to server

Since RPC transport does not have an OAuth flow, you can pass user context (like userId, role, etc.) directly as props:

```typescript
await this.addMcpServer("my-mcp", this.env.MyMCP, {
  props: { userId: "user-123", role: "admin" }
});
```

Your `McpAgent` can then access these props:

```typescript
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

### How RPC transport works under the hood

When you call `addMcpServer()` with a Durable Object binding, the SDK:

1. Creates an `RPCClientTransport` that wraps the DO stub
2. Calls `handleMcpMessage()` on the `McpAgent` for each JSON-RPC message
3. The `McpAgent` routes messages through its `RPCServerTransport` to the MCP server
4. 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 `id` field)
- Automatic reconnection after Durable Object hibernation (when called from `onStart()`)

### Configuring RPC Transport Server Timeout

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`:

```typescript
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" }]
        };
      }
    );
  }
}
```

## Choosing a transport

| Transport           | Use when                              | Pros                                     | Cons                            |
| ------------------- | ------------------------------------- | ---------------------------------------- | ------------------------------- |
| **Streamable HTTP** | External MCP servers, production apps | Standard protocol, secure, supports auth | Slight network overhead         |
| **RPC**             | Internal agents                       | Fastest, simplest setup                  | No auth, Service Bindings only  |
| **SSE**             | Legacy compatibility                  | Backwards compatible                     | Deprecated, use Streamable HTTP |

## Examples

- **Streamable HTTP**: See [`examples/mcp`](../examples/mcp)
- **RPC Transport**: See [`examples/mcp-rpc-transport`](../examples/mcp-rpc-transport)
- **MCP Client**: See [`examples/mcp-client`](../examples/mcp-client)