MCP Client 规模化设计:Progressive Discovery、Prompt Cache 与 Code Mode
MCP 从入门到工程实践系列,第 8 篇,共 9 篇。
本文以 MCP2026-07-28Client Best Practices 为基线。
最小 MCP Client 可以每轮执行:
tools/list ↓ 把全部 Tool Definitions 交给模型 ↓ 模型选择 Tool ↓ Tool Result 全部回到模型 Context只有两个 Weather Tool 时完全合理。但 Host 一旦连接 GitHub、Slack、Salesforce、Database、Logging 等几十个 Server,聚合几百甚至几千个 Tool,就会遇到两个独立问题:
- Tool Schema 太多:模型还没读用户问题,Context 已经被定义占据;
- 中间数据太多:日志、列表和循环结果不断在模型与 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 ContextHost 可以在内存或持久缓存中保存 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_tools和get_tool_details通常是 Host 自己提供给模型的 Meta-tool,不是 MCP Core 新增的强制 Method。Host 对 Server 的协议操作仍然是tools/list和tools/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
toolsArray 会变化; - 可能降低 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 数据"}]运行策略:
- 只连接最少的 Always-on Server;
- 当前任务需要时再连接目标 Server;
- 在合适的 Conversation Boundary 断开;
- 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/list、prompts/list、resources/list、resources/readResult 可以携带:
ttlMs:结果在多长时间内可视为 Fresh;cacheScope:是否可跨 User/Authorization Context 复用。
TTL 不是绝对保证。若收到对应list_changedNotification,相关 Cache 应在 TTL 到期前立即变为 Stale。若 Server 给出保守 TTL 或 Private Scope,Client 应按不可跨上下文复用处理。
2. Model Context
这是当前模型请求真正携带的 Tool Definitions。Progressive Discovery 直接减少的是这一部分。
3. Provider Prompt Cache
Model Provider 可能缓存 Prompt Prefix,其中可能包含toolsArray。频繁增加、删除、修改或重排定义,会改变 Prefix 并导致 Cache Miss。
三者关系:
MCP Server Tool Catalog ↓ Host Cache Host 内部已知的全部工具 ↓ 选择性注入 Model Context ↓ Provider 可能缓存相同 Prefix Prompt CacheHost 缓存了 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已失效,就不可能靠“只追加”永远维持正确列表。常见策略是:
- 执行层立即禁用:即使旧 Definition 暂时还在 Context,也不得继续执行;
- 从 Search Catalog 排除:新的发现不会再选到;
- 在 Conversation Boundary 整理 Array:接受相应 Cache Miss;
- 若存在安全风险,立即从 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:
constlogs=awaitlogging_getLogs({level:"error",since:Date.now()-3600000});constunique=newMap();for(constlogoflogs.entries){if(!unique.has(log.message)){unique.set(log.message,log);}}for(constlogofunique.values()){awaitticketing_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 和 MCPtools/call。
十三、Typed API 与字段来源
Host 可以根据 MCP Schema 生成 Sandbox Function Stub:
functionlogging_getLogs(input:{level:"error"|"warn"|"info";since:number;}):Promise<{entries:LogEntry[]}>inputSchema生成入参类型;outputSchema生成准确返回类型;- 缺少
outputSchema时,只能退化为any、string或额外提取。
如果只在循环之外偶尔需要结构化结果,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 ServersModel
模型负责生成控制逻辑,但不直接拿 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 成功,但 ResultisError: 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 故障。
参考资料
- https://modelcontextprotocol.io/docs/2026-07-28/develop/clients/client-best-practices
- https://modelcontextprotocol.io/docs/2026-07-28/develop/build-client