Skip to main content
随着 agents 等 MCP host 应用连接更多 MCP servers,并逐渐获得对数百甚至数千个 tools 的访问权限,朴素的 tool 管理方式会失效。预先将每个 tool 定义都加载到模型上下文窗口中,会浪费 tokens、增加延迟,并降低模型表现。在连续 tool 调用之间通过模型传递大型中间结果,会进一步放大这个问题。 有两种模式可以应对这些挑战:渐进式发现控制 tool 定义_何时_进入上下文,程序化 tool 调用控制 tools _如何_被调用。

渐进式 Tool 发现

朴素的 MCP host 实现会在每次对话开始时,将每个已连接 server 的 tool 定义直接传给模型。对于少量 tools,这完全合理。但当 host 可以访问几十个 servers、这些 servers 暴露数百个 tools 时,仅这些定义就可能在模型读取用户消息前占用上下文窗口的大部分空间。 对比预先加载所有 tools 与按需发现 tools。预先加载方式仅定义就消耗约 150,000 tokens,而渐进式发现只加载任务所需内容,约使用 2,000 tokens。 渐进式发现可以避免这个问题:
  • host 照常通过 tools/list 获取 tool 定义,但延后将它们注入模型上下文。
  • host 向模型提供一个轻量的 search_tools meta-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 排名综合打分,或根据用例和查询选择不同策略。
一些模型提供商已经提供内置 tool search。例如,OpenAIAnthropic 原生支持这一能力;请查看你的提供商文档中是否有等价功能。可用时,你可能会优先使用平台提供的 tool search,而不是自定义实现。当提供商不提供该能力,或你需要专门的检索逻辑(例如特定领域排序或访问控制过滤)时,再自行构建。 下面的三层模式详细说明了基于自定义搜索的方法,但无论采用何种检索机制,分层原则(catalog、inspect、execute)都适用。

使用渐进式发现

渐进式发现的一种常见实现,是使用基于搜索的三层方法: 第 1 层:Catalog。 host 暴露少量用于搜索可用能力的 meta-tools。search_tools tool 接受自然语言查询,并返回匹配的 tool 名称和简短描述。
// 模型调用轻量搜索 tool
search_tools({ query: "update salesforce record" })

// 只返回精简匹配:名称和单行描述
→ [
    { name: "salesforce_updateRecord", description: "Update fields on a Salesforce object" },
    { name: "salesforce_upsertRecord", description: "Insert or update based on external ID" }
  ]
第 2 层:Inspect。 模型识别出候选 tool 后,只获取该 tool 的完整定义(input schema、output schema、文档)。
// 模型只检查所需 tool
get_tool_details({ name: "salesforce_updateRecord" });
这会返回单个 tool 的完整 schema:
{
  "name": "salesforce_updateRecord",
  "description": "Updates a record in Salesforce",
  "inputSchema": {
    "type": "object",
    "properties": {
      "objectType": {
        "type": "string",
        "description": "Salesforce object type"
      },
      "recordId": { "type": "string", "description": "Record ID to update" },
      "data": { "type": "object", "description": "Fields to update" }
    },
    "required": ["objectType", "recordId", "data"]
  }
}
第 3 层:Execute。 模型在充分了解 tool 接口后调用该 tool,同时只加载所需定义。 这种模式能显著降低 token 使用量,并提高 tool 选择准确性:模型聚焦于少量相关 tools,而不是扫描数百个无关 tools。其他发现策略(embedding、subagents 等)遵循同样的分层原则,只是在 catalog 层替换不同的检索机制。

动态 Server 管理

渐进式发现不仅适用于单个 tools,也可以扩展到整个 servers。host 不必在启动时连接每个已配置 server,而是可以:
  1. 维护可用 servers 及其高层描述的注册表。
  2. 只有当模型判断需要某个 server 的能力时,才连接该 server。
  3. 断开与当前任务不再相关的 servers,释放上下文。
这对通用 agents 尤其有效,因为用户意图在一开始并不明确。agent 从一组最小的常驻 servers 开始,并按需连接其他 servers。结合 agent skills,skill 文件可以声明它需要哪些 MCP servers,host 只在该 skill 被调用时连接它们。

实现指南

实现渐进式发现时:
指南原因
提供多个详细级别让模型在仅名称、名称加描述,或完整 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 实现沙箱环境。 对比直接 tool 调用与程序化 tool 调用。直接调用会让每个中间结果都经过模型(约 100K+ tokens)。程序化调用向沙箱发送约 200-token 脚本,由沙箱执行 tool calls 并返回约 15-token 摘要。

工作原理

host 会将 MCP tool schemas 转换为沙箱内可用的类型化 API。当模型需要 tools 时,它会编写并执行脚本。 第 1 步:从 MCP schemas 生成程序化 API。 host 读取每个 server 的 tool definitions,并基于每个 tool 的参数和 outputSchema 生成类型化函数:
// 由 Logging MCP server 的 tool schema 自动生成
interface LogEntry {
  timestamp: string;
  message: string;
  level: string;
}

function logging_getLogs(input: {
  level: "error" | "warn" | "info";
  since: number;
}): Promise<{ entries: LogEntry[] }> {
  return mcp.callTool<{ entries: LogEntry[] }>("logging_getLogs", input);
}

// 由 Ticketing MCP server 的 tool schema 自动生成
function ticketing_createIssue(input: {
  title: string;
  body?: string;
  priority: "low" | "medium" | "high";
}): Promise<{ issueId: string }> {
  return mcp.callTool<{ issueId: string }>("ticketing_createIssue", input);
}
MCP Servers 可以为每个 tool 提供可选的 outputSchema。当存在 output schema 时,host 可以生成精确返回类型(如上方的 LogEntry)。 当缺少 output schema 时,优先选择简单路径:
  • 使用泛型类型并继续。 接受 anystring,并在下游处理非结构化输出。真正的修复方式是让 server 作者提供 outputSchema
  • 使用快速模型提取类型化结果,适用于循环外的一次性调用。通过与 MCP tool calls 相同的 stub 拦截路径,暴露一个由 host 代理的 extract(value, ExpectedType) helper,使沙箱本身永远不打开网络连接。该 helper 会路由到小模型(例如 Claude Haiku 或 Gemini Flash),将值强制转换为 ExpectedType。这会增加每次调用的延迟,也可能幻觉或丢弃字段,因此使用前要根据 ExpectedType 验证结果。
第 2 步:模型基于这些 API 编写代码。 模型不再发起多个独立 tool calls、让完整结果在调用之间流经上下文,而是编写单个脚本。以“查找过去一小时内所有错误日志,并为每个唯一错误创建工单”为例。使用直接 tool 调用时,成千上万条日志会流经模型上下文;使用代码时,模型在沙箱中完成过滤:
// 模型生成的代码,在沙箱中执行
const logs = await logging_getLogs({
  level: "error",
  since: Date.now() - 3600000,
});

// 在沙箱内过滤和去重,而不是在模型上下文中
const uniqueErrors = new Map<string, LogEntry>();
for (const log of logs.entries) {
  if (!uniqueErrors.has(log.message)) {
    uniqueErrors.set(log.message, log);
  }
}

for (const [message, log] of uniqueErrors) {
  await ticketing_createIssue({
    title: `Error: ${message}`,
    body: `First seen: ${log.timestamp}\nOccurrences: ${
      logs.entries.filter((l) => l.message === message).length
    }`,
    priority: "high",
  });
}

console.log(
  `Filed ${uniqueErrors.size} tickets from ${logs.entries.length} error logs`,
);
第 3 步:沙箱执行代码。 沙箱内的函数调用会被拦截,并通过 host broker 路由回对应的 MCP server。日志数据和工单创建会直接在 servers 之间流动,从不进入模型上下文。只有 console.log 输出,也就是单行摘要,会返回给模型。

选择沙箱

合适的沙箱取决于你希望模型编写的语言、host 应用的语言,以及你需要的隔离程度。下表列出的是示例运行时,并不表示推荐;请根据你的用例评估成熟度:
沙箱语言Runtime / LibraryHost 语言方式
JavaScriptDeno, isolated-vmRust / Node / CLI基于 V8 的运行时,具备细粒度权限。可禁用所有权限以实现完全锁定。
PythonMonty (experimental)Rust为 AI 用例构建的最小 Python 解释器。默认无 I/O。
TypeScriptpctx (early-stage)Python / Rust将 code mode 概念作为库集成,并提供底层 Rust 支持。
Any (via Wasm)WasmtimeRust / C / Go将任意语言编译到 Wasm,并以基于能力的安全模型运行。
无论选择哪种沙箱,集成模式都相同:host 注入函数 stubs,通过进程内或 stdio 通道拦截调用(因此网络权限可以保持完全拒绝),并将它们作为 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。如果未捕获错误终止脚本,应将其作为脚本结果暴露出来,让模型能够自我修正;模型负责报告任何已经提交的部分副作用。

组合两种模式

渐进式发现和程序化 tool 调用可以很好地配合使用。模型使用发现 tools 识别所需 tools,加载它们的 schemas,然后编写一个单脚本,在一次执行过程中调用多个 tools。这种组合同时最小化 tool definitions 的 token 成本和 tool results 的 token 成本,让模型上下文专注于推理,而不是传递数据。