s19: MCP Tools — 外接工具,标准协议
s01 → ... → s17 → s18 → s19 → s20
"外接工具, 标准协议" — 发现、组装、调用,Agent 不需要知道工具是谁写的。
Harness 层: 插件 — 外部能力通过标准协议接入。
问题
s01 到 s18,Agent 的所有工具都是手写的——bash、read、write、task、worktree。每个工具的输入验证、执行逻辑、错误处理,都是你一行行写的。
现在你有 3 个外部服务想接入:公司的 Jira API(查 issue、建 ticket)、自建的部署系统(触发 deploy、看日志)、团队的 Notion 知识库(搜文档、建页面)。你不想为每个服务重写一套工具代码。
你需要一个标准协议——外部服务只要实现它,Agent 就能直接调用,不管服务用什么语言写的。
解决方案
)
MCP(Model Context Protocol)定义了 Agent 如何发现和调用外部工具。核心概念:
沿用 s18 的教学版 worktree 隔离、自主认领、空闲轮询、协议系统。本章新增:connect_mcp 工具——连接外部服务,发现工具,加入工具池。
教学版用 mock handler 模拟外部 server。真实版会启动子进程,通过 stdin/stdout 发送 JSON-RPC 请求。mock 的好处是不依赖外部服务就能跑完整流程;代价是你看不到真正的网络通信和进程管理。
工作原理
MCPClient:发现 + 调用
教学版用 Python 函数模拟 server 的工具实现。真实版通过 stdio JSON-RPC 与子进程通信。
connect_mcp:连接 + 发现
连接后,server 提供的工具立即可用。
normalize_mcp_name:名称规范化
所有非 [a-zA-Z0-9_-] 的字符替换为 _。防止 server 名或工具名中包含特殊字符导致命名冲突或注入问题。
assemble_tool_pool:组装工具池
前缀 mcp__{server}__{tool} 避免不同 server 的工具名冲突。名称经过 normalize_mcp_name 规范化。
MCP 工具的 description 带 (readOnly) 或 (destructive) 标注——教学版用文本标注,真实 CC 用 tool annotations 结构体让权限系统判断。
无缓存:工具池变了,prompt 也变
s10-s18 的 agent_loop 用 prompt cache 避免重复序列化。s19 去掉了缓存:
原因:connect_mcp 之后工具池变化了——新增了 mcp__docs__search 等工具。缓存中的工具列表是旧的,继续用会导致模型调用不到新工具。教学版直接去掉缓存,代价是多花一点序列化时间。
MCP 工具只有 Lead 可用
教学版中,connect_mcp 是 Lead 工具,assemble_tool_pool 也只服务于 Lead 的 agent_loop。Teammate 仍使用固定的 8 个子集工具(bash、read_file、write_file、send_message、submit_plan、list_tasks、claim_task、complete_task)。
这是教学简化。真实 CC 中,MCP 工具对主 agent 和子 agent 都可用——子 agent 继承父级的 MCP 配置。
相对 s18 的变更
试一下
试试这些 prompt:
Connect to the docs MCP server and search for somethingConnect to the deploy server and trigger a deploymentConnect both servers — what tools are now available?
观察重点:连接 MCP server 后,工具名是否带 mcp__docs__ 或 mcp__deploy__ 前缀?两个 server 的工具是否同时可用?MCP 工具的 description 是否带 (readOnly)/(destructive) 标注?
接下来
现在 Agent 可以通过标准协议接入外部工具了。但前面 19 章每章都只加一个机制,真实 Agent 不会这样拆开运行。
工具、权限、hooks、todo、任务图、记忆、压缩、后台、cron、团队、worktree、MCP 这些机制应该挂在同一个循环上,而不是散在 19 个 demo 里。
s20 Comprehensive Agent → 把前 19 章的机制合回一个完整 harness。机制很多,循环一个。
深入 CC 源码
以下基于 CC 源码
services/mcp/client.ts、auth.ts、config.ts、channelNotification.ts的分析。
一、6 种 Transport 类型
教学版只展示了 stdio mock。CC 支持 6 种传输(types.ts:23-25):
连接时本地(stdio)和远程(http/sse/ws)服务器分批并发:本地批量 3 个,远程批量 20 个。
二、工具池组装算法
assembleToolPool()(tools.ts:345-364):
内置工具和 MCP 工具分开排序,不是合起来排。原因是 CC 的 claude_code_system_cache_policy 在最后一个内置工具之后的某个位置放全局缓存断点——混排会破坏这个设计。
三、命名规则:mcp__server__tool
buildMcpToolName()(mcpStringUtils.ts:50-52):
所有非 [a-zA-Z0-9_-] 字符替换为 _(normalization.ts:17-23)。教学版的 normalize_mcp_name 用同样的规则。
四、权限检查
CC 对 MCP 工具有独立的权限系统。checkPermissions() 对 MCP 工具的检查逻辑不同于内置工具——MCP 工具可以声明自己的权限需求(readOnly、destructive 等),CC 根据声明决定是否需要用户确认。教学版只在 description 中用文本标注 (readOnly) / (destructive),不做权限拦截。
五、配置来源与优先级
MCP 服务器配置来自多个来源。CC 的配置优先级从低到高:
claude.ai 连接器单独拉取、按内容签名去重,以最低优先级合并(config.ts:1267-1289)。企业 managed-mcp.json 存在时完全排除其他配置。
教学版直接传 server name 给 MOCK_SERVERS 字典,不做配置合并。
六、Channel 通知:服务器反向推消息
教学版只讲了 Agent → MCP Server 的单向调用。CC 还支持反向通知(channelNotification.ts):
- Server 声明
capabilities.experimental['claude/channel'] - Server 通过 MCP 通知
notifications/claude/channel给 Agent 发消息 - 消息包装在
<channel source="serverName">...</channel>XML 标签中 - Agent 被 SleepTool 唤醒(1 秒内)
Server 还可以请求权限:notifications/claude/channel/permission_request → Agent 回复 notifications/claude/channel/permission。用户通过 5 字母短 ID 确认/拒绝。
七、OAuth 认证流程
CC 的 MCP 认证(auth.ts)支持完整的 OAuth 2.0 + PKCE 流程:
- 通过公钥客户端 + PKCE 发现 OAuth 元数据(RFC 8414 / RFC 9728)
- 本地回调服务器接收授权码
- 令牌通过
getSecureStorage()持久化(macOS Keychain / Linux 加密文件 / Windows 凭据管理器) - 过期前 5 分钟自动刷新
- 支持跨应用访问(XAA):浏览器获取 id_token → RFC 8693 + RFC 7523 交换 → 无需反复弹浏览器
八、连接生命周期的错误处理
CC 对 MCP 连接有精细的错误分类和重试(client.ts:1266-1402):
- 终局性错误(ECONNRESET、ETIMEDOUT、EPIPE 等):连续 3 次 → 关闭 + 重连
- 工具调用 401:令牌过期 → 抛出
McpAuthError→ 触发重认证 - 工具调用超时:
Promise.race超时(可配置,默认约 28 小时) - Stdio 断连:按 SIGINT → SIGTERM → SIGKILL 顺序杀进程
教学版的简化
- 6 种 transport → 1 种(mock stdio):概念量可控
- Channel 反向通知 → 省略:教学版 Agent 是主动方
- OAuth 流程 → 省略:教学版假设 server 不需要认证
- 多层配置优先级 → 省略:教学版直接传 server name
- 复杂的错误分类 → 省略:教学版用 try/except 兜底
- MCP 工具只给 Lead → 省略子 agent 继承:简化代码结构
