Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
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
5 changes: 5 additions & 0 deletions .changeset/optional-mcp-server-peers.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"agents": minor
---

`@modelcontextprotocol/sdk` and `@modelcontextprotocol/server` are now optional peer dependencies, so apps that do not serve MCP no longer install the SDK v1 server stack (`express`, `cors`, `@hono/node-server`, and others). Install `@modelcontextprotocol/server` for `agents/mcp/server`, and `@modelcontextprotocol/sdk` for `McpAgent`, `WorkerTransport`, `createLegacyMcpHandler`, and `agents/experimental/webmcp`. The root `agents` entry and `agents/mcp/client` need neither. If your app imports `@modelcontextprotocol/sdk` without listing it in `package.json`, add it before upgrading. The `agents/mcp` barrel needs `@modelcontextprotocol/sdk` even when you only import client helpers from it, so import those from `agents/mcp/client` instead. See [Creating MCP Servers](https://github.com/cloudflare/agents/blob/main/docs/agents/mcp-servers.md#choosing-an-approach).

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

🔍 Changeset duplicates documentation

The repository requires a single concise description linking to documentation. This multi-sentence release note embeds installation details that belong in the linked guide.

Devin Review


Was this helpful? React with 👍 or 👎 to provide feedback.

2 changes: 2 additions & 0 deletions docs/agents/mcp-client.md
Original file line number Diff line number Diff line change
Expand Up @@ -63,6 +63,8 @@ Install the exact MCP client peer used by this Agents release:
pnpm add agents @modelcontextprotocol/client@2.3.1
```

The MCP client does not need `@modelcontextprotocol/sdk` or `@modelcontextprotocol/server`. Those packages are optional peers used only by the MCP server APIs.

```typescript
import { Agent } from "agents";

Expand Down
2 changes: 1 addition & 1 deletion docs/agents/mcp-servers.md

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

🔍 Legacy server installation guidance is absent

Existing McpAgent users need @modelcontextprotocol/sdk@1.32.1, but the server guide only gives v2 installation steps. A short legacy installation note would help users upgrading existing servers.

Devin Review


Was this helpful? React with 👍 or 👎 to provide feedback.

Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ Build an MCP server on Workers with `createMcpHandler` from `agents/mcp/server`
pnpm add agents @modelcontextprotocol/server@2.3.1 zod
```

Use the exact `@modelcontextprotocol/server` version required by your installed Agents release.
`@modelcontextprotocol/server` is an optional peer dependency of `agents`, so your package manager does not install it for you. Use the exact version required by your installed Agents release.

## Create a server

Expand Down
3 changes: 2 additions & 1 deletion packages/agents/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -509,7 +509,8 @@ export default {
} satisfies ExportedHandler;
```

Install `@modelcontextprotocol/server` alongside `agents`. See
Install `@modelcontextprotocol/server` alongside `agents`; it is an optional
peer, so it is not installed for you. See
[Creating MCP Servers](../../docs/agents/mcp-servers.md) for authentication,
elicitation, and keeping state in bindings.

Expand Down
6 changes: 6 additions & 0 deletions packages/agents/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -109,6 +109,12 @@
"@earendil-works/pi-durable": {
"optional": true
},
"@modelcontextprotocol/sdk": {
"optional": true
},
"@modelcontextprotocol/server": {
"optional": true
Comment on lines +115 to +116

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

🔴 Legacy MCP servers require both peers

Installing only the documented v1 peer makes McpAgent fail to load. McpAgent eagerly imports @modelcontextprotocol/server, which the new optional peer declaration no longer installs.

Learn more

The legacy agents/mcp entry re-exports McpAgent through the compatibility barrel. McpAgent imports v2 server constructors at module initialization for its version check, and the legacy handler barrel also re-exports a compatibility handler that imports v2 code. Marking the v2 peer optional lets npm omit it when users install only the documented v1 SDK, so a Worker importing agents/mcp fails before serving any request.

Example: An application installs agents and @modelcontextprotocol/sdk@1.32.1, then imports McpAgent from agents/mcp. Its Worker cannot load because @modelcontextprotocol/server is absent, although it uses only a v1 server.

Recommended fix: Remove eager v2 imports from the legacy entry, including the instanceof checks in McpAgent and the stateless handler re-export chain. Keep v2-dependent functionality behind the separate v2 entry or an explicit opt-in boundary; test importing and deploying agents/mcp with only the v1 peer installed.

Devin Review


Was this helpful? React with 👍 or 👎 to provide feedback.

},
Comment on lines +115 to +117

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

🔴 Legacy MCP servers require an undocumented peer

When an app installs only the documented v1 SDK, importing McpAgent fails without @modelcontextprotocol/server. McpAgent loads v2 classes at runtime, so the legacy server cannot start.

Learn more

The legacy agents/mcp entry imports McpAgent, which loads the v2 package to reject v2 servers. The same entry re-exports createMcpHandler, whose compatibility dispatcher imports the v2 handler. Marking v2 optional means an installation containing only the documented v1 peer cannot load the legacy entry at all.

Example: An existing app upgrades agents, adds @modelcontextprotocol/sdk@1.32.1 as instructed, and does not install @modelcontextprotocol/server. Loading its McpAgent class fails while resolving the missing v2 package, before it can serve requests.

Recommended fix: Remove eager v2 runtime imports from the legacy entry when v2 is absent, including the instanceof check and compatibility dispatcher. Preserve the v2 overload through an isolated entry or another approach that only loads v2 after the caller selects that path. Test bundling and loading agents/mcp with v1 installed and v2 absent.

Devin Review


Was this helpful? React with 👍 or 👎 to provide feedback.

"@opencode/plugin": {
"optional": true
},
Expand Down
13 changes: 6 additions & 7 deletions packages/agents/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -163,9 +163,8 @@ import {
} from "./observability/tracing/tracer";
import { DisposableStore } from "./core/events";
import { MessageType } from "./types";
import { RPC_DO_PREFIX } from "./mcp/rpc";
import { RPC_DO_PREFIX, type McpRpcAgent } from "./mcp/rpc";
import { ensureMcpServerTable } from "./mcp/client/storage";
import type { McpAgent } from "./mcp";
import { Scheduler, setSchedulerCallbackResolver } from "./schedules/scheduler";
import { Queue, setQueueCallbackResolver } from "./queue/queue";
import type { QueueItem } from "./queue/types";
Expand Down Expand Up @@ -9426,7 +9425,7 @@ export class Agent<
}

private _findBindingNameForNamespace(
namespace: DurableObjectNamespace<McpAgent>
namespace: DurableObjectNamespace<McpRpcAgent>
): string | undefined {
for (const [key, value] of Object.entries(
this.env as Record<string, unknown>
Expand Down Expand Up @@ -9642,7 +9641,7 @@ export class Agent<
* await this.addMcpServer("counter", env.MY_MCP);
* await this.addMcpServer("counter", env.MY_MCP, { props: { userId: "123" } });
*/
async addMcpServer<T extends McpAgent>(
async addMcpServer<T extends McpRpcAgent>(
serverName: string,
binding: DurableObjectNamespace<T>,
options?: AddRpcMcpServerOptions
Expand Down Expand Up @@ -9671,7 +9670,7 @@ export class Agent<
| { id: string; state: typeof MCPConnectionState.READY }
>;

async addMcpServer<T extends McpAgent>(
async addMcpServer<T extends McpRpcAgent>(
serverName: string,
urlOrBinding: string | DurableObjectNamespace<T>,
callbackHostOrOptions?:
Expand Down Expand Up @@ -9833,7 +9832,7 @@ export class Agent<
transport: {
type: "rpc" as TransportType,
namespace:
urlOrBinding as unknown as DurableObjectNamespace<McpAgent>,
urlOrBinding as unknown as DurableObjectNamespace<McpRpcAgent>,
name: normalizedName,
props: rpcOpts?.props
}
Expand All @@ -9855,7 +9854,7 @@ export class Agent<
}

const bindingName = this._findBindingNameForNamespace(
urlOrBinding as unknown as DurableObjectNamespace<McpAgent>
urlOrBinding as unknown as DurableObjectNamespace<McpRpcAgent>
);
if (bindingName) {
this.mcp.saveRpcServerToStorage(
Expand Down
11 changes: 11 additions & 0 deletions packages/agents/src/mcp/client/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,17 @@ import type {
Tool
} from "@modelcontextprotocol/client";
export type { ElicitRequest, ElicitResult } from "@modelcontextprotocol/client";
export {
RPC_DO_PREFIX,
RPCClientTransport,
type McpRpcAgent,
type RPCClientTransportOptions
} from "../rpc";
export type {
MCPElicitationHandler,
MCPElicitationHandlers
} from "./connection";
export type { McpClientOptions } from "../types";
import { type RetryOptions, tryN } from "../../retries";
import { z } from "zod";
import { nanoid } from "nanoid";
Expand Down
86 changes: 86 additions & 0 deletions packages/agents/src/mcp/client/invoker-compat.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,86 @@
import type {
CallToolRequest,
CallToolRequestOptions,
Client,
ListToolsResult
} from "@modelcontextprotocol/client";
import type { Client as LegacyClient } from "@modelcontextprotocol/sdk/client/index.js";
import type {
CallToolResult as LegacyCallToolResult,
ListToolsRequest
} from "@modelcontextprotocol/sdk/types.js";
import type { RequestOptions as LegacyRequestOptions } from "@modelcontextprotocol/sdk/shared/protocol.js";
import {
type CallToolSchemaOrOptions,
normalizeCallToolArguments
} from "./invoker";

// Accepts both SDK v1 and v2 clients. Kept separate from `./invoker` because
// its types depend on `@modelcontextprotocol/sdk`, an optional peer that only
// the x402 entry needs.

export type CompatibleMcpClient = Client | LegacyClient;

export interface BoundMcpClientInvoker {
listTools(
params?: ListToolsRequest["params"],
options?: CallToolRequestOptions
): Promise<ListToolsResult>;
callTool(
params: CallToolRequest["params"],
schemaOrOptions?: CallToolSchemaOrOptions,
options?: CallToolRequestOptions
): Promise<LegacyCallToolResult>;
}

/**
* Bind the original methods before decorators such as x402 replace them. All
* generation-specific calling conventions stay behind this small interface.
*/
export function bindMcpClient(
client: CompatibleMcpClient
): BoundMcpClientInvoker {
const isV2 =
"getProtocolEra" in client &&
typeof (client as { getProtocolEra?: unknown }).getProtocolEra ===
"function";

if (isV2) {
const v2Client = client as Client;
const listTools = v2Client.listTools.bind(v2Client);
const callTool = v2Client.callTool.bind(v2Client);
return {
listTools: (params, options) => listTools(params, options),
callTool: (params, schemaOrOptions, options) => {
const normalized = normalizeCallToolArguments(schemaOrOptions, options);
const pending = normalized.schema
? v2Client.request(
{ method: "tools/call", params },
normalized.schema as never,
normalized.options
)
: callTool(params, normalized.options);
return pending as Promise<LegacyCallToolResult>;
}
};
}

const legacy = client as LegacyClient;
const listTools = legacy.listTools.bind(legacy);
const callTool = legacy.callTool.bind(legacy);
return {
listTools: (params, options) =>
listTools(
params,
options as LegacyRequestOptions
) as Promise<ListToolsResult>,
callTool: (params, schemaOrOptions, options) => {
const normalized = normalizeCallToolArguments(schemaOrOptions, options);
return callTool(
params as Parameters<LegacyClient["callTool"]>[0],
normalized.schema as Parameters<LegacyClient["callTool"]>[1],
normalized.options as LegacyRequestOptions
) as Promise<LegacyCallToolResult>;
}
};
}
93 changes: 13 additions & 80 deletions packages/agents/src/mcp/client/invoker.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,40 +2,25 @@ import type {
CallToolRequest,
CallToolRequestOptions,
CallToolResult,
Client,
ListToolsResult
Client
} from "@modelcontextprotocol/client";
import type { Client as LegacyClient } from "@modelcontextprotocol/sdk/client/index.js";
import type {
CallToolResult as LegacyCallToolResult,
CallToolResultSchema,
CompatibilityCallToolResultSchema,
ListToolsRequest
} from "@modelcontextprotocol/sdk/types.js";
import type { RequestOptions as LegacyRequestOptions } from "@modelcontextprotocol/sdk/shared/protocol.js";

export type LegacyCallToolResultSchema =
| typeof CallToolResultSchema
| typeof CompatibilityCallToolResultSchema;
/**
* A v1 result schema such as `CallToolResultSchema` or
* `CompatibilityCallToolResultSchema` from `@modelcontextprotocol/sdk`.
*
* Typed structurally so the MCP client (reachable from the root `agents`
* entry) does not depend on `@modelcontextprotocol/sdk`, which is an optional
* peer.
*/
export type LegacyCallToolResultSchema = {
parse(data: unknown): unknown;
};

export type CallToolSchemaOrOptions =
| LegacyCallToolResultSchema
| CallToolRequestOptions;

export type CompatibleMcpClient = Client | LegacyClient;

export interface BoundMcpClientInvoker {
listTools(
params?: ListToolsRequest["params"],
options?: CallToolRequestOptions
): Promise<ListToolsResult>;
callTool(
params: CallToolRequest["params"],
schemaOrOptions?: CallToolSchemaOrOptions,
options?: CallToolRequestOptions
): Promise<LegacyCallToolResult>;
}

function isLegacyResultSchema(
value: CallToolSchemaOrOptions | undefined
): value is LegacyCallToolResultSchema {
Expand All @@ -44,7 +29,7 @@ function isLegacyResultSchema(
);
}

function normalizeCallToolArguments(
export function normalizeCallToolArguments(
schemaOrOptions?: CallToolSchemaOrOptions,
options?: CallToolRequestOptions
): {
Expand Down Expand Up @@ -76,55 +61,3 @@ export function callV2Tool(
}
return client.callTool(params, normalized.options);
}

/**
* Bind the original methods before decorators such as x402 replace them. All
* generation-specific calling conventions stay behind this small interface.
*/
export function bindMcpClient(
client: CompatibleMcpClient
): BoundMcpClientInvoker {
const isV2 =
"getProtocolEra" in client &&
typeof (client as { getProtocolEra?: unknown }).getProtocolEra ===
"function";

if (isV2) {
const v2Client = client as Client;
const listTools = v2Client.listTools.bind(v2Client);
const callTool = v2Client.callTool.bind(v2Client);
return {
listTools: (params, options) => listTools(params, options),
callTool: (params, schemaOrOptions, options) => {
const normalized = normalizeCallToolArguments(schemaOrOptions, options);
const pending = normalized.schema
? v2Client.request(
{ method: "tools/call", params },
normalized.schema as never,
normalized.options
)
: callTool(params, normalized.options);
return pending as Promise<LegacyCallToolResult>;
}
};
}

const legacy = client as LegacyClient;
const listTools = legacy.listTools.bind(legacy);
const callTool = legacy.callTool.bind(legacy);
return {
listTools: (params, options) =>
listTools(
params,
options as LegacyRequestOptions
) as Promise<ListToolsResult>,
callTool: (params, schemaOrOptions, options) => {
const normalized = normalizeCallToolArguments(schemaOrOptions, options);
return callTool(
params as Parameters<LegacyClient["callTool"]>[0],
normalized.schema,
normalized.options as LegacyRequestOptions
) as Promise<LegacyCallToolResult>;
}
};
}
9 changes: 4 additions & 5 deletions packages/agents/src/mcp/client/x402.ts
Original file line number Diff line number Diff line change
Expand Up @@ -26,12 +26,11 @@ import type {
CallToolResult,
ToolAnnotations
} from "@modelcontextprotocol/sdk/types.js";
import {
bindMcpClient,
type CallToolSchemaOrOptions,
type CompatibleMcpClient,
type LegacyCallToolResultSchema
import type {
CallToolSchemaOrOptions,
LegacyCallToolResultSchema
} from "./invoker";
import { bindMcpClient, type CompatibleMcpClient } from "./invoker-compat";
import type { z, ZodRawShape } from "zod";

// v2 imports from @x402/core
Expand Down
Loading