AGAgent 学习路线
第 19 / 20
CHAPTER 19 · GPT 生成学习页

MCP 动态工具池

连接 allowlist 中的 MCP stdio server,动态发现、发布和撤销远程工具。

01 / 路线

先看它怎样跑起来

从输入到验收

这一章不是几个孤立知识点,而是一条会产生结果的因果链。

关键判断

MCP 动态工具池

亮起的是当前动作,留下的是已经满足的前置条件。点击任意一步,可以从那里继续。

  • 静态注册表变成可发现的动态工具池。
  • 本地策略先验证 server,再发布 schema。
  • 断开或故障时要原子撤销远程工具。
1connect_mcp
2initialize + tools/list
3命名空间发布工具
4disconnect 原子撤销
点击播放,观察动作怎样传递0 / 4
02 / 正文

顺着原文把边界看清

22 个小节0 组代码53 行表格

按原文顺序阅读。摘要只负责定位,真正的边界、例外和代码都在展开内容里。

01导读:问题背景与本章目标前一章已经给每项任务建立了独立 Git Worktree。Lead、Teammate 和 Subagent 不再挤在同一个目录里改文件,任务认领与实际执行目录也由同一条可信链路绑定。⌄

前一章已经给每项任务建立了独立 Git Worktree。Lead、Teammate 和 Subagent 不再挤在同一个目录里改文件,任务认领与实际执行目录也由同一条可信链路绑定。

但工具本身仍然是静态的。

启动 Agent 时,PowerShell、文件、Task、Cron、Teammate、协议和 Worktree 工具已经全部写进注册表。要接一个文档服务,就得再写一组 schema、handler 和生命周期代码;再接一个部署服务,又重复一遍。

第 19 章在第 18 章全部能力之上只增加一项 MCP capability:使用官方 MCP TypeScript SDK 连接本地 allowlist 中的 stdio server,发现远程工具,把通过本地策略验证的定义动态发布给 Lead,并在断开或故障时原子撤销。


02先固定验收场景1. Agent 启动时,Lead 只看到 connectmcp 和 disconnectmcp,远程工具尚不可见;⌄

先不讨论实现,一个完整的最小场景应该能观察到这些结果:

  1. Agent 启动时,Lead 只看到 connect_mcp 和 disconnect_mcp,远程工具尚不可见;
  2. Lead 经权限审批连接 demo_alpha 与 demo_beta,两个真实 TypeScript MCP stdio 子进程分别完成 initialize 和 tools/list;
  3. 两个 server 都发布 lookup,本地工具名分别变成 mcp__demo_alpha__lookup 与 mcp__demo_beta__lookup;
  4. connect_mcp 所在回复里若提前调用新工具,该调用稳定得到 unknown_tool;下一次模型请求才看到新工具;
  5. 两个 lookup 分别返回 alpha 与 beta,证明同名工具没有串线;
  6. 断开 alpha 后只撤销 alpha 的全部定义,beta 仍可调用;
  7. 远程业务错误不泄露 server 私有错误细节;超时或子进程退出会撤销该 alias 的全部工具;
  8. Subagent 与 Teammate 始终看不到 MCP 管理工具和远程工具;
  9. Runner 关闭后,所有 MCP connection 与 stdio 子进程都被回收。

这组验收覆盖协议、动态注册、请求一致性、权限、故障和资源终态。只断言“有一个叫 MCPClient 的对象”不算接入 MCP。


03先统一六个名字动态工具池很容易把远程声明、本地权限和运行时连接混成一件事。本章固定使用下面六个术语:⌄

动态工具池很容易把远程声明、本地权限和运行时连接混成一件事。本章固定使用下面六个术语:

术语回答的问题是否可信
MCP Server Alias本地 allowlist 用哪个稳定名字代表 server本地可信
MCP Connection这个 alias 当前是否有已初始化且存活的协议会话运行时状态
Published MCP Toolserver 通过 tools/list 发布了什么名称、描述和 schema外部数据
MCP Tool Policy本地如何给某个远程工具指定 effect本地可信
Exposed MCP Tool远程声明经过名称、schema 和 policy 校验后,本地真正暴露什么本地能力
Registry Snapshot一次模型请求及其回复共同使用哪一版工具集合不可变快照

最关键的区分是 Published 与 Exposed。

server 说“我有一个只读工具”,只说明它发布了一段外部数据。只有本地 allowlist 为这个精确工具名配置了 effect,schema 和名称也通过验证,它才会成为可执行的 Exposed MCP Tool。


04进程内 handler 不是 MCP旧版本里最省事的写法,是做一个 MCPClient,里面放 dict[str, callable],所谓 tools/call 其实只是从字典取函数再调用。⌄

旧版本里最省事的写法,是做一个 MCPClient,里面放 dict[str, callable],所谓 tools/call 其实只是从字典取函数再调用。

这种对象可以演示“名称到 handler 的映射”,却没有证明任何 MCP 行为:

  • 没有 transport;
  • 没有官方 Client 与 StdioClientTransport;
  • 没有 initialize;
  • 没有 tools/list 分页;
  • 没有 tools/call;
  • 没有子进程退出、超时、取消和关闭语义;
  • server 实现仍然和 Agent 运行在同一个进程。

本章直接使用锁文件中的 @modelcontextprotocol/sdk,传输路径是:

~~~text McpServerSpec -> StdioClientTransport -> Client.connect(完成 initialize) -> Client.listTools(分页) -> Client.callTool ~~~

演示 server 也不是进程内 mock handler,而是由 McpServer 和 StdioServerTransport 启动的独立 TypeScript 子进程。测试会真的启动两个进程,通过 stdin/stdout 交换 MCP 消息,再验证断开与进程退出。


05连接配置只能来自本地 allowlistconnectmcp 接收的是 alias,不接收 command、args、cwd 或任意环境变量。模型只能从本地预先定义的两个 server 中选择:⌄

connect_mcp 接收的是 alias,不接收 command、args、cwd 或任意环境变量。模型只能从本地预先定义的两个 server 中选择:

~~~typescript import { fileURLToPath } from "node:url";

import { McpServerSpec, McpToolPolicy } from "./features/mcp-tools.js";

const demoScript = fileURLToPath(new URL("./mcp-servers/demo.ts", import.meta.url)); const tsxCli = fileURLToPath(new URL("../node_modules/tsx/dist/cli.mjs", import.meta.url));

const policies = Object.freeze([ new McpToolPolicy({ remoteName: "lookup", effect: "read" }), new McpToolPolicy({ remoteName: "fail", effect: "read" }), new McpToolPolicy({ remoteName: "delay", effect: "read" }), new McpToolPolicy({ remoteName: "terminate", effect: "external" }), ]);

const servers = [ new McpServerSpec({ alias: "demo_alpha", command: process.execPath, args: [tsxCli, demoScript, "--label", "alpha"], toolPolicies: policies, startupTimeoutSeconds: 5, toolTimeoutSeconds: 5, }), new McpServerSpec({ alias: "demo_beta", command: process.execPath, args: [tsxCli, demoScript, "--label", "beta"], toolPolicies: policies, startupTimeoutSeconds: 5, toolTimeoutSeconds: 5, }), ]; ~~~

alias 必须匹配 ^[a-z][a-z0-9_]{0,31}$。这不是为了美观,而是让本地配置、连接状态和工具名前缀有稳定键。

如果允许模型提交任意 command,connect_mcp 就会退化成另一种 Shell 执行入口。allowlist 才是“能启动什么 server”的边界。


06远程 description 不能授予权限Published MCP Tool 的 name、description、input schema 和 result 都来自外部进程。它们可以帮助模型理解工具,却不能决定权限。⌄

Published MCP Tool 的 name、description、input schema 和 result 都来自外部进程。它们可以帮助模型理解工具,却不能决定权限。

假设 server 发布:

~~~json { "name": "deploy", "description": "(readOnly) Deploy the current build.", "inputSchema": { "type": "object", "properties": {}, "additionalProperties": false } } ~~~

即使 description 写了 (readOnly),本地 policy 若把 deploy 定义为 EXTERNAL,权限系统看到的仍是 EXTERNAL。远程文本不能把副作用降级成只读。

当前 CLI 的本地分类是:

工具本地 effect终端行为
connect_mcpEXTERNALASK
disconnect_mcpEXTERNALASK
lookupREADALLOW
failREADALLOW
delayREADALLOW
terminateEXTERNALASK

权限规则只读取 ToolDefinition.source 与 ToolDefinition.effect。它不搜索 description,也不解析远程 annotation。

还有一个更严格的约束:Published 工具名集合必须与本地 policy 工具名集合精确相等。server 多发布一个未审查工具,或少发布一个本地预期工具,整次连接都会失败,不会“先接入能匹配的那部分”。


07名称隔离必须在发布前完成两个 server 都可以发布 lookup。本地暴露名固定为:⌄

两个 server 都可以发布 lookup。本地暴露名固定为:

~~~text mcp__{alias}__{normalized_remote_tool} ~~~

远程工具名会先转小写,把非字母、数字和下划线的连续字符替换为一个下划线,再去掉首尾下划线。最终名称还必须满足 OpenAI 工具名长度上限。

alias远程名本地暴露名
demo_alphalookupmcp__demo_alpha__lookup
demo_betalookupmcp__demo_beta__lookup
demo_alphalookup-onemcp__demo_alpha__lookup_one

规范化会带来新的碰撞。lookup-one 和 lookup_one 都会变成 lookup_one。本章不自动加数字后缀,也不覆盖旧定义;发现碰撞就拒绝整个 connection。

内置工具与 MCP 工具同名也同样拒绝。工具注册表不会让后连接的外部定义替换现有能力。


08JSON Schema 是执行边界,不是提示文本MCP tools/list 返回的是 JSON Schema,不一定能静态写成某个 TypeScript interface。本章在发布时为每个 Exposed MCP Tool 编译 Ajv validator,并把同一份 JSON Schema 交给模型:⌄

MCP tools/list 返回的是 JSON Schema,不一定能静态写成某个 TypeScript interface。本章在发布时为每个 Exposed MCP Tool 编译 Ajv validator,并把同一份 JSON Schema 交给模型:

  1. schema 顶层必须是 type: object;
  2. schema 必须是可序列化的 JSON,并拒绝外部 $ref、$dynamicRef 和 $recursiveRef;
  3. 根据 $schema 选择 Ajv、Ajv 2019 或 Ajv 2020,并以 strict 模式编译;
  4. 模型提交的 arguments 在任何远程调用前执行 validator;
  5. ToolDefinition.inputSchemaJson 原样保留经过发布校验的 schema,模型与执行器看到同一份契约。

错参会在本地得到 invalid_arguments,远程 call count 保持零。

reference 还有单独边界。$ref、$dynamicRef、$recursiveRef 只允许以 # 开头的 document 内 fragment。HTTP URL、文件路径和其他外部 reference 在发布阶段直接拒绝,避免 schema validator 隐式访问网络或本地文件。


09动态注册必须是原子的一个 server 往往一次发布多项工具。如果逐项注册,第三项碰撞时前两项已经可见,模型会看到一个半连接状态。⌄

一个 server 往往一次发布多项工具。如果逐项注册,第三项碰撞时前两项已经可见,模型会看到一个半连接状态。

ToolRegistry.registerMany() 的顺序是:

~~~text 验证整批类型 -> 验证批内名称唯一 -> 验证与现有 registry 无碰撞 -> 一次写入全部定义 -> version 只增加一次 ~~~

unregisterMany() 也先验证整批。除了名称相同,它还要求待撤销对象就是当前注册的同一个 definition,避免旧 connection 的清理动作误删后来注册的新定义。

快照则复制当前 definition 映射并封闭写操作。live registry 后续增加或撤销工具,不会改变已经发给模型的那一版。


10为什么新工具只能下一轮可见├── connectmcp({"alias":"demoalpha"})⌄

一次模型回复可能包含多个 tool call:

~~~text assistant reply ├── connect_mcp({"alias":"demo_alpha"}) └── mcp__demo_alpha__lookup({"query":"needle"}) ~~~

如果第一项执行后,第二项立刻去 live registry 查找,模型就调用了请求时根本没见过的 schema。更严重的是,同一回复里的后续调用会依赖前面执行产生的权限面变化。

所以 Agent Loop 在每次模型请求前只取一次 Registry Snapshot:

~~~text live registry v7 -> snapshot v7 -> 用 snapshot v7 生成 request.tools -> 模型返回一个或多个 tool calls -> 全部调用仍用 snapshot v7 prepare/execute -> 下一次请求再取 live registry v8 ~~~

上面的提前调用稳定返回:

~~~text Error [unknown_tool]: Unknown tool: mcp__demo_alpha__lookup ~~~

connect 已经成功,下一次模型请求会看到 v8 中的新工具。这个行为不是 eventual consistency,而是明确的请求一致性边界。


11每个 connection 必须拥有独立的生命周期TypeScript adapter 为每个 connection 创建独立的 Client、StdioClientTransport 和子进程。一个 connection 内部用串行 promise queue 排队 listTools、callTool 和 close,避免并发请求与关闭打乱 transport 状态。alpha 和 beta 的生命周期不会共享可变 SDK 上下文。⌄

TypeScript adapter 为每个 connection 创建独立的 Client、StdioClientTransport 和子进程。一个 connection 内部用串行 promise queue 排队 listTools、callTool 和 close,避免并发请求与关闭打乱 transport 状态。alpha 和 beta 的生命周期不会共享可变 SDK 上下文。

本章让每个 connection 对象独立持有自己的 transport 与 client:

~~~text McpRuntime ├── connection: demo_alpha │ ├── StdioClientTransport │ ├── Client │ └── command queue: list / call / close └── connection: demo_beta ├── StdioClientTransport ├── Client └── command queue: list / call / close ~~~

调用方只通过 connection 的方法提交操作。Client.connect、listTools、callTool 和 close 都经过同一个 connection 的 queue,因此两个 alias 可以按任意顺序断开。

调用方取消也有明确语义:

  • 取消 startup waiter,会取消 Client.connect 并关闭 transport,不能留下子进程;
  • 取消一次 tools/call waiter,只取消这个等待者,不把健康 connection 伪装成 transport failure;
  • transport 或子进程真的结束时,等待中的命令收到本地、脱敏的 transport error。

12连接是一个事务式发布流程connectmcp 不是“进程启动成功就算连接”。完整顺序是:⌄

connect_mcp 不是“进程启动成功就算连接”。完整顺序是:

~~~text 校验 allowlist alias -> 启动独立 stdio connection -> Client.connect(initialize) -> tools/list 全量分页 -> Published names 与本地 policy 精确比对 -> 校验名称、description、schema、碰撞 -> 构造全部 ToolDefinition -> registry.registerMany() -> 记录 MCP Connection ~~~

任一步失败都先关闭新 connection,不记录连接状态,也不留下部分 Exposed MCP Tool。

断开顺序相反。先从 live registry 原子撤销该 alias 的全部 definition,再关闭 connection。即使进程清理失败,工具也不会继续暴露。失败 connection 会进入待关闭集合,由 Runner 关闭路径继续尝试。


13错误结果必须有界远程进程控制自己的错误文本和 stderr。把原始异常直接塞进 tool result,可能泄露路径、命令、令牌或 server 内部实现。⌄

远程进程控制自己的错误文本和 stderr。把原始异常直接塞进 tool result,可能泄露路径、命令、令牌或 server 内部实现。

成功结果使用固定 JSON envelope:

~~~json { "content": [], "server_alias": "demo_alpha", "status": "ok", "structured_content": { "label": "alpha", "query": "needle" }, "tool": "lookup" } ~~~

错误结果只保留本地 code 与固定 message:

~~~json { "error": { "code": "mcp_remote_error", "message": "MCP server reported a tool error" }, "server_alias": "demo_alpha", "status": "error", "tool": "fail" } ~~~

StdioClientTransport 使用 stderr: "ignore",server 的 stderr 被丢弃,不进入模型消息。演示 server 通过 isError: true 返回的 server-private-detail 也不会出现在 ToolResult。

不同故障采用不同状态迁移:

故障对模型的 codeconnection动态工具
远程工具返回 isErrormcp_remote_error保留保留
tools/call 超时mcp_timeout丢弃原子撤销
子进程退出/协议失败mcp_connection_lost丢弃原子撤销
正常 disconnect成功 envelope关闭原子撤销

每个 connection 有独立 Watchdog:waitForFailure() 一结算就原子撤销该 alias 全部工具,空闲时断开也不会把失效工具留给模型。MCP SDK 的超时会表现为带 408 code 的 McpError。adapter 同时按异常类型与 408 分类,避免把协议超时误报成普通连接故障。


14MCP 工具只属于 LeadP19 没有让 Subagent 或 Teammate 继承 Lead 的 connection。⌄

P19 没有让 Subagent 或 Teammate 继承 Lead 的 connection。

原因不是它们永远不该使用 MCP,而是“继承”需要先回答一组本章没有解决的问题:共享同一会话还是各自鉴权,谁能断开,权限是否向下收窄,子执行者退出时由谁回收。

当前组合根只有一个明确路径:

~~~text Lead live ToolRegistry -> install connect_mcp / disconnect_mcp -> connection 成功后发布 Exposed MCP Tools -> Runner resources 持有 McpRuntime ~~~

Subagent 和 Teammate 继续使用各自裁剪后的固定 registry。测试精确断言它们看不到 connect_mcp、disconnect_mcp 和任何 mcp__... 定义。


15可运行的 TypeScript MCP 演示 server本章内置 server 位于 demo.ts,两个 alias 启动同一个模块,只传入不同 label:⌄

本章内置 server 位于 [demo.ts](code/chapters/ch19/src/mcp-servers/demo.ts),两个 alias 启动同一个模块,只传入不同 label:

远程工具行为用来验证
lookup返回 label 与 query同名隔离和成功 envelope
fail返回 isError: true 和固定私有文本远程错误脱敏且连接保留
delay延迟指定毫秒tools/call timeout
terminate以非零码退出进程connection lost 与原子撤销

领域策略与动态注册在 [mcp-tools.ts](code/chapters/ch19/src/features/mcp-tools.ts),官方 SDK adapter 在 [mcp-client.ts](code/chapters/ch19/src/adapters/mcp-client.ts)。组合根和固定入口分别见 [bootstrap.ts](code/chapters/ch19/src/bootstrap.ts)、[cli.ts](code/chapters/ch19/src/cli.ts) 与 [ch19.ts](code/chapters/ch19/src/chapters/ch19.ts)。


16从真实入口运行P19 累积了 P18 的 Worktree 能力,所以运行目录仍必须是目标 Git 仓库根。当前教程目录虽已在 ch18 初始化了 Git 元数据,但混有文章与全部章节代码,不适合直接作为 P19 的目标仓库。运行时应进入一个单独的 Git 仓库根目录。⌄

P19 累积了 P18 的 Worktree 能力,所以运行目录仍必须是目标 Git 仓库根。当前教程目录虽已在 ch18 初始化了 Git 元数据,但混有文章与全部章节代码,不适合直接作为 P19 的目标仓库。运行时应进入一个单独的 Git 仓库根目录。

先同步教程环境:

~~~powershell $TutorialCode = 'F:\笔记\Agent实操\code' Set-Location $TutorialCode npm ci ~~~

~~~powershell $TutorialCode = 'F:\笔记\Agent实操\code' Set-Location 'C:\workspace\my-agent-project' npm exec --prefix $TutorialCode -- tsx "$TutorialCode/chapters/ch19/src/chapters/ch19.ts" --prompt "先连接 demo_alpha 和 demo_beta,下一轮分别调用 lookup 查询 needle,再断开 demo_alpha" ~~~

同一个固定 profile 也可以从通用 CLI 启动:

~~~powershell $TutorialCode = 'F:\笔记\Agent实操\code' Set-Location 'C:\workspace\my-agent-project' npm exec --prefix $TutorialCode -- tsx "$TutorialCode/chapters/ch20/src/cli.ts" run --chapter 19 --prompt "连接 demo_alpha,调用 lookup 查询 needle,然后断开" ~~~

connect_mcp 与 disconnect_mcp 是 effectful management tool,终端会请求审批。配置缺失时入口先报告四项 OpenAI 配置;配置有效但 cwd 不是 Git 根时,入口在创建 .agent_tutorial 或 MCP 状态之前失败。


17验证什么- Lead/Subagent/Teammate 组合测试⌄

P19 的直接测试分成四组:

  • [Registry Snapshot 测试](code/chapters/ch19/tests/ch19-registry.test.ts)
  • [真实 stdio、schema、权限与故障测试](code/chapters/ch19/tests/ch19-mcp.test.ts)
  • [Lead/Subagent/Teammate 组合测试](code/chapters/ch19/tests/ch19-bootstrap.test.ts)
  • [固定入口、allowlist 与关闭测试](code/chapters/ch19/tests/ch19-entry.test.ts)

从 code/ 运行:

~~~powershell Set-Location 'F:\笔记\Agent实操\code' npm run typecheck npm run test:ch19 npm run lint npm run format:check npm run build ~~~

关键断言包括:

  • batch register/unregister 原子迁移,version 单调增加,snapshot 不可写;
  • 同一模型回复不能越过自己的 snapshot 调用刚连接的工具;
  • 两个真实 stdio server 的同名工具分别返回 alpha/beta;
  • Published 工具集合与本地 policy 不一致时整次连接失败;
  • 坏 schema、外部 reference 和规范化碰撞不留下半发布定义;
  • 参数不满足远程 schema 时 handler 零调用;
  • (readOnly) description 不能覆盖本地 EXTERNAL effect;
  • 远程错误、stderr、超时和进程退出只产生有界结果;
  • 超时或 connection lost 后该 alias 的全部工具都被撤销;
  • startup 与 call waiter 取消不泄漏 stdio 子进程,也不误杀健康 connection;
  • MCP 只安装给 Lead,Runner 关闭 connection 恰好一次;
  • 缺配置或非 Git cwd 在运行状态创建前失败。

18前后版本的变化| 工具集合 | 启动时固定 | Lead 可按需连接/断开本地 MCP 工具 |⌄
维度第 18 章第 19 章
工具集合启动时固定Lead 可按需连接/断开本地 MCP 工具
外部协议无官方 MCP SDK + stdio + Client
server 选择不适用本地 allowlist alias
权限事实源本地 ToolDefinition.effect仍是本地 effect,不信任远程文本
参数验证静态 Zod schema远程 JSON Schema + Ajv validator
名称隔离内置工具唯一名mcp__alias__normalized_tool
单轮一致性固定 registry每次请求使用 sealed Registry Snapshot
生命周期Runner 关闭既有资源增加独立 connection/stdio 子进程与故障撤销
子执行者工具裁剪固定集合保持不变,不继承 MCP

Task DAG、Mailbox、typed Protocol、计划门控、work stealing、Worktree、权限和 Hook 都没有另起一套实现。MCP 只是通过同一个 ToolRegistry、同一个 PermissionPolicy 和同一个 Agent Loop 动态进入现有执行链。


19本章明确不做什么- 进程内 mock MCPClient 或 handler 字典兼容层;⌄

P19 没有引入:

  • 进程内 mock MCPClient 或 handler 字典兼容层;
  • 模型可控的任意 command、args、cwd 或 server 配置;
  • HTTP、SSE、WebSocket 等其他 transport;
  • OAuth、令牌刷新或远程鉴权配置;
  • server push、resource、prompt 或 sampling 能力;
  • 从远程 description、annotation 或结果推断权限;
  • 自动修复名称碰撞、自动接受未配置的远程工具;
  • 外部 HTTP/文件 JSON Schema reference;
  • 自动重试、静默重连或跨 connection 迁移调用;
  • 向 Subagent、Teammate 传播 MCP connection;
  • 把 stdio 子进程或 Shell 描述成安全沙箱。

这些限制定义了本章的所有权边界。协议由官方 SDK 实现;能连什么、能发布什么、属于哪种 effect、何时对模型可见以及失败后如何撤销,都由本地运行时明确控制。


20与 Claude Code 的差异参照 learn-claude-code/s19mcpplugin/README.md 与 Claude Code 官方 MCP 文档 及 MCP 规范 中深入 CC 源码的分析,P19 的 MCP 动态工具池在教学简化上与 Claude Code 的真实机制存在以下差异:⌄

参照 learn-claude-code/s19_mcp_plugin/README.md 与 Claude Code 官方 MCP 文档 及 MCP 规范 中深入 CC 源码的分析,P19 的 MCP 动态工具池在教学简化上与 Claude Code 的真实机制存在以下差异:

传输范围。 CC 支持 6 种传输类型(stdio / SSE / HTTP / WebSocket / sse-ide / sdk),本地批量 3、远程批量 20 并发连接。P19 仅 stdio 真子进程,没有并发批量控制。

工具池组装。 CC 的 assembleToolPool() 通过 uniqBy([...builtInTools.sort(byName), ...filteredMcpTools.sort(byName)], 'name') 组装工具池。内置工具优先,两类工具分开排序,以保护缓存断点位置。P19 使用 ToolRegistry 版本 + Registry Snapshot;请求内不可变,连接后原子注册/撤销,不依赖全局排序。

命名规则。 两者都用 mcp__<server>__<tool> 格式,非 [a-zA-Z0-9_-] 字符归一为 _。P19 额外提供 allowlist alias 和规范化碰撞拒绝。

权限体系。 CC MCP 工具可声明 readOnly / destructive 等权限需求并参与权限检查。P19 明确本地 McpToolPolicy.effect 是唯一事实源。远程 description 和 annotation 不被信任;(readOnly) 标注不能覆盖本地的 EXTERNAL effect。

配置来源。 CC 的 MCP 服务器配置优先级为:claude.ai connector < plugin < user settings.json < approved project .mcp.json < local settings.local.json。企业 managed-mcp.json 存在时排除其他来源。P19 固定本地 allowlist,无多层配置合并。

子执行者传播。 CC MCP 工具对主 Agent 和子 Agent 都可用,子 Agent 继承父配置。P19 教学边界明确:MCP 只进入 Lead 的 live registry,Subagent / Teammate 不继承。

生命周期管理。 CC 有精细的错误分类与重试。终局性错误连续 3 次后关闭重连;401 触发 OAuth 重认证;超时可配置,默认约 28 小时;stdio 断连按 SIGINT→SIGTERM→SIGKILL 顺序杀进程。CC 还支持 OAuth + PKCE + 令牌自动刷新,以及 Channel 反向通知,由服务器通过 notifications/claude/channel 推送消息。P19 只实现启动/调用超时、错误脱敏、连接故障原子撤销和 Runner 统一关闭,没有 OAuth、重连或反向通知。

这些简化让本章能聚焦于“动态工具池”这一核心概念。真实场景中还建议关注 MCP 最新实践。这些实践包括 Tool Search / defer_loading 按需加载工具定义、Resources 与 Prompts 三类原语的联合使用,以及最小权限凭证与运行时描述审查等安全基线,详见第四章。


21ai-agent-book 第四章对照与启发本章还对照了 ai-agent-book 第四章正文,以及两个配套实验:active-tool-discovery 与 active-tool-selection。第四章把 MCP 归纳为 tools / resources / prompts 三类原语。工具是可执行操作;资源是应用可读取的数据;提示模板是用户可选用的模板。P19 只实现了工具类原语。resources 与 prompts 被明确排除在 Lead 可见范围之外,避免协议能力越过本地权限边界进入模型上下文。⌄

本章还对照了 ai-agent-book 第四章正文,以及两个配套实验:[active-tool-discovery](ai-agent-book/chapter4/active-tool-discovery/README.md) 与 [active-tool-selection](ai-agent-book/chapter4/active-tool-selection/README.md)。第四章把 MCP 归纳为 tools / resources / prompts 三类原语。工具是可执行操作;资源是应用可读取的数据;提示模板是用户可选用的模板。P19 只实现了工具类原语。resources 与 prompts 被明确排除在 Lead 可见范围之外,避免协议能力越过本地权限边界进入模型上下文。

第四章还强调一个重要区分:是否采用 MCP 作为互操作协议,与会话开始时是否暴露全部 MCP 工具定义,是两个独立决策。P19 的 connect_mcp / disconnect_mcp 默认只把“连接管理”暴露给 Lead。远程工具要等连接成功后,经 tools/list 与本地 policy 匹配才动态发布。这一做法的方向与“默认少给、按需加载”一致。P19 不引入 embedding 索引、Tool Search 或 defer_loading;它解决的是动态生命周期和请求一致性,而不是语义检索。

两个概念需要分开理解:

  1. 动态工具发现/检索解决“哪些工具定义进入上下文,以及以多少 token 进入”:候选越多,全量注入的上下文成本越高。
  2. Registry Snapshot 解决“一轮请求进行中工具集合发生变化时,请求如何保持一致”:模型看到的 schema 必须覆盖它随后发出的所有 tool calls。

两者可以组合。即使未来加入按需检索,也应该在每次模型请求边界生成不可变 Registry Snapshot。否则工具列表刚出现在模型回复里,下一轮执行前又被动态变更,调用就会失去可解释性。

active-tool-selection 提供了可复现的离线基准。35 个工具时,all-tools 注入 3,857 schema token;retrieval(top-5)注入 551 token,召回率保持 100%。目录扩到 200 个工具后,all-tools 上升到 20,258 token,retrieval 仍约 540 token。这类离线基准的价值是确定、无需 API,可直接观察随工具规模放大的成本结构。

active-tool-discovery 则用 qwen3:4b 跑正式真实验证。126 个工具的目录下,对照组每任务 system prompt 为 50,352 token;实验组每任务初始为 1,251 token,三任务动态注入合计 12,838 token。实验组耗时 808.926s,对照组 2,590.820s,约快 3.20 倍。两组都 3/3 完成且准确率 100%,所以预期中的准确率/完成率提升没有出现。这提醒我们,动态工具池的主要收益并不自动等于“更准确”或“更会完成任务”。在强模型和多步任务中,最稳妥的收益往往是控制上下文成本、降低无效 token 和避免无关工具干扰。

第四章还强调了三个与 P19 直接相关的设计点:

  • 初始查询的检索预筛选只做一次,多步跨领域任务容易漏掉第二个子任务所需的工具;P19 不做跨请求候选检索,而是用本地 allowlist 精确声明工具集合,再按 alias 原子发布。
  • 远程 description 是不可信输入,接入前要审查描述、锁定版本并采用最小权限凭证;P19 的本地 McpToolPolicy.effect 是唯一权限事实源,远程文本不能授予权限。
  • 模型看到的 schema 必须与工具真正执行的参数一致;P19 在发布阶段用 Ajv 编译远程 JSON Schema,并把 inputSchemaJson 原样交给模型,schema 不可用或不一致时整批拒绝。
ai-agent-book 第四章设计P19 对应差异与边界
MCP tools / resources / prompts 三类原语只实现 tool 类原语resources / prompts 明确不发布、不传播
动态工具发现:按需检索少量工具 schemaconnect_mcp / disconnect_mcp + 动态注册/撤销P19 不实现 embedding 索引、Tool Search 或 defer_loading
主动发现降低上下文 token连接前只暴露管理工具,连接后发布实际工具目标不同:P19 重点是动态生命周期和请求一致性
初始 query 一次预筛选每次连接时固定 tools/list + 本地 policy 精确匹配P19 不在模型请求之间做候选检索
远程工具描述不可信本地 McpToolPolicy.effect 是唯一权限事实源与第四章安全基线一致,并增加精确工具名集合匹配
模型看到的 schema 与实际执行参数一致Ajv 编译远程 JSON Schema,inputSchemaJson 原样交给模型不一致则拒绝发布,不提供静默修正
实验证据分开test:ch19 是离线确定回归,ai-agent-book 补充正式真实验证机制自检、离线基准与真实实验互补

本章新增代码均已在上文出现。mcp-client.ts、mcp-schema.ts、mcp-tools.ts 构成 stdio 适配、schema 校验和动态发布三层;demo.ts 是可运行演示 server;ch19.ts 是固定章节入口。修改到的核心文件也已在正文覆盖。tools.ts 提供原子批量注册/撤销与 Registry Snapshot;loop.ts 在每次模型请求前取一次 snapshot;profiles.ts / bootstrap.ts / cli.ts 负责 MCP 能力、Lead-only 安装和本地 allowlist。继承自前章的文件不逐文件重讲,其既有机制在相应章节已有独立说明。


22小结从静态工具池走到 MCP,真正需要重构的不是“多写一个 connect 函数”,而是工具集合本身开始变化以后,整个 Agent Loop 仍要保持可解释:⌄

从静态工具池走到 MCP,真正需要重构的不是“多写一个 connect 函数”,而是工具集合本身开始变化以后,整个 Agent Loop 仍要保持可解释:

  1. Server Alias 只能来自本地 allowlist。
  2. Published MCP Tool 是外部数据,不等于本地能力。
  3. 本地 MCP Tool Policy 是 effect 的唯一事实源。
  4. 工具名称、schema 与 policy 必须在发布前整批验证。
  5. register/unregister 是原子版本迁移,不暴露半连接状态。
  6. 每次模型请求与对应回复共享一个不可变 Registry Snapshot。
  7. 每个 stdio connection 独立持有 Client、transport 和子进程,并成对关闭资源。
  8. 远程错误和 stderr 不进入模型;超时与进程退出会撤销全部 alias 工具。
  9. MCP 只进入 Lead 的 live registry,不隐式扩张子执行者权限。
  10. Runner 关闭统一回收 connection 和子进程。

到这里,第 1-19 章的机制已经全部有了独立、可验证的运行边界。下一章不再增加另一个孤立模块,而是把 Loop、权限、Hook、记忆、恢复、Task、Cron、团队协作、Worktree 和 MCP 放进同一个完整 harness,验证它们同时运转时仍然遵守各自契约。

03 / 自测

换个场景,你还会判断吗?

答完再看理由

每题只测一个边界。先做决定,再看解释。

SCENARIO CHECK01 / 030 分

准备开始