
Cloudflare Codemode Connectors 全解析用统一 Connector 类把 MCP、OpenAPI 与 AI SDK 工具接入沙箱【免费下载链接】agentsBuild and deploy AI Agents on Cloudflare项目地址: https://gitcode.com/GitHub_Trending/agents1/agentsConnector连接器是 codemode 沙箱体系中一个能力只写一遍的统一答案无论能力来自 MCP 服务器、OpenAPI 规范、AI SDK 工具集还是自有服务都封装为一个 Connector 类运行时把它暴露给模型成为一个类型化全局如github.list_pull_requests(...)模型侧协议永不变化变化的只是你继承的基类。读完本文你将掌握CodemodeConnector基类的完整编写面name/instructions/tools/tool、派生 RPC 表面、per-execution 与 per-pass 资源生命周期、replay: reexecute重放策略以及McpConnector、OpenApiConnector、ToolSetConnector三种内置基类的源码级原理与实战用法。Connector 的定位能力接入层只有一个答案在 codemode 的世界里模型不是通过几十个工具描述来理解外部系统而是写 TypeScript 调用沙箱里的类型化全局。Connector 就是这个全局的来源。官方文档 docs/codemode/connectors.md 给出了它的哲学there should be one way to add a capability. Whether the source is an MCP server, an OpenAPI spec, an AI SDK toolset, or your own service, the answer is the same — wrap it in a connector class, put it in a runtime, and the model sees it as a typed global.Connector 只负责定义与执行工具而运行时Runtime负责状态、审批与回滚。从源码结构看这一职责切分体现在三个模块上CodemodeConnector基类与工具类型定义在 packages/codemode/src/connectors/base.ts执行日志、暂停/审批/回滚与重放逻辑在 packages/codemode/src/runtime.ts详见 docs/codemode/runtime.md审批流在 docs/codemode/approvals.md。沙箱里每次 connector 调用都会先经过运行时决策replay / execute / pause再落到 connector 的executeTool因此持久化日志始终是唯一事实来源。一个 Connector 只回答三个问题模型使用什么全局名name、模型获得什么引导instructions、有哪些工具tools。每个工具自带文档、schema、审批要求、执行函数和可选的撤销函数——关于一个工具的一切都放在一个地方。基类CodemodeConnector三个问题 一个全息工具记录CodemodeConnectorEnv是一个继承自WorkerEntrypoint的抽象类见 packages/codemode/src/connectors/base.ts意味着每个 connector 实例天然携带ctx与env方便直接读取 Worker 环境绑定。构造函数特意放宽参数类型为DurableObjectState | ExecutionContext这样在 Agent/Durable Object 内部可以直接new GithubConnector(this.ctx, this.env, conn)而无需类型断言——源码注释里明确说明这是为了让this.ctx一个DurableObjectState可以直接传入。文档给出了一个最小自研 Connector 的完整骨架import { CodemodeConnector } from cloudflare/codemode; export class MyConnector extends CodemodeConnectorEnv { name() { return myService; } protected instructions() { return Use for interacting with My Service.; } protected tools() { return { listItems: { description: List all items., inputSchema: { type: object, properties: { limit: { type: number } } }, execute: (args) this.env.MY_SERVICE.list(args) }, createItem: { description: Create an item., inputSchema: { type: object, properties: { title: { type: string } }, required: [title] }, requiresApproval: true, execute: (args) this.env.MY_SERVICE.create(args), revert: (_args, result) this.env.MY_SERVICE.delete(result.id) } }; } }Authoring surface编写面方法必填用途name()是沙箱中的唯一命名空间github、stripe等模型通过这个全局名调用instructions()否展示给模型的引导说明会随类型块一起暴露给模型tools()是一条记录一个工具派生型 Connector 会自动替你生成tool(name, t)否装饰钩子——用于调整你没内联编写的工具如补上审批、revert、文档在基类实现中tools()返回的记录会先整体经过resolvedTools()处理逐个调用tool(name, t)装饰钩子后再使用见 base.ts。这意味着无论工具是内联手写还是由 MCP/OpenAPI 派生而来tool()都是统一的后置装饰入口。每个工具一个记录承载全部语义ConnectorTool的完整类型定义在源码 base.tstype ToolExecuteContext { executionId: string }; type ConnectorTool { description?: string; inputSchema?: JSONSchema7; // defaults to an open object outputSchema?: JSONSchema7; requiresApproval?: boolean; // omit to execute immediately execute: ( args: unknown, ctx?: ToolExecuteContext ) Promiseunknown | unknown; revert?: ( args: unknown, result: unknown, ctx?: ToolExecuteContext ) Promisevoid | void; };几个关键点execute/revert的第二个参数ctx携带executionId这个 id 在一次 run 的 pause/resume 全程保持稳定是作用域为整次执行的资源浏览器会话、数据库事务、临时工作区的首选键。ctx是可选类型为了保证 AI SDK ToolSet 形状兼容因此真实调用中请用ctx?.executionId读取而运行时在真实调用时总是会传入。requiresApproval: true会让 run 在调用到该工具时暂停、等待审批见 docs/codemode/approvals.mdrevert则启用回滚见 docs/codemode/runtime.md。两者都不设置的工具立即执行其结果记入持久化日志。schema 兼容三种形状基类在toolInputSchema()中会依次尝试inputSchema与parameters字段兼容 AI SDK v4/v5 两种 schema 约定v5 可能是 Zod schema 而非 JSON Schema都识别不出时回退为开放对象{ type: object }见 base.ts。AI SDK ToolSet 直接兼容由于ConnectorTool与 AI SDK 工具形状兼容一个现成的ToolSet可以直接从tools()返回export class LinearConnector extends CodemodeConnectorEnv { name() { return linear; } protected tools() { return linearTools; // an AI SDK ToolSet } }仓库还提供了更完整的ToolSetConnector位于 packages/codemode/src/connectors/toolset.ts它把整个 ToolSet 归入一个命名空间默认tools即tools.getWeather(...)并做了两件关键过滤只暴露带execute函数的工具——客户端侧/由 provider 执行的工具会被跳过并在控制台告警因为广告一个沙箱调不了的方法只会让模型走进死胡同见 toolset.tsneedsApproval映射为requiresApprovaltrue或函数值函数无法预评估沙箱参数都会保守地要求审批走的是运行时的持久化 pause/approve/resume 流程而不是 AI SDK 的逐调用审批见 toolset.ts。派生 RPC 表面从工具记录自动推导的线上协议代理工具proxy tool通过 Workers RPC 与 connector 通信。你不需要实现这些方法——基类从 tools 记录自动推导出整条线上协议见 base.tsdescribe()— 返回name、instructions、每个工具的 JSON Schema 描述符descriptors与注解annotations。值得注意的是它在这里就做了校验requiresApproval与replay: reexecute不能组合否则直接抛错理由是已审批的动作是副作用必须被记录不能重新执行见 base.tsgetTypeScriptTypes()— 为 describe 结果生成 TypeScript 声明并把模板里的declare const codemode替换为declare const ${this.name()}从而让沙箱类型块对应到正确的全局名executeTool(method, args, ctx?)— 分发到工具的execute。实现上用return await而非return这是为了避免 workerd 在 promise 收养过程中产生误报的 unhandled rejection见 base.tsrevertAction(method, args, result, ctx?)— 分发到工具的revert并返回布尔值表示是否真的执行了撤销读操作和没有revert的工具返回false运行时据此只把真正撤销过的日志条目标记为reverted见 base.ts。一次执行一个资源executionIddisposeExecution有些 connector 拥有必须存活整次 run 的资源——浏览器/CDP 会话、数据库事务、临时工作区。生命周期契约由两部分构成execute(args, ctx)收到executionId首次使用时按 id 惰性获取或重连资源。由于 id 跨 pause/resume 稳定即使 run 因等待审批暂停、又在后续某个全新的 Worker 调用中被恢复资源依然可寻址。disposeExecution(executionId, status)在 run 达到终态时被调用用于拆除资源。文档中的 BrowserConnector 示例完整展示了这一对钩子export class BrowserConnector extends CodemodeConnectorEnv { name() { return browser; } protected tools() { return { navigate: { description: Open a URL in the runs browser session., execute: async ({ url }, ctx) { const session await this.sessionFor(ctx?.executionId); return session.goto(url); } } }; } // Fires on completed / error / rejected / rolled_back — never on pause. override async disposeExecution( executionId: string, _status: ExecutionEndStatus ) { await this.closeSessionFor(executionId); } }disposeExecution故意不在暂停时触发——暂停的 run 可能恢复作用域为整次 run 的资源必须越过暂停存活。它会对运行时里每一个connector 在每个终态转移时触发不只触发 run 用过的那个所以什么都没打开的 connector 应该发现没有东西可关。status的取值在 packages/codemode/src/connectors/types.ts 有精确定义ExecutionEndStatus触发时机completedrun 完成并返回结果errorrun 抛错或触发重放发散replay divergencerejected待审批动作被用户拒绝rolled_backrun 已应用的效果被回滚实现规则默认是 no-op没有 per-run 状态的 connector 完全不用管它幂等。每个终态转移都会触发同一个 execution 可能被触发多次例如一个completed的 run 随后被 rollback 就触发两次第二次应当直接 no-op。不依赖实例内存。它可能运行在打开资源的不同 connector 实例上宿主可能按请求重建 connector打开资源的那次 pass 可能已经休眠需要的东西要从按executionId键控的持久化存储里读取。绝不抛错。拆除失败不能让一个已完成的 run 变成失败运行时忽略该钩子的 rejection。放弃暂停中的 run想放弃一个暂停的 run 并释放资源reject待审批动作即可——那是一次终态转移会触发disposeExecution。没人应答的过期暂停 run 可以用runtime.expirePaused批量回收见 docs/codemode/runtime.md。注意rollback只有在真正撤销了效果时才触发disposeExecution一个暂停的只读 run 不会被 rollback 终止。每次 pass 一个资源onPassEndonPassEnd(executionId, status)在每一次执行 pass 结束时触发——包括以暂停收尾、disposeExecution刻意不触发的 pass。用它释放 per-pass 资源打开的 socket、租约、内存缓存条目。status是ExecutionEndStatus或paused在终态 pass 上先触发onPassEnd再触发disposeExecution。// Keep the browser session (per-execution) across a pause, but close the // CDP socket (per-pass) — the resume pass reconnects. override async onPassEnd(executionId: string, _status: PassEndStatus) { await this.closeSocketFor(executionId); }PassEndStatus在源码 types.ts 定义为ExecutionEndStatus | paused注释点明了它的设计意图pass 结束不等同于 execution 结束per-pass 资源socket应释放而 per-execution 资源session必须保留。它与disposeExecution遵守相同的三条实现规则幂等、无实例内存、绝不抛错。AI SDK ToolSet 提示当tools()返回 AI SDKToolSet时codemode 会把{ executionId }作为工具的第二个execute参数传入——这个位置恰好是 AI SDK 留给自身 call options 的槽位。在 codemode 内部这些 options 不会被填充所以按 AI SDK 的toolCallId/messages编写的工具在这里收不到它们。重放策略replay: reexecute默认情况下每次调用的结果都会被记入持久化日志并在恢复时重放。对于结果很大且重新计算很便宜的工具可以配置replay: reexecute选择退出protected tools(): ConnectorTools { return { read_file: { description: Read a file from the workspace., replay: reexecute, // ephemeral: re-runs on replay, result never stored execute: ({ path }) this.fs.read(path) } }; }该调用仍然会被记录用于排序和发散检测但其结果不会进入持久化日志重放时重新执行而非重放记录值。只建议用于幂等读取——每次恢复 pass 都会再跑一次且值可能在不同 pass 间合理变化代码必须容忍。replay: reexecute不能与requiresApproval组合已审批的副作用必须被记录绝不能重放执行。源码层面这条策略通过ToolAnnotations.replay?: log | reexecute表达见 types.ts基类describe()会把replay reexecute的工具写进 annotations运行时据此生成ephemeral日志条目ephemeral?: boolean详见 docs/codemode/runtime.md。McpConnector把一个 MCP 服务器包装成沙箱全局McpConnectorEnv见 packages/codemode/src/connectors/mcp.ts包装一个 MCP 连接。每个 MCP 工具成为工具记录中的一个条目执行时通过connection.client.callTool()走真实调用。工具名会被清洗sanitize成合法的 JS 标识符。你只需实现createConnection()对派生出来的工具做审批/revert 装饰则用tool(name, t)钩子import { McpConnector, type McpConnectionLike, type ConnectorTool } from cloudflare/codemode; export class GithubConnector extends McpConnectorEnv { constructor( ctx: ExecutionContext, env: Env, private conn: McpConnectionLike ) { super(ctx, env); } name() { return github; } protected instructions() { return Use for GitHub operations.; } protected createConnection() { return this.conn; } protected tool(name: string, t: ConnectorTool): ConnectorTool { if (name create_issue) { return { ...t, requiresApproval: true, revert: (_args, result) this.closeIssue(result) }; } return t; } }方法用途createConnection()必填。返回一个 MCP 连接McpConnectionLiketoolName(tool)可覆盖。定制 MCP 工具名到沙箱标识符的映射方式从源码看McpConnector做了几件值得注意的事连接缓存getConnection()用#connectionPromise惰性缓存createConnection()的结果fetchTools()优先用连接上的tools否则回退到fetchTools()见 mcp.ts指令合并describe()会把连接自带的instructions与子类instructions()合并输出即连接层引导 业务层引导可以叠加见 mcp.ts重名冲突检测两个 MCP 工具清洗后映射到同一个沙箱名时直接抛错并提示OverridetoolName()to disambiguate见 mcp.ts结果归一化unwrapMcpResult依次提取toolResult→ 抛错isError→structuredContent→ 纯文本内容能 JSON.parse 就解析否则返回文本把 MCP SDK 复杂的返回结构拍平成模型能直接用的值见 mcp.ts。沙箱内模型看到的是每个 MCP 工具一个方法github.list_pull_requests({ owner, repo, state }); github.search_issues({ query });OpenApiConnector一次主机侧推导每个 operation 一个类型化工具OpenApiConnectorEnv见 packages/codemode/src/connectors/openapi.ts包装一份 OpenAPI 规范。基类在主机侧只读取一次 spec为每个 operation 派生一个类型化工具模型直接按操作调用如stripe.CreatePaymentIntent({ amount, currency })并可通过codemode.search/describe发现——且主机侧推导消耗零 prompt token。你只需覆盖两个方法spec()返回 OpenAPI 文档用于派生 operationsrequest()执行一次带认证的请求。import { OpenApiConnector, type OpenApiRequestOptions } from cloudflare/codemode; export class StripeConnector extends OpenApiConnectorEnv { name() { return stripe; } protected instructions() { return Use for Stripe payments. Call the per-operation tools directly.; } protected spec() { return stripeOpenApiSpec; } protected request(options: OpenApiRequestOptions) { return fetch(https://api.stripe.com${options.path}, { method: options.method ?? GET, headers: { Authorization: Bearer ${this.env.STRIPE_KEY} }, body: options.body ? JSON.stringify(options.body) : undefined }).then((r) r.json()); } }方法用途spec()必填。返回 OpenAPI spec 文档可异步。operations 从它推导request(options)必填。执行带认证的请求接收{ path, method, params, body, headers }exposeSpec()可选。返回true时额外把原始spec文档暴露成一个工具默认关闭每个派生工具接收一个对象参数顶层键是 operation 的 path/query/header 参数operation 有 JSON 请求体时再加一个body键。基类负责替换 path 参数然后交给request()一个干净的{ path, method, params, body, headers }。spec 里的本地$ref会被内联resolveRef会递归处理properties/items/additionalProperties/allOf/oneOf/anyOf外部引用和循环则降级为开放对象而非抛错见 openapi.ts保证生成的输入类型可用。此外还会暴露一个低层request工具作为逃生舱口用于覆盖派生工具够不到的操作。源码还有两个值得了解的实现细节operation 命名优先用operationId否则用sanitizeToolName(\${method}_${path})生成与保留名request/spec冲突或重名时会跳过并告警提示设置唯一的operationId见 openapi.ts。派生元数据按 spec 身份缓存解析 spec、解析 schema 很昂贵而 spec 是静态的、connector 每条消息都会重建所以用WeakMap把无闭包的派生操作元数据缓存在 spec 对象本身只有绑定到当前实例request()的execute闭包是每次新建的见 openapi.ts。沙箱内模型看到的是// Path params are substituted; the body is a typed object. const intent await stripe.CreatePaymentIntent({ amount: 2000, currency: usd }); // Escape hatch, if needed: const raw await stripe.request({ path: /v1/charges, method: GET });构造函数约定构造器只装依赖文档明确了一条约定构造函数只用于依赖注入——连接、token、客户端服务的身份与行为来自可覆盖的方法而不是构造函数配置。这也解释了为什么GithubConnector把conn以private参数属性注入而StripeConnector直接从this.env.STRIPE_KEY读取凭据// Good — constructor receives dependency export class GithubConnector extends McpConnectorEnv { constructor( ctx: ExecutionContext, env: Env, private conn: McpConnectionLike ) { super(ctx, env); } name() { return github; } protected createConnection() { return this.conn; } } // Also good — reads from env export class StripeConnector extends OpenApiConnectorEnv { name() { return stripe; } protected spec() { return stripeSpec; } protected async request(options: OpenApiRequestOptions) { // Uses this.env.STRIPE_KEY } }两种方式都合法连接类依赖MCP 连接对象走构造器凭据类依赖API key走 env。原则是——依赖可以注入行为必须覆盖。在真实项目中装配 ConnectorConnector 不是孤立存在的它最终被装配进createCodemodeRuntime再以runtime.tool()的形式交给模型。仓库 examples/codemode 提供了一个完整的可运行示例见 examples/codemode/src/server.ts其装配模式与文档 docs/codemode/index.md 完全一致// vite.config.ts import codemode from cloudflare/codemode/vite; import agents from agents/vite; import { cloudflare } from cloudflare/vite-plugin; export default { plugins: [agents(), codemode(), cloudflare()] };// server.ts — create the runtime and hand the model runtime.tool() const runtime createCodemodeRuntime({ ctx: this.ctx, executor: new DynamicWorkerExecutor({ loader: this.env.LOADER }), connectors: [ new GithubConnector(this.ctx, this.env, conn) ] });Vite 插件cloudflare/codemode/vite会自动为 Worker 入口模块追加export { CodemodeRuntime } from cloudflare/codemode;让运行时句柄能按名字创建它的 Durable Object facet详见 docs/codemode/vite-plugin.md。装配时开发者还可以通过运行时句柄获得完整控制面——pending()/approve()/reject()/rollback()处理审批、executions()提供审计轨迹、expirePaused()回收无人应答的暂停 run、saveSnippet()把好用的 run 提升为可复用片段codemode.run(open-prs, {...})。Connector 的disposeExecution钩子在这些终态转移和过期回收中扮演着资源回收的关键角色构成了定义—执行—审批—回滚—回收的完整闭环。总结Connector 是 codemode 写代码而非调工具范式的接入层基石。围绕 docs/codemode/connectors.md本文完整覆盖了从基类编写面name/instructions/tools/tool、单工具语义execute/revert/requiresApproval/replay、派生 RPC 表面到disposeExecution/onPassEnd双级资源生命周期再到McpConnector、OpenApiConnector、ToolSetConnector三种派生基类以及构造器依赖注入约定。配合仓库源码 packages/codemode/src/connectors 与可运行示例 examples/codemode你现在已经具备为任意外部服务编写一个一次编写、处处可用的 codemode Connector 的完整能力。【免费下载链接】agentsBuild and deploy AI Agents on Cloudflare项目地址: https://gitcode.com/GitHub_Trending/agents1/agents创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考