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_mcpdisconnect_mcp,远程工具尚不可见;
  2. Lead 经权限审批连接 demo_alphademo_beta,两个真实 TypeScript MCP stdio 子进程分别完成 initialize 和 tools/list;
  3. 两个 server 都发布 lookup,本地工具名分别变成 mcp__demo_alpha__lookupmcp__demo_beta__lookup
  4. connect_mcp 所在回复里若提前调用新工具,该调用稳定得到 unknown_tool;下一次模型请求才看到新工具;
  5. 两个 lookup 分别返回 alphabeta,证明同名工具没有串线;
  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;
  • 没有官方 ClientStdioClientTransport
  • 没有 initialize;
  • 没有 tools/list 分页;
  • 没有 tools/call;
  • 没有子进程退出、超时、取消和关闭语义;
  • server 实现仍然和 Agent 运行在同一个进程。

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

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

演示 server 也不是进程内 mock handler,而是由 McpServerStdioServerTransport 启动的独立 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.sourceToolDefinition.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-onelookup_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 创建独立的 ClientStdioClientTransport 和子进程。一个 connection 内部用串行 promise queue 排队 listToolscallToolclose,避免并发请求与关闭打乱 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.connectlistToolscallToolclose 都经过同一个 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_mcpdisconnect_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_mcpdisconnect_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.mdClaude 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.tsmcp-schema.tsmcp-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 分

准备开始