返回 AICAP-180
B6 · Day 54真 MCP server (2026-07-28 spec)

tools/list 网络语义

Day 53 把第一个 Zod 工具注册进了 server。但「注册了」不等于「客户端能正确发现」——发现走的是 tools/list,而在 stateless 规范下,tools/list 有它特定的网络语义:任意节点都能回完整清单、客户端可按 TTL 缓存、工具顺序必须稳定。今天用 MCP Inspector 真连上 server,调 tools/list,核对返回的工具数/名称/顺序与进程内

阶段: B6 · 真 MCP server (2026-07-28 spec)(Day 51-60) 标签: #mcp #tools-list #caching #mcp-inspector

今日导引(由浅入深)

Day 53 把第一个 Zod 工具注册进了 server。但「注册了」不等于「客户端能正确发现」——发现走的是 tools/list,而在 stateless 规范下,tools/list 有它特定的网络语义:任意节点都能回完整清单、客户端可按 TTL 缓存、工具顺序必须稳定。今天用 MCP Inspector 真连上 server,调 tools/list,核对返回的工具数/名称/顺序与进程内 list() 一致。这是 B6 从「能起 server」走向「契约可对拍」的关键一步:顺序稳定是缓存与 mock↔真 server 对拍的前提。今日最小可判定产出:Inspector 截图,tools/list 返回 ≥1 工具(Day 53 注册的工具)。

0. 协议背景速记

  • 承上:Day 53 注册了第一个 Zod 工具;但「注册了」不等于「客户端能正确发现」。
  • 本日:用 MCP Inspector 真连 server 调 tools/list,核对数量/名称/顺序与进程内 list() 一致。
  • 判定点:Inspector 截图,tools/list 返回 ≥1 工具。

1. 机理精读

stateless 下 tools/list 是无状态发现:因为 server 不维护会话,tools/list 对所有客户端、从任意节点返回的结果都应当相同——它是一个纯发现操作。两个直接性质:

  • (1) 可缓存:客户端可以按 TTL(time-to-live)把工具清单缓存起来,不必每次请求前都重拉,省一次网络往返。
  • (2) 可水平扩容:LB 后面任意一台 server 都能回完整清单,不需要粘性路由。

这正是 Day 51 讲 stateless 好处时承诺的「tools/list 结果可被客户端缓存」的落地。

契约关键是排序稳定tools/list 返回的工具顺序必须稳定且确定。为什么这条这么重要?两个下游都依赖它:

  • 缓存一致性:客户端缓存的是一份有序清单;如果同一份工具集每次 tools/list 顺序不同,客户端没法判断「清单变了还是只是顺序抖动」,缓存失效逻辑会误判。
  • mock ↔ 真 server 对拍:我们有进程内 mock(toolRegistry.tslist())和网络版两套实现。要确认网络版没改变契约,得拿两边的 tools/list 输出逐条比对——如果顺序不稳定,对拍会因「顺序不同」而误报差异,掩盖真正的问题。

所以「网络 tools/list 的工具顺序须与进程内 list() 一致」是今天的硬契约。实现上靠确定性排序保证(见第 2 节 list()localeCompare),而不是依赖 Map 的插入顺序——后者会随注册时序变化。

stateless 的发现刷新策略

  • stateless 客户端不能假设 server 会主动推送 listChanged 通知来告诉它「工具变了」。
  • 那需要 server 维护会话 + SSE 推送,与 stateless 的无会话前提相悖。
  • 正确做法是客户端靠 TTL 主动重拉:缓存到期就重新 tools/list
  • 代价:工具集变化的感知有 TTL 级延迟,换来的是无状态可扩容——这是 stateless 在「实时性」上的明确取舍。

与相邻概念的边界

  • tools/list 只负责「有哪些工具、各自的契约」,不执行任何工具——执行是明天 Day 55 的 tools/call
  • 今天确认的是发现层契约:数量、名称、inputSchema(Day 53 的 Zod 派生)、以及顺序稳定。

2. 推导 / 手算 / 代码走读

进程内 list() 的排序保证(Read src/agent/mcp/toolRegistry.ts,真实已存在):

  • list()(line 169):
    list(): McpToolSpec[] {
      return [...this.tools.values()].map((t) => t.spec)
        .sort((a, b) => a.name.localeCompare(b.name))
    }
    
    关键在 .sort((a, b) => a.name.localeCompare(b.name))——按 name 字典序稳定排序。这就是「排序稳定」契约的进程内实现:不管工具注册进 Map 的先后,list() 永远按 name 升序输出。
  • 注释(line 166-167)写明 "tools/list — 工具发现。返回稳定排序(按 name)的声明列表。stateless:不依赖任何会话,结果可被客户端按 ttl 缓存。"——进程内 mock 已经把「稳定排序 + 可缓存 + 无会话」三性质都标注到位。

tools/list 返回里每条工具长什么样:除了 name,还携带 descriptioninputSchema(即 Day 53 由 Zod 派生的 JSON Schema)。也就是说今天 Inspector 上看到的不只是工具名,而是 LLM 客户端将来用来「决定调哪个工具、传什么参数」的完整契约——description 是自然语言提示、inputSchema 是参数约束。校验 tools/list 时除了核对 name 与顺序,也应顺带确认 inputSchema 与 Zod 声明一致。

对拍逻辑(手推):假设 Day 53 注册了工具 assessTypology,进程内 list() 返回 ["assessTypology"](只有 1 个时排序无歧义)。网络版 tools/list 经 Inspector 调出,应当返回同样的 name 集合 + 同样的顺序。判定步骤:

  1. 进程内:registry.list().map(s => s.name) → 得有序 name 数组 A。
  2. 网络版:Inspector 调 tools/list,取 result.tools.map(t => t.name) → 得有序 name 数组 B。
  3. 断言 A 深度等于 B(同元素 + 同顺序)。若注册了多个工具,这一步才真正检验排序稳定性。
  • handle(req)tools/list 分支(line 201-203):return { ...base, result: { tools: this.list() } }——网络信封里 result.tools 直接来自 list(),所以排序保证是从 list() 一路继承到网络响应的。

排序为何用 localeCompare 而非默认 sort()

  • 默认 Array.prototype.sort() 不传比较函数时按字符串 UTF-16 code unit 排,跨工具名可能给出反直觉次序。
  • localeCompare 给确定且符合语言习惯的字典序,工具名稳定可预测——这对「客户端缓存 + 对拍」更友好。
  • 关键不是「用哪种序」,而是「同一份工具集每次都给同一个序」——确定性才是契约本身。

多工具时的对拍才有意义:单个工具 ["assessTypology"] 排序无歧义;当 Day 56 注册 assessCase/draftSar/listTypologies 三个工具后,list() 会稳定输出 ["assessCase","draftSar","listTypologies"](按字典序),此时网络版若给出别的顺序,对拍立刻报错——这才是排序契约真正发挥作用的场景。

3. 今日实战

  1. 启动 Day 52/53 起的本地 server(确保 Day 53 的 Zod 工具已注册)。
  2. 用 MCP Inspector 连上:npx @modelcontextprotocol/inspector,在 UI 里填本地 server 的 transport(Streamable HTTP)与 endpoint。
  3. 在 Inspector 里点 List Tools(即调 tools/list),截图返回的工具数与名称
  4. 核对两点:(a) 工具数量与你注册的一致;(b) 顺序与 toolRegistry.tslist() 输出一致(按 name 字典序)。
  5. 可选对拍脚本:导出 Inspector 返回的 name 数组,与 registry.list().map(s=>s.name) 做深度相等断言。
  6. 用最新版 Inspector,确保支持 2026-07-28 stateless 语义。

4. 今日实测 / 产出

  • 待建——Inspector 截图:tools/list 返回 ≥1 工具(Day 53 注册的 assessTypology)。
  • 本地可验,无需 key
  • 可附一条对拍记录:进程内 registry.list().map(s=>s.name) 与 Inspector 返回 name 数组深度相等(顺序敏感)。

对拍的工程意义:今天确立的「网络 tools/list == 进程内 list()」不是形式主义。它让我们在后面 Day 56 把 AML 工具集批量上网时,能用进程内 mock 作为「金标准」对照——网络版多/少/错序了任何一个工具,对拍立刻暴露。没有这条契约,工具集越大越难保证「客户端看到的」与「我们以为暴露的」一致。

TTL 怎么定

  • TTL 太长 → 工具集更新后客户端长时间用旧清单(漏掉新工具、调到已下线工具)。
  • TTL 太短 → 退化成「每次 call 前都重拉 list」,缓存形同虚设、白白增加往返。
  • 实践上按工具集变更频率定:稳定的 AML 工具集可给较长 TTL(分钟级到小时级),快速迭代期给短 TTL;关键是客户端必须有「缓存过期重拉」逻辑,而非永久缓存。

5. 常见误区 / 陷阱

  • 假设 server 会主动推 listChanged:stateless 客户端不能依赖推送,应靠 TTL 主动重拉。
  • 忽视排序稳定:工具多了之后,若网络版顺序与 list() 不一致,缓存/对拍会误判——务必核对顺序,不只核对集合。
  • 依赖 Map 插入顺序当排序:插入顺序随注册时序变化,不是确定性的;必须显式 sortlist()localeCompare)。
  • 用旧版 Inspector:旧 Inspector 可能不懂 2026-07-28 stateless 语义,会误报握手/会话问题;用最新版。
  • tools/list 当成执行:它只发现不执行,看到工具不代表 tools/call 一定通——那是明天 Day 55 的事。

6. 学习资源(每条带 YYYY-MM)

  • MCP Specification — tools 章节(tools/list 网络语义 / 排序 / 缓存,草案 2026-07-28 预定定稿)
  • MCP Inspector — npx @modelcontextprotocol/inspector 官方调试工具文档(随 spec 升级,用最新版)
  • src/agent/mcp/toolRegistry.tslist()(2026-06)——稳定排序(按 name)实现,对拍基线
  • MCP TypeScript SDK — server 文档(2026-03)
  • MDN — String.prototype.localeCompare()(确定性字典序参考,长期有效)

7. 一句话回顾

  • 发现层契约:stateless tools/list = 无状态发现,对所有客户端/任意节点返回相同清单,可按 TTL 缓存。
  • 硬约束:排序稳定(list()localeCompare 按 name 排)——这是缓存一致性与 mock↔真 server 对拍的前提。
  • 刷新策略:客户端靠 TTL 主动重拉,不依赖 server 推 listChanged
  • 诚实状态:Inspector 截图为「待建」,本地可验、无需 key。

8. 在 B1→B18 能力曲线上的位置

  • B6 三件套之一:MCP 协议核心是 list(发现)/ call(执行)/ error(失败)三件套。今天攻下发现层,明天 Day 55 攻执行+错误层。
  • 承接 Day 53:Zod 派生的 inputSchema 正是 tools/list 返回里每条工具携带的契约——今天验证它能被客户端正确发现。
  • 通向 Day 56:今天确立的「网络 list == 进程内 list()」对拍契约,是 Day 56 批量上网 AML 工具时的金标准对照手段。
  • 一句话:发现层是 LLM 客户端「知道有哪些工具可用」的入口,排序稳定则是这个入口可缓存、可对拍的工程前提。

9. 给 hiring manager 的一句话价值

  • 「我能把同一套工具集在进程内 mock 与远端 MCP server 两处暴露,并用排序稳定契约自动对拍——保证客户端看到的工具清单与我声明的完全一致,这是工具集规模化后避免静默契约漂移的关键。」

SOTA检查 (2026-06 更新)

  • 当前主流:stateless tools/list + 客户端 TTL 缓存 + 稳定排序 + Inspector 对拍,仍是 SOTA 调试/验证路径。
  • 是否仍 SOTA:MCP Inspector 随 spec 升级;用最新版以支持 2026-07-28 stateless 语义。
  • 过时黑名单(AVOID):假设 server 会主动推送 listChanged 通知——stateless 客户端应靠 TTL 主动重拉。
  • 下次复查点:07-28 规范定稿后复验 tools/list 是否新增字段;Inspector 当周用最新版。

衔接

  • 昨天:Day 53 — Zod 工具 schema vs 现有 JsonSchema 子集(用 Zod 注册第一个工具)
  • 今天:Inspector 调 tools/list,验证网络发现层契约(数量/名称/顺序)与进程内 list() 一致
  • 明天:Day 55 — tools/call 网络往返 + content 封装(真正执行工具,验证 -32602 失败路径与 result.content 成功路径)