渐进式 Tool 发现
朴素的 MCP host 实现会在每次对话开始时,将每个已连接 server 的 tool 定义直接传给模型。对于少量 tools,这完全合理。但当 host 可以访问几十个 servers、这些 servers 暴露数百个 tools 时,仅这些定义就可能在模型读取用户消息前占用上下文窗口的大部分空间。- host 照常通过
tools/list获取 tool 定义,但延后将它们注入模型上下文。 - host 向模型提供一个轻量的
search_toolsmeta-tool。 - host 只在需要时将完整定义加载到上下文中。
何时使用渐进式发现
当 tool 定义占据上下文窗口较大部分时,最适合使用渐进式发现。对于少量 tools 且定义只占上下文窗口很小一部分的场景,加载全部 tools 没问题。一旦 tool 定义占用可用上下文窗口的显著部分,clients 就应该切换到渐进式发现。建议 clients 实现阈值来判断何时切换:- 将阈值实现为上下文窗口的百分比,例如 1%-5%。
- 加载 tool 定义。一旦达到阈值,就切换到渐进式发现。
选择发现策略
模型调用search_tools tool 后,需要选择搜索策略:
- 基于关键词:关键词匹配(BM25、regex)。简单有效,尤其适合描述性较强的 tool 名称和描述。
- 基于 embedding:对 tool 描述进行向量相似度检索。更擅长处理同义词和语义匹配。
- 基于 subagent:由第二个模型为任务选择 tools,通常是 Claude Haiku 或 Gemini Flash 等小而快的模型。通常效果很好,但成本可能高于 embedding 或关键词方案。
- 混合策略:组合多种方式。例如,对关键词和 embedding 排名综合打分,或根据用例和查询选择不同策略。
使用渐进式发现
渐进式发现的一种常见实现,是使用基于搜索的三层方法: 第 1 层:Catalog。 host 暴露少量用于搜索可用能力的 meta-tools。search_tools tool 接受自然语言查询,并返回匹配的 tool 名称和简短描述。
动态 Server 管理
渐进式发现不仅适用于单个 tools,也可以扩展到整个 servers。host 不必在启动时连接每个已配置 server,而是可以:- 维护可用 servers 及其高层描述的注册表。
- 只有当模型判断需要某个 server 的能力时,才连接该 server。
- 断开与当前任务不再相关的 servers,释放上下文。
实现指南
实现渐进式发现时:| 指南 | 原因 |
|---|---|
| 提供多个详细级别 | 让模型在仅名称、名称加描述,或完整 schema 响应之间选择。 |
| 缓存 tool definitions | 从 server 获取定义后,在 host 侧记忆化该定义,这样后续重新注入时不需要再进行一次 tools/list 往返。这与当前模型上下文中的内容是分开的。 |
在 list_changed 时刷新 | 当 server 发送 notifications/tools/list_changed 时,重新索引搜索 catalog。 |
| 按 server 分组 tools | 按来源 server 组织 tools,让模型能够推理相关能力。 |
与 Prompt Caching 的交互
大多数提供商会缓存 prompt 前缀,包括tools 数组。在对话中途添加或移除 tool definitions 会使该缓存失效,导致缓存未命中,其 token 成本可能高于你移除的定义。为保留缓存:
- 将新发现的定义追加到缓存断点之后,而不是重新排序
tools数组;或者通过单个稳定的call_tool({name, args})meta-tool 路由每次调用,让数组永不变化。 - 将 server 断开连接视为对话边界操作,而不是每轮操作。
- 结合上方 tool-search 链接,查阅你的提供商缓存文档。
程序化 Tool 调用 / Code Mode
使用直接 tool 调用时,每次 tool invocation 都是一次往返:模型生成 tool call,client 执行它,然后完整结果流回模型上下文。当任务需要串联多个 tools(读取文档、转换文档、写入其他位置)时,每个中间结果都会经过模型,即便模型并不需要处理这些结果,也会消耗 tokens 并增加延迟。 程序化 tool 调用(有时称为 “code mode”)为 clients 提供了一种有效组合 tool calls 的方式。模型不直接调用 tools,而是编写调用 tools 的代码。代码在沙箱环境中执行,只有最终结果返回给模型。 程序化 tool 调用很强大,可以更高效地使用 MCP tools 和 resources,但要求 clients 实现沙箱环境。工作原理
host 会将 MCP tool schemas 转换为沙箱内可用的类型化 API。当模型需要 tools 时,它会编写并执行脚本。 第 1 步:从 MCP schemas 生成程序化 API。 host 读取每个 server 的 tool definitions,并基于每个 tool 的参数和outputSchema 生成类型化函数:
outputSchema。当存在 output schema 时,host 可以生成精确返回类型(如上方的 LogEntry)。
当缺少 output schema 时,优先选择简单路径:
- 使用泛型类型并继续。 接受
any或string,并在下游处理非结构化输出。真正的修复方式是让 server 作者提供outputSchema。 - 使用快速模型提取类型化结果,适用于循环外的一次性调用。通过与 MCP tool calls 相同的 stub 拦截路径,暴露一个由 host 代理的
extract(value, ExpectedType)helper,使沙箱本身永远不打开网络连接。该 helper 会路由到小模型(例如 Claude Haiku 或 Gemini Flash),将值强制转换为ExpectedType。这会增加每次调用的延迟,也可能幻觉或丢弃字段,因此使用前要根据ExpectedType验证结果。
console.log 输出,也就是单行摘要,会返回给模型。
选择沙箱
合适的沙箱取决于你希望模型编写的语言、host 应用的语言,以及你需要的隔离程度。下表列出的是示例运行时,并不表示推荐;请根据你的用例评估成熟度:| 沙箱语言 | Runtime / Library | Host 语言 | 方式 |
|---|---|---|---|
| JavaScript | Deno, isolated-vm | Rust / Node / CLI | 基于 V8 的运行时,具备细粒度权限。可禁用所有权限以实现完全锁定。 |
| Python | Monty (experimental) | Rust | 为 AI 用例构建的最小 Python 解释器。默认无 I/O。 |
| TypeScript | pctx (early-stage) | Python / Rust | 将 code mode 概念作为库集成,并提供底层 Rust 支持。 |
| Any (via Wasm) | Wasmtime | Rust / C / Go | 将任意语言编译到 Wasm,并以基于能力的安全模型运行。 |
tools/call 请求分发到 MCP servers。
执行架构
该实现包含三个组件: 沙箱在没有直接网络访问的隔离环境中运行模型生成的代码。它与外部世界的唯一接口是生成的函数 stubs,这些 stubs 会将调用路由回 host。 host 充当 broker。它接收来自沙箱的函数调用,将其映射到正确的 MCP server,执行 tool call,并将结果返回给沙箱。授权 tokens 和凭据由 host 持有,永远不会暴露给生成的代码。 模型只看到沙箱返回的内容,通常是console.log 语句的输出或最终返回值。这让模型(以及 client 开发者)能够精确控制哪些内容进入上下文窗口。
安全注意事项
程序化 tool 调用引入了代码执行面,需要仔细沙箱化:- 逐调用授权:从规范角度看,broker 仍然是 MCP host。对沙箱发起的调用,应应用与直接调用相同的人类确认策略(参见 Tools: Security)。批准脚本并不等于对其运行时发起的每个 tool call 都授予一揽子批准;hosts 可以授予类别级批准(例如“允许本次脚本运行调用
ticketing_createIssue”),而不是每次迭代都提示,但 broker 仍必须根据该授权评估每次调用。 - 跨 server 数据流:来自一个 server 的 tool 结果,对另一个 server 来说是不可信输入。broker 应对代理调用应用与直接调用相同的输入审查策略;仅截断输出并不能防止外泄。
- 网络隔离:沙箱不应有直接网络访问。所有外部通信都通过 host broker 流动,由其执行授权和访问控制。
- 不暴露凭据:API keys 和 tokens 由 host 持有。生成的代码调用类型化函数;host 在转发到 servers 时添加认证。
- 资源限制:为沙箱执行设置超时和内存限制,防止脚本失控。
- 输出过滤:将沙箱 console 输出送回模型前,先验证并截断。
错误处理
MCP tool 错误会以带有isError: true 的成功响应形式返回,而不是传输失败。生成的 wrappers 应将其转换为抛出的异常,使模型编写的代码可以使用 try/catch。如果未捕获错误终止脚本,应将其作为脚本结果暴露出来,让模型能够自我修正;模型负责报告任何已经提交的部分副作用。