第六章:MCP 与扩展
学习时间: 3 小时 | 难度: ⭐⭐⭐ | 前置: 第三章
学习目标
完成本章后,学员将能够:
- 解释 MCP 客户端的 8 种传输层架构
- 描述工具发现和调用的完整流程
- 理解 Hook 系统的 20+ 事件和执行管道
- 应用 Strategy、Adapter、Event-Driven Middleware 模式
1. MCP 客户端架构
mcp/client.ts (3351行) 实现了 Claude Code 的 MCP (Model Context Protocol) 客户端,支持 8 种传输协议。
1.1 8 种传输层
| # | 传输协议 | 用途 | 实现方式 |
|---|---|---|---|
| 1 | Stdio | 本地子进程 | 通过 stdin/stdout 通信 |
| 2 | SSE | Server-Sent Events | HTTP 长连接,服务器推送 |
| 3 | HTTP | Streamable HTTP | HTTP 请求/响应 |
| 4 | WebSocket | 全双工通信 | WS 连接 |
| 5 | SDK 控制传输 | Anthropic SDK 内部 | SDK 直接控制 |
| 6 | claude.ai 代理 | 云端代理 | 通过 claude.ai 中转 |
| 7 | SSE-IDE | IDE 集成 | 针对 IDE 优化的 SSE |
| 8 | WS-IDE | IDE 集成 | 针对 IDE 优化的 WebSocket |
1.2 Strategy Pattern
每种传输协议都是一个独立的 Strategy,实现统一的接口:
// 伪代码:传输策略接口
interface TransportStrategy {
connect(config: TransportConfig): Promise<Connection>
send(message: MCPMessage): Promise<void>
onMessage(handler: (message: MCPMessage) => void): void
close(): Promise<void>
}
// 具体策略
class StdioTransport implements TransportStrategy { ... }
class SSETransport implements TransportStrategy { ... }
class HTTPTransport implements TransportStrategy { ... }
class WebSocketTransport implements TransportStrategy { ... }
1.3 连接管理
MCP 客户端使用 memoizeWithLRU 缓存连接,结合断路器模式管理连接生命周期:
连接请求
↓
检查 LRU 缓存
├── 命中且健康 → 返回缓存连接
└── 未命中或不健康 → 创建新连接
↓
连接成功 → 存入 LRU 缓存
连接失败 → 错误计数 +1
↓
连续错误 >= 阈值 → 断路器触发
→ 清除缓存
→ 下次请求时重新连接
// 伪代码:连接管理
const memoizedConnect = memoizeWithLRU(
async (config: MCPConfig) => {
const transport = createTransport(config.transport)
const connection = await transport.connect(config)
// 设置断路器
let errorCount = 0
connection.on('error', () => {
errorCount++
if (errorCount >= CIRCUIT_BREAKER_THRESHOLD) {
cache.delete(config.key) // 清除缓存
}
})
// 设置自动重连
connection.on('close', () => {
cache.delete(config.key) // 清除缓存,下次自动重连
})
return connection
},
{ maxSize: 10, ttl: 'session' }
)
2. 工具发现和调用
2.1 工具发现流程
MCP 连接建立
↓
发送 tools/list 请求
↓
服务器返回工具列表
↓
每个工具转换为 MCPTool
├── 继承 Tool 接口
├── 设置 mcpInfo (serverName, toolName)
└── 适配 inputSchema (JSON Schema → Zod)
↓
assembleToolPool() 合并到工具池
→ 内置工具在前,MCP 工具在后
2.2 Adapter Pattern (MCPTool)
MCPTool 是一个适配器,将 MCP 协议的工具适配为 Claude Code 的 Tool 接口:
// 伪代码:MCPTool 适配器
class MCPTool implements Tool {
constructor(
private mcpClient: MCPConnection,
private toolDef: MCPToolDefinition
) {}
get name() { return this.toolDef.name }
get inputSchema() { return zodSchema(this.toolDef.inputSchema) }
async call(input: any): Promise<ToolResult> {
// 适配 MCP 调用协议
const result = await this.mcpClient.send({
method: 'tools/call',
params: { name: this.toolDef.name, arguments: input }
})
// 适配返回格式
return {
output: result.content.map(c => c.text).join('\n'),
isError: result.isError
}
}
// 行为元数据:MCP 工具默认保守
isConcurrencySafe() { return false }
isReadOnly() { return false }
}
2.3 Template + Clone Pattern
MCPTool 作为模板,运行时为每个 MCP 工具克隆一个实例:
MCP 服务器返回 5 个工具
↓
每个工具创建一个 MCPTool 克隆
→ 共享 mcpClient 连接
→ 独立的 toolDef 配置
↓
注入到工具池
3. Hook 系统
hooks/ (~5121行) 实现了 Claude Code 的生命周期钩子系统,支持 20+ 种事件。
3.1 Hook 事件列表
| 事件 | 触发时机 | 用途 |
|---|---|---|
PreToolUse | 工具执行前 | 输入验证、权限检查 |
PostToolUse | 工具执行后 | 结果修改、副作用触发 |
SessionStart | 会话开始 | 初始化、环境检查 |
Stop | 会话结束 | 清理、统计 |
PreCompact | 压缩前 | 保存状态 |
PostCompact | 压缩后 | 恢复状态 |
Notification | 通知事件 | 用户提醒 |
SubagentStart | 子Agent启动 | 上下文注入 |
SubagentStop | 子Agent结束 | 结果合并 |
| ... | ... | ... |
3.2 Hook 执行管道
事件触发
↓
收集所有注册的 Hook
↓
按优先级排序
↓
顺序执行每个 Hook
├── 返回 approve → 继续
├── 返回 block → 中断管道
├── 返回 systemMessage → 注入系统消息
└── 抛出异常 → 错误处理
↓
所有 Hook 执行完毕
→ 返回最终决策
3.3 Zod 验证
Hook 输出通过 Zod schema 严格验证:
// Hook 输出 schema
const hookOutputSchema = z.object({
decision: z.enum(['approve', 'block', 'systemMessage']),
reason: z.string().optional(),
modifiedInput: z.any().optional(), // 允许修改工具输入
systemMessage: z.string().optional(),
})
// 验证 Hook 输出
function validateHookOutput(output: unknown): HookOutput {
return hookOutputSchema.parse(output)
}
3.4 Event-Driven Middleware Pattern
Hook 系统本质上是一个事件驱动的中间件管道:
请求 → [Hook 1] → [Hook 2] → [Hook 3] → 执行 → 响应
↓ ↓ ↓
可修改输入 可阻止执行 可注入消息
4. 插件系统
plugins/ (~3302行) 实现了 Claude Code 的插件架构。
4.1 插件结构
plugin-name/
├── plugin.json # 插件元数据
├── commands/ # 自定义命令
│ └── my-command.ts
├── agents/ # 自定义 Agent
│ └── my-agent.ts
└── hooks/ # 自定义 Hook
└── my-hook.ts
4.2 插件发现
插件来源:
├── marketplace (官方插件市场)
├── git 仓库 (直接引用)
└── 本地目录 (开发中)
发现流程:
1. 扫描配置的插件源
2. 验证 plugin.json 格式
3. 检查版本兼容性
4. 加载并注册插件组件
4.3 Plugin Architecture Pattern
| 组件 | 接口 | 注册方式 |
|---|---|---|
| 命令 | PluginCommand | commands.register() |
| Agent | PluginAgent | agents.register() |
| Hook | PluginHook | hooks.register() |
| 工具 | PluginTool | tools.register() |
5. 沙箱系统
Claude Code 还包含一个沙箱系统,将权限规则转换为运行时限制:
权限规则
↓
转换为 @anthropic-ai/sandbox-runtime 配置
├── 文件系统访问控制
│ ├── 允许读取的目录
│ ├── 允许写入的目录
│ └── 禁止访问的目录
└── 网络访问控制
├── 允许的域名
└── 禁止的域名
↓
沙箱运行时强制执行
6. 扩展系统架构图
┌─────────────────────────────────────────────────────────────┐
│ 扩展系统 │
│ │
│ ┌──────────────┐ ┌──────────────┐ ┌──────────────┐ │
│ │ MCP 客户端 │ │ Hook 系统 │ │ 插件系统 │ │
│ │ (3351行) │ │ (5121行) │ │ (3302行) │ │
│ │ │ │ │ │ │ │
│ │ 8种传输协议 │ │ 20+事件 │ │ 命令/Agent/ │ │
│ │ LRU缓存 │ │ Zod验证 │ │ Hook/Tool │ │
│ │ 断路器 │ │ 中间件管道 │ │ marketplace │ │
│ └──────┬───────┘ └──────┬───────┘ └──────┬───────┘ │
│ │ │ │ │
│ └─────────────────┼─────────────────┘ │
│ │ │
│ ┌──────▼───────┐ │
│ │ 沙箱系统 │ │
│ │ 权限→沙箱配置 │ │
│ │ 运行时强制执行 │ │
│ └──────────────┘ │
└─────────────────────────────────────────────────────────────┘
设计模式
本章涉及的模式:
| 模式 | 定义 | 应用位置 |
|---|---|---|
| Strategy Pattern | 统一接口,多种传输实现 | MCP 8种传输协议 |
| Adapter Pattern | 协议/格式转换适配器 | MCPTool 适配器 |
| Event-Driven Middleware | 事件触发 + 中间件管道 | Hook 系统 |
| Plugin Architecture | 标准化接口 + 版本化缓存 | 插件系统 |
| Circuit Breaker | 连续错误触发断路 | MCP 连接管理 |
| Template + Clone | 模板定义骨架,运行时克隆 | MCPTool 创建 |
源码验证
- ✅ MCP 客户端 3351行:client.ts 行数确认
- ✅ 8 种传输协议:全部在源码中找到对应实现
- ✅ Hook 系统 ~5121行:hooks/ 目录总行数确认
- ✅ 插件系统 ~3302行:plugins/ 目录总行数确认
- ✅ Zod 验证:Hook 输出 schema 确认
思考题
-
MCP 支持 8 种传输协议,但大多数用户只用 Stdio。为什么要支持这么多传输方式? 这是否增加了维护成本?
-
Hook 系统的 20+ 事件中,哪些是「必须」的,哪些是「锦上添花」的? 如果要精简到 5 个核心事件,你会选哪些?
-
MCPTool 的默认行为元数据是 isConcurrencySafe=false, isReadOnly=false(保守策略)。如果 MCP 服务器声明了工具的并发安全性,客户端应该信任吗?
-
插件系统的 marketplace 模式需要版本管理和兼容性检查。如果插件 A 依赖插件 B 的特定版本,如何处理依赖冲突?
← 05-上下文管理 | 07-状态管理与Agent协调 →