第 十九 章 Agent 架构实操 深入学习 · 交互式 含 QA 测试

从静态工具到动态工具池:一次 MCP 接入让我重构了 Agent 架构

💡 MCP:本地 allowlist 决定能连什么;Registry Snapshot 保证请求一致。
用官方 MCP TypeScript SDK 连接本地 allowlist 的 stdio server,发现远程工具,把通过本地策略验证的定义动态发布给 Lead,并在断开或故障时原子撤销。远程描述不能授予权限。
本章进度
0%
1 本章要掌握的目标
  • 连接配置只能来自本地 allowlist(alias),不接受 command/args/cwd。
  • Published(外部数据)≠ Exposed(本地能力);本地 McpToolPolicy.effect 是唯一权限事实源。
  • 名称隔离 mcp__{alias}__{normalized_tool};规范化碰撞拒绝整个连接。
  • 每次模型请求与对应回复共享一个不可变 Registry Snapshot;新工具下一轮才可见。
  • 远程错误脱敏;超时/进程退出原子撤销;MCP 只进入 Lead。
2 核心知识点
先统一六个名字

MCP Server Alias(本地可信)、MCP Connection(运行时状态)、Published MCP Tool(外部数据)、MCP Tool Policy(本地可信 effect)、Exposed MCP Tool(本地能力)、Registry Snapshot(不可变工具集合)。

Published = 外部数据(能做什么未知)
Exposed = 经 alias + 名称隔离 + policy 转换后的本地能力
本地 allowlist

connect_mcp 接收 alias,不接收 command/args/cwd。alias 必须匹配 ^[a-z][a-z0-9_]{0,31}$。如果允许任意 command,connect_mcp 就退化成另一种 Shell 入口。

const servers = [
  new McpServerSpec({
    alias: "demo_alpha",
    command: process.execPath,
    args: [tsxCli, demoScript, "--label", "alpha"],
    toolPolicies: policies,
    startupTimeoutSeconds: 5, toolTimeoutSeconds: 5,
  }),
];
远程 description 不能授予权限

即使远程描述写着 (readOnly),本地 policy 若定义为 EXTERNAL,权限看到的仍是 EXTERNAL。权限只读 ToolDefinition.source 与 effect,不解析远程 annotation。且 policy 集合与远程申报集合必须精确相等(不多不少,不是取交集):远程少发或多发一个都拒绝整条连接,否则就会出现「半连接」状态。

本地分类:
  lookup     READ    -> ALLOW
  delay      READ    -> ALLOW
  terminate  EXTERNAL-> ASK
((readOnly) 描述不能覆盖本地 EXTERNAL effect)
名称隔离与 schema 边界

mcp__{alias}__{normalized}:远程名转小写、连续非字母数字下划线替换为 _。JSON Schema 顶层必须 type:object,拒绝外部 $ref(只能 # 开头 fragment),用 Ajv 编译 validator 在 handler 前校验参数。

demo_alpha lookup       -> mcp__demo_alpha__lookup
demo_beta  lookup       -> mcp__demo_beta__lookup
lookup-one/ lookup_one -> 都变 lookup_one → 碰撞拒绝
为什么新工具只能下一轮可见

模型回复可能同时含 connect_mcp 与 mcp__xxx__lookup。所有调用仍用请求时的 snapshot v7 prepare/execute;提前调用稳定得到 unknown_tool;下一次请求再取 live registry v8。registerMany/unregisterMany 整批原子:一批要么全成功要么一个不留;撤销按对象身份而不只是名字——同名工具被另一连接重新注册过,按名撤销会误删别人的。

live registry v7 → snapshot v7 → request.tools
模型返回多个 tool calls → 全部用 snapshot v7
下一次请求再取 live registry v8
(证明:不是 eventual consistency,是请求一致性)
三类故障,三种迁移;错误信封本地造

远程 isError:true → mcp_remote_error,连接保留;调用超时 → mcp_timeout,连接丢弃;子进程退出/transport 断 → mcp_connection_lost,连接丢弃,空闲时由 Watchdog 主动撤销该 alias 全部工具。错误信封字段全部本地构造,远程私有细节(如 server-private-detail)一律不进模型上下文;prepare() 先 JSON 解析 + Ajv 校验,坏参数不出网。不承诺 exactly-once:超时判失败后远程副作用可能已发生。

3 机制流程
1
连接

校验 allowlist → 启动 stdio → Client.connect → tools/list 全量分页。

2
发布

Published 集合与 policy 精确比对 → 校验名称/schema/碰撞 → registerMany 原子发布。

3
调用

调用用当时的 Registry Snapshot;Ajv 校验参数后再 callTool。

4
断开/故障

先原子撤销该 alias 全部工具,再关闭 connection;超时/进程退出也撤销。

4 术语表
MCP Server Alias本地 allowlist 中的稳定名字 ^[a-z][a-z0-9_]{0,31}$。
Published MCP Toolserver 发布的远程声明(外部数据)。
Exposed MCP Tool经本地验证后进入工具池的能力。
Registry Snapshot一次请求及其回复共同使用的不可变工具集合;live 与 snapshot 分离。
McpToolPolicy本地为远程工具指定的 effect 分类,权限唯一事实源;与申报精确相等。
Watchdog连接级看门狗:waitForFailure() 一结算就撤销该 alias 全部工具。
关闭顺序resources 逆序,mcpRuntime 最先关;锁内撤销,锁外逆序 close,多失败 AggregateError 一次报全。
5 QA 测试环节(自测题)
已完成 0 / 6 · 答对 0
Q1. 模型能通过 connect_mcp 指定任意 command 吗?
Q2. Published 不等于 Exposed 的含义是?
Q3. 同一回复里 connect 后立即调用新工具会?
Q4. 远程 description 写着 (readOnly) 但本地 policy 是 EXTERNAL?
Q5. 名称规范化碰撞(lookup-one vs lookup_one)会?
Q6. MCP 工具与 Subagent/Teammate 的关系是?
6 验证与实验
  • npm run typecheck && npm run test:ch19:63 个测试文件(含前 18 章累积,全程离线);Lead 工具面 +2(connect_mcp / disconnect_mcp,均 external),Subagent/Teammate 闭包里没有 mcpRuntime。
  • 验证真实 stdio 进程、同一回复 snapshot(同轮抢跑 = unknown_tool)、policy 精确相等、名称隔离、错误脱敏(远程私有细节不进上下文)、三类故障迁移、Lead-only。
  • 在独立 Git 仓库根运行两个 alias 分别 lookup 返回 alpha/beta。