Cloudflare Agents MCP 客户端能力实战:在原生 Durable Object 中组合 MCPClientManager Cloudflare Agents MCP 客户端能力实战在原生 Durable Object 中组合 MCPClientManager【免费下载链接】agentsBuild and deploy AI Agents on Cloudflare项目地址: https://gitcode.com/GitHub_Trending/agents1/agents导读本文基于开源仓库 agents1/agentsCloudflare Agents 项目中的官方示例 examples/next/mcp-client完整讲解如何在不继承Agent基类的前提下把一个普通的 CloudflareDurableObject通过生命周期Lifecycle能力机制装配成持久化的 MCPModel Context Protocol客户端它能够接收任意 Streamable HTTP 形式的 MCP 端点、在弹窗中完成 OAuth 授权、持久化 HTTP 长连接并在对象被驱逐eviction后自动恢复最终以 JSON 参数调用服务器发现的工具。读完本文你将掌握MCPClientManagerLifecycle.install()的组合模式、OAuth 回调路由的注册规则、Durable Object RPC 边界下的生命周期启动约定以及完整的可复制 HTTP API 与前端交互实现。一、示例概览一个不依赖 Agent 基类的 MCP 客户端仓库中的示例examples/next/mcp-client是一个 Vite React 的前后端一体演示Worker 入口把请求通过routeAgentRequest()路由到名为McpClientObject的 Durable Object该对象内部只组合了MCPClientManager这一生命周期能力并没有继承Agent。其核心声明只有寥寥几行见 src/server.tsexport class McpClientObject extends DurableObjectEnv { readonly mcp new MCPClientManager(mcp-client-object, 1.0.0); readonly lifecycle Lifecycle.install(this).use(this.mcp); ... }从官方文档 docs/agents/mcp-client.md 可以确认MCPClientManager本身就是一个lifecycle capability生命周期能力类可以直接继承平台DurableObject并安装该管理器而不必扩展Agent。Agent内部其实也是直接安装同一个能力对象因此现有的this.mcp、addMcpServer()、removeMcpServer()、getMcpServers()等 API 在该框架中依然可用。生命周期Lifecycle为管理器做了什么Lifecycle.install(this).use(this.mcp)把管理器注册进宿主对象的生命周期由生命周期自动完成三件关键工作初始化 Schema在onStart()阶段初始化管理器的数据库表结构与持久化状态之后宿主才开始处理工作恢复被驱逐后的 HTTP 连接Durable Object 被逐出内存后对象重新冷启动时生命周期会恢复此前持久化的 MCP 连接回调 URL 拦截在宿主onRequest()之前先拦截已注册的 OAuth 回调 URL交给管理器完成 OAuth 流程。因此示例中onStart()与onRequest()两个钩子只做演示相关的收尾工作MCP 相关的底层逻辑完全交给生命周期与管理器onStart(): void { configureOAuthPopup(this.mcp); } onRequest(request: Request): PromiseResponse { return handleDemoRequest(this.mcp, request); }关键约定不要手动调用生命周期钩子官方文档明确警告不要手动调用这些钩子。但存在一个例外场景——原生 Durable Object RPC 方法不走fetch生命周期不会自动启动因此 RPC 方法内部需要先显式await this.lifecycle.start()。本文第四节会结合getCatalog()详细展开这一点。二、运行与部署本地启动进入示例目录执行pnpm install pnpm run start其中start脚本定义为vite dev见 package.json由 Vite 同时承载前端与 Worker。部署到 Cloudflarepnpm run deploy # 等价于 vite build wrangler deploy示例还提供了pnpm run typechecktsc --noEmit与pnpm run typeswrangler types env.d.ts --include-runtime false两个辅助脚本。wrangler 配置解读示例的 wrangler.jsonc 是最小但完整的 Durable Object 静态资源配置{ name: next-mcp-client-capability, main: src/server.ts, compatibility_date: 2026-06-11, compatibility_flags: [nodejs_compat], assets: { not_found_handling: single-page-application, run_worker_first: [/agents/*] }, durable_objects: { bindings: [ { name: McpClientObject, class_name: McpClientObject } ] }, migrations: [ { tag: v1, new_sqlite_classes: [McpClientObject] } ], observability: { enabled: true } }durable_objects.bindings把McpClientObject类绑定到同名引用Worker 入口用它创建/寻址 Durable Objectmigrations.new_sqlite_classes声明该 Durable Object 使用SQLite 存储——MCPClientManager的服务端元数据server 列表、OAuth 回调 URL、client_id 等正是持久化在 SQLite 表中核心实现见 packages/agents/src/mcp/client/index.ts 中saveServerToStorage等存储相关方法assets配置把/agents/*的请求优先交给 Worker 处理run_worker_first其余路径回退到 SPA 静态资源not_found_handling: single-page-application——这正是 MCP 对象 API 与 React 页面共存的配置基础。三、HTTP API 设计以请求路径驱动 OAuth 回调完整路由表示例对象暴露的 HTTP API 如下来自 README.mdGET /agents/mcp-client-object/:instance POST /agents/mcp-client-object/:instance/connect POST /agents/mcp-client-object/:instance/tools/call DELETE /agents/mcp-client-object/:instance/servers/:id其中:instance是浏览器本地生成的随机对象名详见第五节它保证每个浏览器拥有互相隔离的服务器目录与 OAuth 凭据。connect注册、连接与回调 URL 推导connect路由在 demo-api.ts 中实现是理解整套设计的关键const id normalizeServerId(input.name); if (mcp.listServers().some((server) server.id id)) { await mcp.removeServer(id); } const callbackUrl new URL(request.url); callbackUrl.pathname callbackUrl.pathname.replace( /\/connect\/?$/, /callback ); callbackUrl.search ; await mcp.registerServer(id, { name: input.name, url: input.url, callbackUrl: callbackUrl.href, transport: { type: auto } }); const connection await mcp.connectToServer(id); if (connection.state connected) { const discovery await mcp.discoverIfConnected(id); return Response.json({ id, connection, discovery }); } return Response.json({ id, connection });这个实现同时体现了管理器的三条设计约定回调 URL 由当前请求推导/connect路径被替换为/callback即POST .../connect的同一对象路径GET .../callback就是 OAuth 回调入口。README 强调管理器不强制任何回调路由形状你在registerServer()里传入什么精确 URL就把那个请求路由到同一个 Durable Object 即可——管理器持久化该 URL并且只会拦截来源与路径都匹配的回调请求。normalizeServerId生成稳定的存储主键名称经过规范化后作为 server id同时作为 SQLite 表cf_agents_mcp_servers的主键、AI SDK 工具名嵌入片段以及mcpConnections映射的键。核心实现 packages/agents/src/mcp/client/index.ts 的规则是转小写 → 非[a-z0-9_-]字符序列替换为单个-→ 折叠连续-并去掉首尾-/_→ 空串或非字母开头时加id-前缀 → 截断到MCP_SERVER_ID_MAX_LENGTH64字符。例如normalizeServerId(GitHub MCP!)得到github-mcpnormalizeServerId(42-things)得到id-42-things。连接与发现分离connectToServer()只负责建立连接可能返回{ state: authenticating, authUrl }等待授权也可能直接connected连接成功后调用discoverIfConnected()拉取服务器能力清单。registerServer()本身只做注册与持久化README 明确建议新代码分别调用registerServer()与connectToServer()。连接状态机与工具调用/tools/call路由demo-api.ts在调用前检查连接状态const connection mcp.mcpConnections[input.serverId]; if (!connection) { return Response.json({ error: MCP server is not connected }, { status: 404 }); } if (connection.connectionState ! ready) { return Response.json( { error: MCP server is not ready (${connection.connectionState}) }, { status: 409 } ); } const result await mcp.callTool({ serverId: input.serverId, name: input.name, arguments: input.arguments });前端StatusBadge见 client.tsx按状态着色ready绿色、failed红色、authenticating黄色、其余蓝色与后端的connectionState字段一一对应。工具调用失败时后端返回 502 并把底层错误消息透传给前端展示。输入校验connect与tools/call都先经过严格的运行时校验parseConnectInput/parseToolCallInput名称与 URL 非空、URL 必须是http:或https:协议、参数必须是 JSON 对象否则返回 400。DELETE /servers/:id则直接从路径尾部提取并decodeURIComponent解码出 id 后调用removeServer(id)返回 204。安全纵深SSRF 防护需要留意的是管理器的registerServer()在源码层面还内置了SSRF 防护isBlockedUrl()会拒绝私网/内网地址的连接见 packages/agents/src/mcp/client/index.ts包括明确封锁的主机名集合0.0.0.0、[::]、metadata.google.internal私有/保留 IPv4 段10.0.0.0/8、172.16.0.0/12、192.168.0.0/16、169.254.0.0/16link-local / 云元数据、0.0.0.0/8IPv6 私有段fc00::/7ULA、fe80::/10link-local按/10边界正确匹配fe80–febf、以及::ffff:IPv4-mapped IPv6环回地址127.x.x.x、::1则被有意放行以支持本地开发。被拦截时会抛出Blocked URL: ... — MCP client connections to private/internal addresses are not allowed。四、RPC 边界为什么getCatalog()要先lifecycle.start()示例中McpClientObject除了onRequest()之外还暴露了一个原生 RPC 方法getCatalog()/** Native RPC bypasses fetch, so start the lifecycle explicitly. */ async getCatalog(): PromiseMcpCatalog { await this.lifecycle.start(); return getMcpCatalog(this.mcp); }这是整个示例最值得深入理解的细节经由fetch的 HTTP 请求生命周期会自动为宿主启动并拦截回调因此onRequest()内部无需手动启动而Durable Object 原生 RPC 走的是RpcStub通道完全绕过fetch生命周期无法自动介入。因此 RPC 方法体内部必须显式await this.lifecycle.start()确保管理器的 schema 已初始化、持久化连接已恢复之后再安全地读取mcp.listServers()/mcp.listTools()。getMcpCatalog()demo-api.ts把管理器内部状态投影成 UI 可序列化的目录结构servers: mcp.listServers().map((server) ({ id: server.id, name: server.name, url: server.server_url, state: mcp.mcpConnections[server.id]?.connectionState ?? not-connected, authUrl: server.auth_url ?? undefined })), tools: mcp.listTools()这里也展示了mcpConnections映射与listServers()的分工前者是内存中的实时连接状态后者是持久化的服务器元数据。关于 RPC 服务器的补充约定官方文档还提到一个 RPC 相关的配置细节如果目录中可能出现RPC 类型的服务器而非纯 HTTP则需要把 Durable Object 环境传给管理器以便对象被唤醒wake后能解析持久化的 binding 名称readonly mcp new MCPClientManager(my-object, 1.0.0, { env: this.env });若存在 RPC 记录却没有传入env启动时会输出一条警告日志指明具体服务器并且不会重建该连接。示例本身是 HTTP-only因此无需传env。五、前端交互实例隔离、OAuth 弹窗与轮询刷新React 界面由 client.tsx 实现包含几个值得复用的模式。按浏览器隔离实例const INSTANCE_KEY mcp-client-instance; const instanceName localStorage.getItem(INSTANCE_KEY) ?? crypto.randomUUID(); localStorage.setItem(INSTANCE_KEY, instanceName); const OBJECT_PATH /agents/mcp-client-object/${encodeURIComponent(instanceName)};首次加载时生成一个crypto.randomUUID()作为随机对象名存入localStorage。之后所有 API 请求都指向/agents/mcp-client-object/:instance。由于 Durable Object 以名字寻址每个浏览器事实上拥有一个独立命名的对象实例从而获得互相隔离的服务器目录和 OAuth 凭据存储。注意路径段经过了encodeURIComponent对象名可以安全地作为 URL 段使用。OAuth 授权流程两步弹窗const popup window.open(about:blank, _blank, OAUTH_WINDOW_FEATURES); // 640x760 const response await fetch(${OBJECT_PATH}/connect, { method: POST, ... }); const result (await response.json()) as ConnectResult; if (result.connection.authUrl) { if (popup) { popup.opener null; popup.location.assign(result.connection.authUrl); // 第一步跳到授权页 } }第一步提交连接后若服务器返回authUrl即authenticating状态就把该 URL 加载进新开的about:blank弹窗。window.open时传入noopener以外的受限特性并在跳转前清空popup.opener以增强安全性回调完成OAuth 服务商把用户重定向回对象自身的/callback路径。此时生命周期已把该 URL 注册给管理器管理器拦截回调、完成 code 交换。后端的configureOAuthPopup()demo-api.ts为回调返回一个自动window.close()的 HTML 页面MCP authorization complete — You can close this window.并附带严格 CSPdefault-src none; script-src unsafe-inline; style-src none让弹窗在授权完成后自关闭恢复授权入口目录中authUrl非空的服务器会渲染为“Authorization required”卡片提供“Authorize”按钮用第二个弹窗noopener直接打开授权页便于对已授权但状态为authenticating的服务器继续完成流程。轮询刷新目录useEffect(() { void refresh(); const timer window.setInterval(() void refresh(), 2_500); return () window.clearInterval(timer); }, [refresh]);前端每 2.5 秒拉取一次目录GET OBJECT_PATH因此授权状态从authenticating变为ready、新工具被discoverIfConnected()发现后UI 会在数秒内自动更新无需手动刷新。工具卡片ToolCard为每个发现的工具展示名称、描述、所属serverId徽章、可展开的inputSchemaJSON 格式化预览以及“Arguments (JSON)”文本框。调用时前端把{ serverId, name, arguments }POST 到/tools/call结果以 JSON 美化输出output.isError true时按错误态红色渲染。这种“schema 可展开 JSON 参数直接填写”的交互本质上就是 MCP 工具发现/调用协议在浏览器里的最小可视实现。六、从示例到生产与Agent方式的对照官方文档 docs/agents/mcp-client.md 给出了与示例互补的另一种接入路径——直接继承Agent使用更简洁的封装 APIimport { Agent } from agents; export class MyAgent extends Agent { async onRequest(request: Request) { const result await this.addMcpServer( github, https://mcp.github.com/mcp ); if (result.state authenticating) { return Response.redirect(result.authUrl); // 服务器需要 OAuth } const state this.getMcpServers(); console.log(Connected! ${state.tools.length} tools available); return new Response(MCP server connected); } }关键依赖对齐modelcontextprotocol/client的精确版本需要与本 Agents 版本匹配示例 package.json 固定为2.0.0。两种接入方式的取舍可以归纳为维度原生 DurableObject Lifecycle本文示例继承Agent官方 Quick Start继承DurableObjectEnvAgentEnvMCP 管理器手动new MCPClientManager(...)并Lifecycle.install(this).use(...)框架内置this.mcp常用 APIregisterServer/connectToServer/callTooladdMcpServer/removeMcpServer/getMcpServers生命周期由Lifecycle托管RPC 方法需手动lifecycle.start()由Agent托管适用只想给现有 Durable Object 加 MCP 能力、不引入 Agent 语义需要 Agent 的对话、会话、调度等完整能力无论哪种方式底层都是同一个MCPClientManager能力对象——这也是该框架“能力可组合composable capabilities”设计的直接体现Lifecycle作为能力运行容器MCPClientManager作为能力本身宿主只需声明式地use()即可获得 schema 初始化、连接恢复、OAuth 回调拦截三项自动服务。七、小结examples/next/mcp-client是一个“小而全”的参考实现串联起了 Cloudflare Agents MCP 客户端能力的完整链路装配DurableObject Lifecycle.install().use(MCPClientManager)能力即插即用持久化SQLitenew_sqlite_classesregisterServer()落库驱逐后可恢复连接路由routeAgentRequest()把请求送达同名对象回调 URL 由/connect推导为/callback管理器不限制路由形状授权configureOAuthCallback 前端双弹窗完成 OAuth回调由生命周期抢先拦截RPC 边界绕过fetch的 RPC 方法需显式await this.lifecycle.start()安全normalizeServerId保证存储/工具名安全SSRF 防护拒绝私网地址交互crypto.randomUUID()实例隔离 2.5s 轮询 工具 schema 可视化构成可直接借鉴的浏览器端 MCP 控制台。如果希望进一步深入底层实现建议顺藤摸瓜阅读 packages/agents/src/mcp/client/index.ts管理器核心、normalizeServerId与 SSRF 防护、packages/agents/src/lifecycle/capability.ts能力运行容器的服务接口以及 docs/agents/mcp-client.md官方完整的 MCP 客户端文档含Agent方式、OAuth、重试与恢复等进阶主题。【免费下载链接】agentsBuild and deploy AI Agents on Cloudflare项目地址: https://gitcode.com/GitHub_Trending/agents1/agents创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考