s01 → ... → s17 → s18 → s19
"能力不够? 插上 MCP" — 多传输、通道路由、工具池合并。
Harness 层: 插件 — 外部能力通过标准协议接入。
从 s01 到 s18,Agent 用的所有工具都是你手写的——bash、read、write、task、todo_write。你自己写代码实现每个工具的输入验证、执行逻辑、错误处理。
但现在你有 3 个外部服务想接入 Agent:公司的 Jira API(查 issue、创建 ticket)、自建的部署系统(触发 deploy、查看日志)、团队的 Notion 知识库(搜索文档、创建页面)。你不想为每个服务重写一套工具代码。
你需要一套标准协议——只要外部服务实现了这个协议,Agent 就能直接调用它的工具,不管服务是用什么语言写的。
MCP(Model Context Protocol)定义了 Agent 如何发现和调用外部工具。核心概念:
| 概念 | 作用 |
|---|---|
| MCPClient | Agent 这边的客户端,连接 server、发现工具、调用工具 |
| MCP Server | 外部服务那边,实现 tools/list + tools/call |
| assemble_tool_pool | 把内置工具和 MCP 工具合并成一个池子 |
| mcp__server__tool 命名 | 避免不同 server 的工具名冲突 |
s18 的全部能力保留(worktree 隔离、自主认领、空闲轮询、协议系统)。新增一样:connect_mcp 工具——连接外部服务,发现工具,合并到工具池。
class MCPClient:
def __init__(self, name: str):
self.name = name
self.tools: list[dict] = []
self._handlers: dict[str, callable] = {}
def register(self, tool_defs, handlers):
"""Simulates tools/list discovery."""
self.tools = tool_defs
self._handlers = handlers
def call_tool(self, tool_name: str, args: dict) -> str:
"""Simulates tools/call."""
handler = self._handlers.get(tool_name)
if not handler:
return f"MCP error: unknown tool '{tool_name}'"
return handler(**args)教学版用 mock handler 模拟 stdio JSON-RPC。真实版会启动子进程,通过 stdin/stdout 发送 tools/list 和 tools/call 请求。
def connect_mcp(name: str) -> str:
"""Connect to an MCP server and discover its tools."""
factory = MOCK_SERVERS.get(name)
mcp_client = factory()
mcp_clients[name] = mcp_client
tool_names = [t["name"] for t in mcp_client.tools]
return f"Connected to '{name}'. Discovered: {', '.join(tool_names)}"连接后,server 提供的工具立即可用。
def assemble_tool_pool() -> tuple[list[dict], dict]:
tools = list(BUILTIN_TOOLS)
handlers = dict(BUILTIN_HANDLERS)
for server_name, mcp_client in mcp_clients.items():
for tool_def in mcp_client.tools:
prefixed = f"mcp__{server_name}__{tool_def['name']}"
tools.append({
"name": prefixed,
"description": tool_def.get("description", ""),
"input_schema": tool_def.get("inputSchema", {}),
})
handlers[prefixed] = (
lambda **kw, c=mcp_client, t=tool_def["name"]: c.call_tool(t, kw))
return tools, handlers前缀 mcp__{server}__{tool} 避免不同 server 的工具名冲突。调用 connect_mcp 后,agent_loop 自动重新 assemble,新工具立即可用。
| 组件 | 之前 (s18) | 之后 (s19) |
|---|---|---|
| 工具来源 | 全部手写 builtin | 手写 + MCP 外部工具动态发现 |
| 工具池 | 固定 BUILTIN_TOOLS | assemble_tool_pool 动态合并 mcp__ 前缀工具 |
| 新类型 | — | MCPClient 类(模拟 tools/list + tools/call) |
| 命名空间 | — | mcp__server__tool 避免冲突 |
| Lead 工具 | 16 (s18) | 17 (+connect_mcp) |
| 扩展方式 | 写代码加工具 | 标准协议,任意语言实现 server |
cd learn-claude-code
python s19_mcp_plugin/code.py试试这些 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 的工具是否同时可用?
这是最后一章。回顾你走过的路:
s01-s04 工具管线 loop → dispatch → permission → hooks
s05-s08 单Agent能力 planning → subagent → skill → compact
s09-s11 知识与韧性 memory → prompt → error recovery
s12-s14 持久化工作 task graph → background → cron
s15-s19 多Agent平台 teams → protocols → autonomy → worktree → MCP
你已经从零构建了一个完整 Agent 的 harness。19 个章节,每个只加一个机制。每个机制都挂在同一个 while True 循环上——循环本身,从未改变。
深入 CC 源码
以下基于 CC 源码
services/mcp/client.ts(3348 行)、auth.ts(2466 行)、config.ts(1579 行)、channelNotification.ts(317 行)的完整分析。
教学版只展示了 stdio。CC 支持 6 种传输(types.ts:23-25):
| Transport | 通信方式 |
|---|---|
stdio |
子进程 stdin/stdout(跨平台默认) |
sse |
HTTP Server-Sent Events |
http |
Streamable HTTP(POST/SSE 双向) |
ws |
WebSocket |
sse-ide |
IDE 内嵌 SSE 传输 |
sdk |
进程内 SDK 传输 |
连接时本地(stdio)和远程(http/sse/ws)服务器分批并发:本地批量 3 个,远程批量 20 个。
assembleToolPool()(tools.ts:345-367):
// 去重时优先保留内置工具(name 相同时内置在前)
return uniqBy(
[...builtInTools.sort(byName), ...filteredMcpTools.sort(byName)],
'name',
)关键细节:内置工具和 MCP 工具分开排序,不是合起来排。原因是 CC 的 claude_code_system_cache_policy 在最后一个内置工具之后的某个位置放全局缓存断点——混排会破坏这个设计。
buildMcpToolName()(mcpStringUtils.ts:50-52):
mcp__<normalizedServerName>__<normalizedToolName>
所有非 [a-zA-Z0-9_-] 字符替换为 _。例如 slack 服务器的 post_message → mcp__slack__post_message。
教学版只讲了 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 确认/拒绝。
CC 的 MCP 认证(auth.ts,2466 行)支持完整的 OAuth 2.0 + PKCE 流程:
- 通过公钥客户端 + PKCE 发现 OAuth 元数据(RFC 8414 / RFC 9728)
- 本地回调服务器接收授权码
- 令牌通过
getSecureStorage()持久化(macOS Keychain / Linux 加密文件 / Windows 凭据管理器) - 过期前 5 分钟自动刷新
- 支持跨应用访问(XAA):浏览器获取 id_token → RFC 8693 + RFC 7523 交换 → 无需反复弹浏览器
MCP 服务器配置来自 6 个来源,优先级从低到高(config.ts:1071-1251):
插件 > claude.ai 连接器 > 用户 settings.json > 项目 .mcp.json > 本地 settings.local.json
同名服务器按内容签名去重。企业 managed-mcp.json 存在时完全排除其他配置。
CC 对 MCP 连接有精细的错误分类和重试(client.ts:1266-1402):
- 终局性错误(ECONNRESET、ETIMEDOUT、EPIPE 等):连续 3 次 → 关闭 + 重连
- 工具调用 401:令牌过期 → 抛出
McpAuthError→ 触发重认证 - 工具调用超时:
Promise.race超时(可配置,默认 ~28 小时) - Stdio 断连:按 SIGINT → SIGTERM → SIGKILL 顺序杀进程
- 6 种 transport → 1 种(stdio):概念量可控
- Channel 反向通知 → 省略:教学版 Agent 是主动方
- OAuth 流程 → 省略:教学版假设 server 不需要认证
- 6 层配置优先级 → 省略:教学版直接传 server_command
- 复杂的错误分类 → 省略:教学版用 try/except 兜底