Skip to content

RPC transport#565

Merged
threepointone merged 34 commits intomainfrom
rpc-transport
Feb 22, 2026
Merged

RPC transport#565
threepointone merged 34 commits intomainfrom
rpc-transport

Conversation

@mattzcarey
Copy link
Contributor

@mattzcarey mattzcarey commented Oct 13, 2025

RPC Transport for MCP

Enables an Agent to connect to an McpAgent via Durable Object RPC bindings — no HTTP, no network overhead. Both can live in the same Worker.

Closes #548

User-facing API

// Pass the DO namespace directly — TypeScript discriminates string vs namespace
await this.addMcpServer("counter", env.MY_MCP);

// With props for user context
await this.addMcpServer("counter", env.MY_MCP, {
  props: { userId: "user-123", role: "admin" }
});

// HTTP still works exactly as before
await this.addMcpServer("github", "https://mcp.github.com");

addMcpServer now has proper TypeScript overloads so the RPC and HTTP paths are type-safe. AddRpcMcpServerOptions (with props) is separate from AddMcpServerOptions (with callbackHost, transport, etc.) — you cannot accidentally pass HTTP options to an RPC binding or vice versa.

Architecture

Agent (MCP client)                    McpAgent (MCP server)
  │                                      │
  │ this.addMcpServer("name", env.MCP)   │
  │                                      │
  ├─► RPCClientTransport                 │
  │     │                                │
  │     │ getServerByName(namespace,      │
  │     │   "rpc:name", { props })       │
  │     │                                │
  │     │ stub.handleMcpMessage(msg) ──► handleMcpMessage()
  │     │                            │   │
  │     │                            │   ├─► RPCServerTransport
  │     │                            │   │     .handle(msg)
  │     │                            │   │     → onmessage → MCP Server
  │     │                            │   │     ← send() ← MCP Server
  │     │                            │   │
  │     │ ◄── response ─────────────┘   │
  │     │                                │
  │     ▼ onmessage(response)            │
  │                                      │
  ├─► MCPClientManager                   │
  │     .connect() / .discoverIfConnected()
  │                                      │
  └─► tools available via getAITools()   │

Design decisions

RPCClientTransport accepts a namespace, not a stub. It creates the stub internally during start() via getServerByName from partyserver, which handles setName + props in one call. The transport is a thin wrapper: start() creates the stub, send() calls stub.handleMcpMessage(msg) and routes responses to onmessage.

RPCServerTransport is minimal. Dropped session management (unnecessary — RPC within a DO is inherently session-scoped). Dropped 170 lines of hand-written JSON-RPC validation in favor of JSONRPCMessageSchema.parse() from the MCP SDK. What remains: handle() receives a message, calls onmessage, waits for send() via a microtask-batched promise, returns the response.

Hibernation works automatically. When addMcpServer is called with an RPC binding, the binding name (looked up by scanning env) and props are persisted to the cf_agents_mcp_servers table. On wake-up, _restoreRpcMcpServers() reads these rows, looks up env[bindingName], and reconnects with the same ID. This matches the existing HTTP restore path. Depends on partyserver 0.3.0 which persists this.name to DO storage so it survives RPC-triggered wake-ups.

Deduplication by server name. Calling addMcpServer with the same name returns the existing connection instead of creating duplicates. This applies to both RPC and HTTP. Connection IDs are stable across hibernation (passed via reconnect: { id } to mcp.connect()).

No changes to the Agent base class beyond addMcpServer. The original PR added _resolveRpcBinding, _buildRpcTransportOptions, _buildHttpTransportOptions, and _connectToMcpServerInternal to the ~4300-line index.ts. All removed. The RPC path in addMcpServer is ~30 lines that create transport options and delegate to mcp.connect().

Files changed

File What changed
packages/agents/src/mcp/rpc.ts Rewritten. 609→245 lines. New RPCClientTransport (namespace-based), simplified RPCServerTransport, RPC_DO_PREFIX constant
packages/agents/src/index.ts addMcpServer overloads for RPC/HTTP, AddRpcMcpServerOptions type, name-based dedup, _restoreRpcMcpServers(), _findBindingNameForNamespace()
packages/agents/src/mcp/client.ts getRpcServersFromStorage(), saveRpcServerToStorage(), restoreConnectionsFromStorage skips rpc: URLs, fixed callTool stripping serverId
packages/agents/src/mcp/client-connection.ts RPC transport in MCPTransportOptions union, getTransport("rpc"), auth probe rejects RPC
packages/agents/src/mcp/types.ts 108→26 lines. Removed RPC connection config types (now internal to transport)
packages/agents/src/mcp/index.ts Updated exports, RPC_DO_PREFIX
examples/mcp-rpc-transport/ Full rewrite: Workers AI (no API keys), Kumo/agents-ui, Tailwind
docs/mcp-transports.md New documentation for RPC transport

Test coverage

Unit tests (tests/mcp/transports/rpc.test.ts — 21 tests):

  • RPCClientTransport lifecycle, messaging, error handling, validation
  • RPCServerTransport request/response, batching, notifications, timeout
  • End-to-end via McpAgent (tool listing, tool calling, sequential requests)

E2e tests (tests/mcp/add-rpc-mcp-server.test.ts — 7 tests):

  • Tool discovery via RPC binding
  • Tool execution with correct response content
  • Storage persistence (binding name + props saved)
  • Hibernation restore with stable connection ID
  • Deduplication (same name → same connection)
  • Props passing end-to-end
  • Server removal (connection + storage cleanup)

Notes for reviewers

  • The as unknown as DurableObjectNamespace<McpAgent> cast in addMcpServer is needed because DurableObjectNamespace<T> where T extends McpAgent is not directly assignable to DurableObjectNamespace<McpAgent> due to generic variance. The cast is safe since T is a subclass.
  • RPCClientTransportOptions has a generic <T extends McpAgent> for the namespace type, but the class itself stores DurableObjectNamespace<McpAgent> (the base type) to avoid the infinite type recursion that DurableObjectNamespace<any> causes with getServerByName.
  • The deprecated mcp.connect() method is used intentionally for RPC connections because registerServer() saves transport options to storage via JSON.stringify, and DurableObjectNamespace is not serializable. RPC persistence is handled separately via saveRpcServerToStorage.
  • Requires partyserver 0.3.0+ which persists this.name to DO storage, fixing the "Attempting to read .name before it was set" error on RPC-triggered wake-ups.

@changeset-bot
Copy link

changeset-bot bot commented Oct 13, 2025

🦋 Changeset detected

Latest commit: d9b6246

The changes in this PR will be included in the next version bump.

This PR includes changesets to release 1 package
Name Type
agents Minor

Not sure what this means? Click here to learn what changesets are.

Click here if you're a maintainer who wants to add another changeset to this PR

@pkg-pr-new
Copy link

pkg-pr-new bot commented Oct 13, 2025

Open in StackBlitz

npm i https://pkg.pr.new/cloudflare/agents@565
npm i https://pkg.pr.new/cloudflare/agents/@cloudflare/ai-chat@565
npm i https://pkg.pr.new/cloudflare/agents/@cloudflare/codemode@565
npm i https://pkg.pr.new/cloudflare/agents/hono-agents@565

commit: d9b6246

@mattzcarey mattzcarey marked this pull request as ready for review October 14, 2025 09:30
@mattzcarey mattzcarey marked this pull request as draft October 14, 2025 10:23
@mattzcarey mattzcarey marked this pull request as ready for review October 16, 2025 14:29
@mattzcarey mattzcarey marked this pull request as draft October 17, 2025 16:48
@mattzcarey mattzcarey marked this pull request as ready for review October 17, 2025 19:05
@jpm-halfspace
Copy link

How far is this from being merged?

@whoiskatrin
Copy link
Contributor

How far is this from being merged?

not far, it will go though another round of reviews today

threepointone and others added 5 commits February 22, 2026 11:33
Resolved conflicts in:
- packages/agents/src/index.ts (imports, workflow methods, addMcpServer)
- packages/agents/src/mcp/index.ts (transport exports)
- packages/agents/src/mcp/client-connection.ts (RPC transport + auth provider)
- packages/agents/src/mcp/types.ts (transport type + jurisdiction)
- package-lock.json (regenerated from main)

Kept RPC transport additions (_resolveRpcBinding, _buildRpcTransportOptions,
_buildHttpTransportOptions, _connectToMcpServerInternal) alongside main's
refactored addMcpServer, workflow integration, and new MCP handler APIs.

Fixed McpAgent generic constraints to use Cloudflare.Env instead of unknown.

Co-authored-by: Cursor <cursoragent@cursor.com>
Revamp RPC transport docs and overhaul the mcp-rpc-transport example. Docs: clarify Durable Object binding usage, remove RPC-specific auth wording, simplify examples and explain automatic reconnection and timeout configuration. Example: replace OpenAI usage with Workers AI, add env.d.ts, update package.json (deps, scripts, types), remove legacy normalize.css and utils, migrate styles to Tailwind/Kumo/agents-ui, and rebuild the React client UI with agents-ui/kumo components. Server: simplify McpAgent/McpServer wiring, streamline tool handlers and chat flow, and return UI message streams via Workers AI. Tests: add and update MCP-related tests (including add-rpc-mcp-server.test.ts and other test adjustments). Also include multiple agent package updates to support these changes.
Introduce an RPC transport that lets Agents connect to McpAgent instances in the same Worker using Durable Object bindings (no HTTP). Key changes:

- New feature: addMcpServer now accepts string | DurableObjectNamespace with TypeScript overloads so HTTP and RPC paths are type-safe.
- RPC connections persist across Durable Object hibernation; binding name and props are stored/restored automatically and duplicate connections are deduplicated.
- Rewrote RPCClientTransport/ServerTransport to create DO stubs internally, drop unnecessary session management, and use validated JSONRPC schemas.
- Added RPC_DO_PREFIX constant and discriminated AddRpcMcpServerOptions so props are only available for DO binding.
- Persist/restore helpers added: getRpcServersFromStorage(), saveRpcServerToStorage(); restoreConnectionsFromStorage now skips RPC servers (Agent restores them with env access).
- Fixed MCPClientManager.callTool to strip serverId before calling conn.client.callTool.
- Removed several Agent base helpers that leaked RPC logic and reduced rpc/types files significantly.
- Updated example to Kumo v1.7.0 and adjusted server example; added tests to verify RPC removal behavior.

Also included minor logging cleanup and various refactors to simplify the RPC implementation.
@threepointone
Copy link
Contributor

I took a crack at this and rewrote it all, with tests and such. I also managed to fix some other gnarly stuff along the way (of note, this.name is now available inside alarms, because of enhancements to partyserver).

I (and by I I mean cursor) also rewrote the PR description.

Quite happy, I think this is ready to land. I might just land it tonight.

Copy link
Contributor

@threepointone threepointone left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

rewrote it myself, approved.

Image

@threepointone threepointone merged commit 0e9a607 into main Feb 22, 2026
4 checks passed
@threepointone threepointone deleted the rpc-transport branch February 22, 2026 20:28
@github-actions github-actions bot mentioned this pull request Feb 22, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

feedback requested: define Agent and McpAgent in the same worker

4 participants