MCP Client 规模化设计:Progressive Discovery、Prompt Cache 与 Code Mode

MCP 从入门到工程实践系列,第 8 篇,共 9 篇。
本文以 MCP 2026-07-28 Client Best Practices 为基线。

最小 MCP Client 可以每轮执行:

tools/list
  ↓
把全部 Tool Definitions 交给模型
  ↓
模型选择 Tool
  ↓
Tool Result 全部回到模型 Context

只有两个 Weather Tool 时完全合理。但 Host 一旦连接 GitHub、Slack、Salesforce、Database、Logging 等几十个 Server,聚合几百甚至几千个 Tool,就会遇到两个独立问题:

  1. Tool Schema 太多:模型还没读用户问题,Context 已经被定义占据;
  2. 中间数据太多:日志、列表和循环结果不断在模型与 Host 之间往返。

官方 Best Practices 分别用两类思路处理:

  • Progressive Tool Discovery:减少模型一次看到的 Tool Definition;
  • Programmatic Tool Calling / Code Mode:减少模型亲自读取的中间结果。

这两者可以组合,但解决的不是同一个瓶颈。

一、先区分 Registry 与 Model Context

最关键的认识是:

Host 已通过 tools/list 知道全部工具,不等于模型当前上下文已经看见全部工具。

MCP Servers
    ↓ tools/list
Host Tool Registry / Cache
    ↓ 只注入少量必要定义
Model Context

Host 可以在内存或持久缓存中保存 2000 个 Tool Definition,但这一轮只向模型提供最相关的 5 个。

因此“Client 发现 Tool”和“模型看见 Tool”是两个时刻:

阶段 执行者 得到什么
Server Discovery MCP Client / Host 全部可用 Tool 的目录
Context Selection Host 当前任务应给模型的候选
Tool Selection 模型 本次要调用的 Tool 与参数

二、什么时候需要 Progressive Discovery

工具少时直接全量注入最简单。官方页面给出的经验信号是:当 Tool Definitions 占到 Context Window 约 1%~5% 时,可以考虑 Progressive Discovery。

这不是 Protocol Rule,也不是所有模型的固定阈值。实际应测量:

  • Schema Token 数;
  • First-token Latency;
  • Tool Selection Accuracy;
  • 错选和漏选率;
  • Provider Prompt Cache Hit;
  • 搜索本身带来的额外延迟。

官方图用“全量约 150K Token、按需发现约 2K Token”的极端示例说明量级,数字不是性能承诺。

三、Catalog、Inspect、Execute 三层

一种清晰设计是把 Tool 使用拆成三层。

1. Catalog:搜索候选能力

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"
  }
]

这一阶段只需要 Name、简短 Description、Server 来源和权限标签,不必加载所有 JSON Schema。

2. Inspect:读取完整 Schema

get_tool_details({
  name: "salesforce_updateRecord"
})

返回该 Tool 的:

  • Input Schema;
  • Output Schema;
  • 详细说明;
  • Server 来源;
  • 权限、风险和可用状态。

3. Execute:执行真实 Tool

模型知道精确参数后,再执行目标 Tool。底层仍由 Host 路由到正确 MCP Client,并发出 tools/call

search_toolsget_tool_details 通常是 Host 自己提供给模型的 Meta-tool,不是 MCP Core 新增的强制 Method。Host 对 Server 的协议操作仍然是 tools/listtools/call

四、Tool Catalog 怎样搜索

策略 优点 局限
Keyword / BM25 / Regex 简单、便宜、可解释 同义表达容易漏召回
Embedding / Vector Search 能处理语义相似 需要 Embedding 和索引维护
小模型选择 能综合复杂 Description 多一次模型成本和延迟
Provider Tool Search 集成方便 依赖 Provider 能力
Hybrid 可结合关键词、向量、权限与重排 实现复杂度更高

工具目录只有几百或几千项时,不一定需要大型 Vector Database;进程内索引、SQLite FTS 或小型向量索引也可能足够。

无论使用哪种检索,权限过滤都应在候选进入模型前进行。模型不应看到当前用户无权调用的敏感 Tool,再依赖它“自觉不选”。

五、它和知识库 Search Tool 有什么区别

这是最容易混淆的地方。

名称 搜索对象 返回内容
search_tools 能力目录 候选 Tool
get_tool_details 某个 Tool 的定义 Schema 与说明
search_knowledge_base 文档、记录、业务数据 相关内容片段
call_tool 不搜索,执行精确 Tool 真实业务结果

例如用户问“公司软件退款期限”:

search_tools("查询公司制度")
  → 找到 search_knowledge_base

get_tool_details("search_knowledge_base")
  → 得到 query、topK 等参数 Schema

call_tool(
  name="search_knowledge_base",
  args={"query": "软件退款期限", "topK": 5}
)
  → 返回制度文档片段

第一层搜的是“哪个能力能解决问题”;第二层真正的知识库 Tool 搜的是“业务数据里有哪些答案”。

call_tool 本身也不会替模型决定调用谁。模型通常先找到精确 Name,再把 Name 和 Arguments 交给统一入口;Host 负责映射、Schema Validation、Authorization 和执行。

六、两种 Progressive Discovery 执行设计

找到真实 Tool 后,有两种常见做法。

设计 A:动态加入真实 Tool

发现 salesforce_updateRecord
        ↓
把完整 Schema 加入 Provider tools Array
        ↓
模型原生调用 salesforce_updateRecord

优点:

  • Provider 直接看到专属 Schema;
  • 参数约束强;
  • 模型使用标准 Tool Calling 机制。

缺点:

  • Provider tools Array 会变化;
  • 可能降低 Prompt Cache Hit;
  • 动态增删 Tool 需要管理 Conversation 一致性。

设计 B:保持稳定 Meta-tool

模型始终只看到:

[search_tools, get_tool_details, call_tool]

最终统一调用:

{
  "name": "call_tool",
  "arguments": {
    "name": "salesforce_updateRecord",
    "args": {
      "recordId": "123",
      "fields": {
        "phone": "13800000000"
      }
    }
  }
}

优点:

  • Provider 的 Tools Array 很稳定;
  • 适合大规模、动态 Tool Catalog;
  • Prompt Prefix 更容易复用。

缺点:

  • Provider 只看到通用 args: object
  • Host 必须按真实 Schema 再校验;
  • 模型更可能写错 Tool Name 或 Arguments;
  • 通用 Meta-tool 可能削弱 Provider 原生 Tool Validation 的价值。

设计 B 不是“call_tool 自动搜索并决定调用谁”。它只是精确执行入口。

七、Dynamic Server Management

除了减少每台 Server 的 Tool,还可以按需连接 Server。Host 先保存一个轻量 Registry:

[
  {
    "name": "github",
    "description": "管理 Repository、Issue 和 Pull Request"
  },
  {
    "name": "salesforce",
    "description": "访问和修改 CRM 数据"
  }
]

运行策略:

  1. 只连接最少的 Always-on Server;
  2. 当前任务需要时再连接目标 Server;
  3. 在合适的 Conversation Boundary 断开;
  4. Skill 可以声明依赖哪些 MCP Server,触发时再连接。

断开 Server 不只是关闭 Socket,还要:

  • 从 Search Catalog 排除其 Tool;
  • 把关联 Definition 和 Cache 标为 Stale;
  • 阻止新 Call;
  • 妥善处理正在运行的 Request;
  • 向模型返回明确的 unavailable 状态。

尽量在 Conversation Boundary 调整工具集合,可降低一轮对话中 Provider Tools Prefix 大幅变化带来的混乱。

八、Progressive Discovery 的实现清单

Guideline 实现方式
Multiple detail levels 支持 Name-only、Name + Description、Full Schema
Cache definitions Host 保存 tools/list 结果,避免重复拉取
Refresh on change 收到 list_changed 后将旧目录标为 Stale,重新 List 和建索引
Group by server Catalog 保留来源,便于理解和路由
Namespace names github__create_issue,避免跨 Server 重名
Permission-aware search 搜索前按用户、租户和授权上下文过滤

公开名称可加 Namespace,Registry 内保留原始 Server Tool Name。真正执行时再映射回来:

模型看到 github__create_issue
        ↓
Registry 找到 GitHub Client
        ↓
映射回 create_issue
        ↓
MCP tools/call

九、三个容易混淆的“缓存”

1. Host Tool Cache

Host 保存 tools/list 返回的 Tool Definition,避免频繁请求 Server,并用于搜索和路由。

2026-07-28 版本中,tools/listprompts/listresources/listresources/read Result 可以携带:

  • ttlMs:结果在多长时间内可视为 Fresh;
  • cacheScope:是否可跨 User/Authorization Context 复用。

TTL 不是绝对保证。若收到对应 list_changed Notification,相关 Cache 应在 TTL 到期前立即变为 Stale。若 Server 给出保守 TTL 或 Private Scope,Client 应按不可跨上下文复用处理。

2. Model Context

这是当前模型请求真正携带的 Tool Definitions。Progressive Discovery 直接减少的是这一部分。

3. Provider Prompt Cache

Model Provider 可能缓存 Prompt Prefix,其中可能包含 tools Array。频繁增加、删除、修改或重排定义,会改变 Prefix 并导致 Cache Miss。

三者关系:

MCP Server Tool Catalog
        ↓ Host Cache
Host 内部已知的全部工具
        ↓ 选择性注入
Model Context
        ↓ Provider 可能缓存相同 Prefix
Prompt Cache

Host 缓存了 1000 个 Tool,不等于模型看到 1000 个;Provider 命中 Prompt Cache,也不等于 Host 的 Server Definition 一定仍然 Fresh。

十、“追加保持前缀稳定”到底能做到什么

初始 Provider Tool Array:

[search_tools, get_tool_details, github_list_prs]

新增 Tool 时只追加:

[search_tools, get_tool_details, github_list_prs, slack_send_message]

已有 Prefix 和顺序得以保留,更可能复用 Provider Prompt Cache。

但如果前面的 github_list_prs 已失效,就不可能靠“只追加”永远维持正确列表。常见策略是:

  1. 执行层立即禁用:即使旧 Definition 暂时还在 Context,也不得继续执行;
  2. 从 Search Catalog 排除:新的发现不会再选到;
  3. 在 Conversation Boundary 整理 Array:接受相应 Cache Miss;
  4. 若存在安全风险,立即从 Context 删除:正确性和安全性优先。

如果模型在过渡期仍调用旧 Tool,Host 应返回明确的 unavailable/stale Error。不能为了 Prompt Cache Hit,让已经撤销的能力继续工作。

“前缀稳定”只是一种新增和排序策略,不是永不删除的承诺。

十一、第二个瓶颈:大量中间结果

即使只让模型看到两个相关 Tool,也可能出现:

模型调用 logging_getLogs
        ↓
10,000 条日志进入模型 Context
        ↓
模型阅读、过滤、去重
        ↓
模型逐次调用 ticketing_createIssue

传统 Tool Loop 的每一步都需要:

Model → Host → Tool → Host → Model

如果任务主要是循环、筛选、排序、去重或把一个 Tool 的结果传给另一个 Tool,大量中间数据进入模型 Context 既昂贵,也容易干扰推理。

十二、Programmatic Tool Calling / Code Mode

Code Mode 让模型生成一段调用 Tool 的程序,而不是每次只生成一个 Tool Call:

const logs = await logging_getLogs({
  level: "error",
  since: Date.now() - 3600000
});

const unique = new Map();
for (const log of logs.entries) {
  if (!unique.has(log.message)) {
    unique.set(log.message, log);
  }
}

for (const log of unique.values()) {
  await ticketing_createIssue({
    title: log.message,
    body: log.stackTrace,
    priority: "high"
  });
}

console.log(
  "Filed " + unique.size +
  " tickets from " + logs.entries.length +
  " logs"
);

10,000 条日志留在 Sandbox 内,模型只看到最终摘要。

这不会让模型绕开 MCP。Sandbox 中的函数只是 Host 生成的 Stub,真正执行仍要经过 Host Broker 和 MCP tools/call

十三、Typed API 与字段来源

Host 可以根据 MCP Schema 生成 Sandbox Function Stub:

function logging_getLogs(input: {
  level: "error" | "warn" | "info";
  since: number;
}): Promise<{ entries: LogEntry[] }>
  • inputSchema 生成入参类型;
  • outputSchema 生成准确返回类型;
  • 缺少 outputSchema 时,只能退化为 anystring 或额外提取。

如果只在循环之外偶尔需要结构化结果,Host 也可以提供:

extract(value, ExpectedType)

把非结构化结果交给小模型抽取,再按 Expected Type 校验。但这会增加延迟,而且模型可能遗漏或幻觉字段。因此更根本的方案仍是推动 Server 提供可靠 outputSchema

这和 Weather Server 的问题本质相同:开发者不能猜 properties.forecast,Code Mode 也不能猜某个 Tool Result 一定有 entries。字段必须来自上游 API Contract 或 Tool Output Schema。

十四、Code Mode 的三层架构

Model
  ↓ 生成代码
Sandbox
  ↓ 调用 Host 注入的 Typed Function Stub
Host Broker
  ↓ MCP tools/call
MCP Servers

Model

模型负责生成控制逻辑,但不直接拿 Credential,也不直接访问 Server 网络。

Sandbox

Sandbox:

  • 执行模型生成的代码;
  • 默认没有直接网络;
  • 只能调用 Host 注入的函数;
  • 限制 CPU、Memory、Time、Call Count 和 Output;
  • 保存中间数据;
  • 只把必要的最终结果交回模型。

官方页面列出的 Runtime 只是候选示例,不代表统一背书:

生成代码语言 Runtime / Library 关注点
JavaScript Deno、isolated-vm V8 权限与隔离
Python Monty(experimental) 面向 AI 的精简 Interpreter
TypeScript pctx(early-stage) Code Mode Library
Wasm 路径 Wasmtime Capability-based Isolation

选择时应评估模型擅长的语言、Host 技术栈、隔离强度、启动成本、Library Maturity 和可观测性,而不是看到一个名字就直接用于生产。

Host Broker

Broker 才是受信任执行边界:

  • 拦截 Sandbox Function Call;
  • 查 Registry,定位 MCP Server 与 Client;
  • 按真实 Schema 校验 Arguments;
  • 检查用户、租户与 Tool 权限;
  • 必要时请求用户确认;
  • 持有 Credential;
  • 发出 tools/call
  • 把 Result 或 Error 返回 Sandbox。

十五、Code Mode 的安全边界

Code Mode 引入了“模型生成代码”这一攻击面,至少要处理:

1. 每个 Tool Call 单独授权

批准执行脚本,不等于批准脚本内部的所有操作。创建 Issue、删除文件、发送消息等有副作用的动作仍应按策略逐项授权或批量明确授权。

2. Sandbox 默认禁止直接网络

否则生成代码可能绕过 Broker:

  • 把数据发送到未知地址;
  • 自行调用未经批准的 API;
  • 直接使用错误或泄露的 Credential。

3. Credential 只保存在 Host

Sandbox 看到的是函数能力,不应拿到 OAuth Token、API Key 或 Server Secret。

4. 跨 Server 数据仍是不可信输入

一个 Server 的 Result 传给另一个 Server,并不会自动变成可信数据。Host 仍需防范 Prompt Injection、恶意字段、超大 Payload 和数据外泄。

5. 限制资源

至少设置:

  • Script Timeout;
  • Memory Limit;
  • Tool Call Count;
  • Loop/Execution Budget;
  • Console Output Limit;
  • Result Size Limit;
  • 并发和速率限制。

6. 正确转换 Error

MCP Tool 的业务失败可能表现为协议 Request 成功,但 Result isError: true。Generated Wrapper 应把它转成 Sandbox 内可 try/catch 的 Exception,而不是只捕获 Transport Exception。

7. 处理部分成功

脚本可能已创建三个 Ticket,第四个失败。系统不能假设自动回滚,而应报告:

  • 哪些动作已完成;
  • 哪一步失败;
  • 是否可重试;
  • 重试会不会重复产生副作用。

十六、两种模式怎样组合

几千个 Tool
        ↓ Progressive Discovery
只加载 logging_getLogs 与 ticketing_createIssue
        ↓ Code Mode
Sandbox 内完成查询、过滤、去重和循环调用
        ↓
模型只收到最终摘要

对应关系:

技术 减少什么
Progressive Discovery Tool Definition Token
Code Mode 中间 Tool Result Token 与模型往返

一个解决“模型需要看到哪些能力”,另一个解决“能力之间的数据怎样流动”。

十七、落地决策建议

可以按规模逐步演进:

阶段 1:工具很少

  • 全量 tools/list 后注入;
  • 使用 Provider 原生 Tool Calling;
  • 优先保证正确性和可观测性。

阶段 2:定义开始挤占 Context

  • 建 Host Registry;
  • 实现 Tool Namespace;
  • 引入 Catalog/Inspect;
  • 先用 Keyword/Hybrid Search;
  • 只注入少量真实 Schema。

阶段 3:目录高度动态

  • 按需连接 Server;
  • 监听 list_changed
  • 引入 TTL、Scope 和 Stale 管理;
  • 在动态真实 Tool 与稳定 Meta-tool 之间权衡。

阶段 4:中间数据与循环很大

  • 要求关键 Tool 提供 outputSchema
  • 生成 Typed Stub;
  • 引入隔离 Sandbox 和 Broker;
  • 设置权限、资源与副作用边界。

十八、常见误区

误区 1:Progressive Discovery 就是不调用 tools/list

不是。Host 仍需发现并维护目录,只是不把全部 Schema 同时放进模型 Context。

误区 2:工具目录大就必须部署大型向量数据库

不一定。目录规模、语言表达和召回要求决定索引方案,小型内存或本地索引可能足够。

误区 3:call_tool 会自动找到合适 Tool

不会。它通常只按精确 Name 执行;搜索和选择发生在之前。

误区 4:稳定 Meta-tool 与知识库 Search 是同一个东西

不是。一个管理能力目录与执行入口,另一个检索业务数据。

误区 5:为了 Prompt Cache,失效 Tool 可以继续调用

绝对不行。执行层必须立即禁用;正确性和安全性高于 Cache Hit。

误区 6:Code Mode 让模型直接访问所有 Server

不是。模型生成逻辑,Sandbox 运行,Host Broker 仍掌握 Credential、权限与 MCP 调用。

误区 7:脚本失败就表示什么都没发生

不一定。跨 Tool 操作往往没有自动 Transaction,必须追踪并报告 Partial Effects。

十九、总结

规模化 MCP Client 不是简单地“把更多工具给模型”,而是建立清晰分层:

Server Registry
  → 哪些 Server 存在

Host Tool Catalog
  → 全部能力及其来源、状态、权限

Progressive Discovery
  → 当前模型需要看哪些定义

Provider Tool Calling / Meta-tool
  → 模型怎样表达执行意图

Sandbox + Broker
  → 大量中间数据和循环怎样安全执行

最终原则是:

Host 可以知道很多,但模型每轮只需要知道足够完成当前任务的部分;模型可以生成复杂逻辑,但真正的权限、Credential 和执行边界必须留在 Host。

系列最后一篇将把这些组件放进真实排错流程:怎样使用 Inspector、stderr Log、Client DevTools、路径和协议错误码定位 MCP 故障。

参考资料

Logo

欢迎加入 MCP 技术社区!与志同道合者携手前行,一同解锁 MCP 技术的无限可能!

更多推荐