三亩地 三亩地SAN MU DI · CODE DIARY
ARTICLE DETAIL

日记详情

真实记录编程学习的某一天,欢迎挑你感兴趣的翻一翻。

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

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

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,就会遇到两个独立问题:

  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 DiscoveryMCP Client / Host全部可用 Tool 的目录
Context SelectionHost当前任务应给模型的候选
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 机制。

缺点:

  • ProvidertoolsArray 会变化;
  • 可能降低 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 definitionsHost 保存tools/list结果,避免重复拉取
Refresh on change收到list_changed后将旧目录标为 Stale,重新 List 和建索引
Group by serverCatalog 保留来源,便于理解和路由
Namespace namesgithub__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/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 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:

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时,只能退化为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关注点
JavaScriptDeno、isolated-vmV8 权限与隔离
PythonMonty(experimental)面向 AI 的精简 Interpreter
TypeScriptpctx(early-stage)Code Mode Library
Wasm 路径WasmtimeCapability-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 DiscoveryTool 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
← 返回列表