主题
Day 8 — MCP 设计文档
设计目标:实现 MCP(Model Context Protocol)的客户端和服务端,让 Agent 能调用外部工具服务(作为客户端),也能把自己的工具暴露给外部使用(作为服务端),通过标准化的 JSON-RPC 2.0 协议通信。
一、核心接口设计
1.1 MCPMessage(基于 JSON-RPC 2.0)
| 字段 | 类型 | 说明 |
|---|---|---|
| jsonrpc | str | 固定值 "2.0" |
| id | Optional[int] | 请求/响应 ID(通知没有) |
| method | Optional[str] | 请求的方法名 |
| params | Optional[dict] | 请求参数 |
| result | Optional[Any] | 成功响应结果 |
| error | Optional[MCPError] | 错误响应 |
MCPMethod 枚举:
| 方法 | 值 | 说明 |
|---|---|---|
| INITIALIZE | "initialize" | 握手初始化 |
| TOOLS_LIST | "tools/list" | 列出可用工具 |
| TOOLS_CALL | "tools/call" | 调用工具 |
| NOTIFICATION | "notifications/message" | 通知消息 |
MCPError:
| 字段 | 说明 |
|---|---|
| code | 标准错误码(-32700 解析错误、-32600 无效请求、-32601 方法不存在、-32603 内部错误) |
| message | 错误描述 |
1.2 MCPClient
| 方法 | 签名 | 说明 |
|---|---|---|
| connect | async connect(command, args) | 启动子进程并连接 |
| initialize | async initialize() -> dict | 握手 |
| discover_tools | async discover_tools() -> List[RemoteTool] | 发现远程工具 |
| call_tool | async call_tool(name, args) -> Any | 调用远程工具 |
| close | async close() | 关闭连接 |
1.3 MCPServer
| 方法 | 签名 | 说明 |
|---|---|---|
| start | async start() | 启动服务(监听 stdin) |
| handle_request | async handle_request(msg) -> MCPMessage | 处理请求并返回响应 |
1.4 RemoteTool
继承 Tool 接口,作为远程工具的本地代理:
| 属性/方法 | 说明 |
|---|---|
| name | 来自远程工具定义 |
| description | 来自远程工具定义 |
| parameters | 来自远程工具定义 |
| execute | 内部调用 client.call_tool() |
二、关键流程图
MCPClient 连接与发现流程
MCPServer 请求处理流程
端到端调用流程
三、设计决策与权衡
决策 1:基于 JSON-RPC 2.0
| 方案 | 优点 | 缺点 |
|---|---|---|
| JSON-RPC 2.0(选择) | 标准化、简单、与官方 MCP 规范一致 | 文本协议,性能不如二进制 |
| gRPC / Protobuf | 高性能、强类型 | 复杂,学习成本高 |
| 自定义 JSON 格式 | 完全自由 | 不标准,互操作性差 |
选择理由:MCP 协议规范本身就基于 JSON-RPC 2.0,保持一致性。且 JSON 可读性好,适合教学和调试。
决策 2:Stdio 传输层
| 方案 | 优点 | 缺点 |
|---|---|---|
| Stdio(选择) | 零配置、安全(无网络暴露) | 仅限本机 |
| HTTP/SSE | 支持远程调用 | 需要端口配置、网络安全 |
| WebSocket | 全双工 | 同上 |
选择理由:教学阶段用 Stdio 最简单——MCPClient 直接用 subprocess 启动 MCPServer,通过 stdin/stdout 通信。理解了协议后,换 HTTP 传输层只需替换 IO 读写部分。
决策 3:RemoteTool 透明代理
RemoteTool 实现了和本地 Tool 完全一样的接口,Agent 无需区分工具是本地还是远程的:
| 方案 | 优点 | 缺点 |
|---|---|---|
| 透明代理(选择) | Agent 代码零修改,本地远程一视同仁 | 远程调用延迟不可见 |
| 显式区分 | 可以针对远程做超时/重试 | Agent 代码需要感知工具来源 |
选择理由:"代理模式"是经典设计模式——消费者不关心实现细节。这也是 MCP 协议的核心价值:标准化让集成变得透明。
决策 4:请求 ID 管理
MCPClient 用递增整数作为请求 ID,维护一个 pending_requests: Dict[int, Future] 映射:
python
self._next_id += 1
future = asyncio.get_event_loop().create_future()
self._pending[request_id] = future
# 发送请求...
# 收到响应时根据 id 找到 future 并设置结果这样可以在同一个连接上并发多个请求,通过 ID 匹配响应。
四、与前序章节的集成点
- 依赖 Day 5:MCPServer 暴露的是 ToolRegistry 中的工具;RemoteTool 继承 Tool 接口
- 被 Day 4 依赖:MCPClient 发现的远程工具注册到 ToolRegistry,供 AgentRuntime 使用
五、与真实生产系统的对比
| 维度 | miniOpenClaw | 生产级实现 |
|---|---|---|
| 传输层 | Stdio(本机) | HTTP + SSE / WebSocket(跨网络) |
| 认证 | 无 | API Key / OAuth2 / mTLS |
| 工具发现 | 一次性 list | 动态发现 + 缓存 + 版本检查 |
| 错误处理 | 基础错误码 | 完整 JSON-RPC 错误码 + 重试 |
| 资源管理 | 未实现 | MCP Resources(文件、数据库等) |
| 采样 | 未实现 | MCP Sampling(服务端请求客户端 LLM 能力) |
| 连接池 | 单连接 | 连接池 + 健康检查 + 负载均衡 |