Skip to content
Open
Show file tree
Hide file tree
Changes from 15 commits
Commits
Show all changes
29 commits
Select commit Hold shift + click to select a range
4fa6ed9
feat: demo setup
mattzcarey Oct 9, 2025
132f761
feat: rpc transport first draft
mattzcarey Oct 13, 2025
24ba379
fix: rpc tests
mattzcarey Oct 13, 2025
fd9d155
fix: remove unused rpc types
mattzcarey Oct 13, 2025
7aacc06
fix: wrangler
mattzcarey Oct 13, 2025
f66440c
add public
mattzcarey Oct 13, 2025
2cc44ff
feat: json rpc validation + batches
mattzcarey Oct 14, 2025
289e7b3
fix: naming of connection helper
mattzcarey Oct 14, 2025
00a20d9
fix: cleaner handleMcpMessage
mattzcarey Oct 14, 2025
5273dcd
fix: example for oauth2
mattzcarey Oct 14, 2025
3baaeaa
fix: using onStart directly is bad
mattzcarey Oct 16, 2025
dafa564
fix: rpc-transport example
mattzcarey Oct 16, 2025
6e404aa
feat: example readme
mattzcarey Oct 16, 2025
1a363a2
chore: rename example to mcp-rpc-transport
mattzcarey Oct 16, 2025
0ab36d8
feat: transport doc
mattzcarey Oct 16, 2025
3078318
more fiddling
mattzcarey Oct 16, 2025
46018dd
feat: overload the addMcpServer function
mattzcarey Oct 16, 2025
b5cf27b
feat: pass props
mattzcarey Oct 16, 2025
7268b17
add note about onStart
mattzcarey Oct 16, 2025
da55f6b
fix: mega typing
mattzcarey Oct 16, 2025
12dbb84
fix: probs nesting
mattzcarey Oct 16, 2025
67ad112
feat: restructure docs
mattzcarey Oct 16, 2025
0e9a151
fix: concurent sends
mattzcarey Oct 17, 2025
f7da517
fix: setting name after hibernation
mattzcarey Oct 17, 2025
cae6c3e
fix: types
mattzcarey Oct 17, 2025
1d18f41
feat: add validation to rpc binding
mattzcarey Oct 20, 2025
dacbce8
fix: lazy load props
mattzcarey Oct 21, 2025
0d563e2
chore: enable observability in mcp-rpc-transport demo config
mattzcarey Oct 21, 2025
b1245a8
refactor: normalize RPC server names
mattzcarey Oct 22, 2025
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
283 changes: 283 additions & 0 deletions docs/mcp-transports.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,283 @@
# 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

⚠️ **No authentication**: Not suitable for public APIs - use HTTP/SSE for external connections

### Connecting an Agent to an McpAgent via RPC

The RPC transport uses [Cloudflare Service Bindings](https://developers.cloudflare.com/workers/runtime-apis/bindings/service-bindings/) to connect your `Agent` (MCP client) directly to your `McpAgent` (MCP server) using Durable Object RPC calls.

#### Step 1: Define your MCP server

First, 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";

export class MyMCP extends McpAgent<Env, State, {}> {
server = new McpServer({
name: "MyMCP",
version: "1.0.0"
});

initialState: State = {
counter: 0
};

async init() {
// Define a tool
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 `addRpcMcpServer()` in the `onStart()` method:

```typescript
import { AIChatAgent } from "agents/ai-chat-agent";

export class Chat extends AIChatAgent<Env> {
async onStart(): Promise<void> {
// Connect to MyMCP via RPC
await this.addRpcMcpServer("my-mcp", "MyMCP");
// ▲ ▲
// │ └─ Binding name (from wrangler.jsonc)
// └─ Server ID (any unique string)
}

async onChatMessage(onFinish) {
// MCP tools are now available!
const allTools = this.mcp.getAITools();

const result = streamText({
model,
tools: allTools
// ...
});

return createUIMessageStreamResponse({ stream: result });
}
}
```

The `addRpcMcpServer()` method:

1. Gets the Durable Object stub from `env.MyMCP.get(id)`
2. Creates an RPC transport that calls `stub.handleMcpMessage(message)`
3. Connects your agent's MCP client to this transport

#### 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", // This is the binding name you pass to addRpcMcpServer
"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);

// Serve MCP server via streamable-http on /mcp endpoint
if (url.pathname.startsWith("/mcp")) {
return MyMCP.serve("/mcp").fetch(request, env, ctx);
}

// Route other requests to agents
const response = await routeAgentRequest(request, env);
if (response) return response;

return new Response("Not found", { status: 404 });
}
};
```

Optionally, you can also expose your MCP server via streamable-http.

That's it! When your agent makes an MCP call, it:

1. Serializes the JSON-RPC message
2. Calls `stub.handleMcpMessage(message)` over RPC
3. The `McpAgent` processes it and returns the response
4. Your agent receives the result - all without any network calls

### How RPC transport works under the hood

When you call `addRpcMcpServer()`, the SDK creates an RPC transport that calls the `handleMcpMessage()` method on your `McpAgent`:

```typescript
// Built into the McpAgent base class
async handleMcpMessage(
message: JSONRPCMessage
): Promise<JSONRPCMessage | JSONRPCMessage[] | undefined> {
// Recreate transport if needed (e.g., after hibernation)
if (!this._transport) {
const server = await this.server;
this._transport = new RPCServerTransport();
await server.connect(this._transport);
}

return await this._transport.handle(message);
}
```

This happens entirely within your Worker's execution context using Cloudflare's RPC mechanism - no HTTP, no WebSockets, no public internet.

The RPC transport is minimal by design (~350 lines) and fully supports:

- JSON-RPC 2.0 validation (with helpful error messages)
- Batch requests
- Notifications (messages without `id` field)
- Automatic reconnection after Durable Object hibernation

### Advanced: Custom RPC function names

By default, the RPC transport calls the `handleMcpMessage` function. You can customize this:

```typescript
await this.addRpcMcpServer("my-server", "MyMCP", {
functionName: "customHandler"
});
```

Your `McpAgent` would then need to implement:

```typescript
async customHandler(
message: JSONRPCMessage
): Promise<JSONRPCMessage | JSONRPCMessage[] | undefined> {
// Your custom logic
}
```

## 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)
1 change: 1 addition & 0 deletions examples/mcp-rpc-transport/.env_example
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
OPENAI_API_KEY=your_openai_api_key_here
83 changes: 83 additions & 0 deletions examples/mcp-rpc-transport/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,83 @@
# RPC Transport for MCP

Example showing an `Agent` calling an `McpAgent` within the same worker using a custom RPC transport.

## Why RPC Transport?

If your MCP server and your agent/client are both deployed to the Cloudflare developer platform, our RPC transport is the fastest way to connect them:

- **No network overhead** - Direct function calls instead of HTTP
- **Simpler** - No endpoints to configure, no connection management, no authentication.

This is very useful for internal applications. You can define `tools`, `prompts` and `resources` in your MCP server, expose that publically to your users, and also power your own `Agent` from the same `McpAgent`.

## How it works

Both the agent (MCP client) and MCP server can exist in the same Worker.

The MCP server is just a regular `McpAgent`:

```typescript
export class MyMCP extends McpAgent<Env, State, {}> {
server = new McpServer({
name: "Demo",
version: "1.0.0"
});

async init() {
this.server.tool(
"add",
"Add to the counter, stored in the MCP",
{ a: z.number() },
async ({ a }) => {
this.setState({ ...this.state, counter: this.state.counter + a });
return {
content: [
{
text: `Added ${a}, total is now ${this.state.counter}`,
type: "text"
}
]
};
}
);
}
}
```

The agent calls out to the MCP server using Cloudflare's RPC bindings:

```typescript
export class Chat extends AIChatAgent<Env> {
async onStart(): Promise<void> {
// Connect to MyMCP server via RPC
await this.addRpcMcpServer("test-server", "MyMCP");
}

async onChatMessage(onFinish: StreamTextOnFinishCallback<ToolSet>) {
// MCP tools are now available
const allTools = this.mcp.getAITools();

const result = streamText({
model,
tools: allTools
// ...
});
}
}
```

## Instructions

1. Copy `.dev.vars.example` to `.dev.vars` and add your OpenAI API key
2. Run `npm install`
3. Run `npm start`
4. Open the UI in your browser

Try asking the AI to add numbers to the counter!

## More Info

Sevice bindings over RPC are commonly used in Workers to call out to other Cloudflare services. You can find out more [in the docs](https://developers.cloudflare.com/workers/runtime-apis/bindings/).

The Model Context Protocol supports [pluggable transports](https://modelcontextprotocol.io/specification/2025-06-18/basic/transports). The code for this custom RPC transport can be found [here](packages/agents/src/mcp/rpc.ts)
10 changes: 10 additions & 0 deletions examples/mcp-rpc-transport/index.html
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
<!doctype html>
<html>
<head>
<title>MCP Example</title>
</head>
<body>
<div id="root"></div>
<script type="module" src="/src/client.tsx"></script>
</body>
</html>
11 changes: 11 additions & 0 deletions examples/mcp-rpc-transport/package.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
{
"author": "Matt Carey <mcarey@cloudflare.com>",
"keywords": [],
"name": "@cloudflare/agents-mcp-rpc-transport-demo",
"private": true,
"scripts": {
"start": "vite dev",
"deploy": "vite build && wrangler deploy"
},
"type": "module"
}
Binary file added examples/mcp-rpc-transport/public/favicon.ico
Binary file not shown.
Loading
Loading