渐进式工具发现
朴素的 MCP 宿主实现在每次对话开始时直接把每个已连接服务器的工具定义传给模型。对于少量工具,这完全合理。但当宿主能访问几十个暴露数百个工具的服务器时,仅这些定义本身就能在模型甚至还没读到用户消息之前,就消耗掉上下文窗口的大部分。- 宿主照常通过
tools/list获取工具定义,但推迟将它们注入模型的上下文。 - 宿主向模型提供一个轻量的
search_tools元工具(meta-tool)。 - 宿主仅在需要时才将完整定义加载到上下文中。
何时使用渐进式发现
渐进式发现最适合在工具定义占据上下文窗口很大一部分时使用。对于一小组工具、其工具定义只占上下文窗口一小部分的情况,加载所有工具是可以的。一旦工具定义占据可用上下文窗口的相当一部分,客户端就应切换到渐进式发现。我们建议客户端实现阈值来确定何时切换:- 将阈值实现为上下文窗口的百分比。例如 1%–5%。
- 加载工具定义。一旦达到阈值,就切换到渐进式发现。
选择一种发现策略
一旦模型调用search_tools 工具,我们需要选择一种搜索策略:
- 基于关键词:关键词匹配(BM25、正则)。简单而有效,特别是对于描述性的工具名和描述。
- 基于嵌入:在工具描述上进行向量相似度检索。更好地处理同义词和语义匹配。
- 基于子智能体:一个次级模型(通常是一个小而快的模型,如 Claude Haiku 或 Gemini Flash)为任务选择工具。这通常效果很好,但可能比基于嵌入或基于关键词的方案更昂贵。
- 混合:组合多种方法。例如,通过在关键词和嵌入排名之间打分,或根据用例或查询选择不同的策略。
使用渐进式发现
渐进式发现的一种常见实现使用基于搜索的三层方法: 第 1 层:Catalog(目录)。 宿主暴露一小组用于搜索可用能力的元工具。一个search_tools 工具接受一个自然语言查询,并返回匹配的工具名及简短描述。
动态服务器管理
渐进式发现不仅限于单个工具,还延伸到整个服务器。宿主可以不在启动时连接每个已配置的服务器,而是:- 维护一个可用服务器及其高层描述的注册表。
- 仅在模型确定它需要某个服务器的能力时才连接该服务器。
- 断开与当前任务不再相关的服务器,释放上下文。
实现指南
在实现渐进式发现时:与提示缓存的交互
大多数提供方缓存提示前缀,包括tools 数组。在对话中途添加或移除工具定义会使该缓存失效,而由此产生的未命中所耗费的 token 可能比你移除的定义还多。为保留缓存:
- 在缓存断点之后追加新发现的定义,而不是重新排序
tools数组;或者将每个调用通过单个稳定的call_tool({name, args})元工具路由,使该数组永不改变。 - 将服务器断开视为一个对话边界操作,而不是一个按轮次的操作。
- 结合上面的工具搜索链接查阅你提供方的缓存文档。
编程式工具调用 / 代码模式
在直接工具调用中,每次工具调用都是一次往返:模型生成一次工具调用,客户端执行它,完整结果流回模型的上下文。当一个任务需要串联多个工具(读取一个文档、转换它、把它写到别处)时,每个中间结果都会经过模型,消耗 token 并增加延迟,即使它与这些结果毫无关系。 编程式工具调用(有时称为“代码模式”)为客户端提供了一种有效编排工具调用的方式。模型不直接调用工具,而是编写调用工具的代码。代码在一个沙箱化的环境中执行,只有最终结果返回给模型。 编程式工具调用很强大,允许更高效地使用 MCP 工具和资源,但需要客户端实现一个沙箱环境。它如何运作
宿主将 MCP 工具 schema 转换为沙箱内可用的类型化 API。当模型需要工具时,它编写一个脚本并执行它。 第 1 步:从 MCP schema 生成一个编程式 API。 宿主读取每个服务器的工具定义,并基于每个工具的参数和outputSchema 产生类型化的函数:
outputSchema。当存在输出 schema 时,宿主可以产生精确的返回类型(如上面的 LogEntry)。
当输出 schema 缺失时,优先选择简单路径:
- 使用一个通用类型并继续。 接受
any或string,并在下游处理这个无结构的输出。真正的修复是让服务器作者提供outputSchema。 - 使用一个快速模型提取一个类型化的结果,用于循环之外的单次调用。通过与 MCP 工具调用相同的 stub 拦截路径暴露一个由宿主中介的
extract(value, ExpectedType)辅助函数,这样沙箱本身永远不会打开网络连接。该辅助函数路由到一个小模型(例如 Claude Haiku 或 Gemini Flash)来将值强制转换为ExpectedType。这会增加每次调用的延迟,并可能产生幻觉或丢弃字段,因此在使用前对照ExpectedType校验结果。
console.log 输出(单个摘要行)返回给模型。
选择一个沙箱
正确的沙箱取决于你想让模型编写的语言、你宿主应用的语言,以及你需要多少隔离。下表列出的是示例运行时而非背书;请针对你的用例评估其成熟度:
无论采用何种沙箱,集成模式都相同:宿主注入函数 stub,通过一个进程内或 stdio 信道拦截调用(这样网络权限可以保持完全拒绝),并将它们作为
tools/call 请求分派给 MCP 服务器。
执行架构
该实现有三个组件: 沙箱在一个无直接网络访问的隔离环境中运行模型生成的代码。它与外部世界的唯一接口是通过生成的函数 stub,后者将调用路由回宿主。 宿主充当中介。它接收来自沙箱的函数调用,将它们映射到正确的 MCP 服务器,执行工具调用,并将结果返回给沙箱。授权令牌和凭据由宿主持有,永不暴露给生成的代码。 模型只看到沙箱返回的内容,通常是console.log 语句的输出或一个最终返回值。这给予模型(以及客户端开发者)对何物进入上下文窗口的精确控制。
安全考量
编程式工具调用引入了一个需要仔细沙箱化的代码执行面:- 按调用授权:就规范目的而言,中介仍然是 MCP 宿主。对源自沙箱的调用应用与你对直接调用相同的人在回路确认策略(参见 Tools:安全)。批准脚本并不意味着为它在运行时进行的每个工具调用授予笼统的批准;宿主可以授予分类批准(例如,“为本次脚本运行允许
ticketing_createIssue”)而不是逐次迭代提示,但中介仍必须对照该授予评估每个调用。 - 跨服务器数据流:来自一个服务器的工具结果是对另一个服务器的不受信任输入。中介应对被中介的调用应用与直接调用相同的输入审查策略;仅靠输出截断并不能防止数据外泄。
- 网络隔离:沙箱不应有直接的网络访问。所有外部通信都流经宿主中介,由它强制执行授权和访问控制。
- 不暴露凭据:API 密钥和令牌由宿主持有。生成的代码调用类型化的函数;宿主在转发到服务器时添加身份认证。
- 资源限制:为沙箱执行设置超时和内存限制,以防止失控的脚本。
- 输出过滤:在将沙箱控制台输出反馈给模型之前对其进行校验和截断。
错误处理
MCP 工具错误作为一个带有isError: true 的成功响应到达,而不是一次传输失败。生成的包装器应将其转换为一个抛出的异常,以便模型编写的代码可以使用 try/catch。如果一个未捕获的错误终止了脚本,将它作为脚本的结果呈现,以便模型可以自我纠正;模型负责报告任何已经提交的部分副作用。