mcp-protocol.mdx 7.6 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191
  1. ---
  2. title: "MCP 协议 - 连接管理、工具发现与执行链路"
  3. description: "从源码角度解析 Claude Code 的 MCP 集成:7 种传输层实现、connectToServer 的 memoize 缓存、工具发现的 LRU 策略、认证状态机、以及 MCP 工具如何进入权限检查链路。"
  4. keywords: ["MCP", "Model Context Protocol", "工具扩展", "MCP 客户端", "工具发现"]
  5. ---
  6. {/* 本章目标:从源码角度揭示 MCP 客户端的连接管理、工具发现协议和执行链路 */}
  7. ## 架构总览:从配置到可用工具
  8. ```
  9. settings.json: { mcpServers: { "my-db": { command: "npx", args: [...] } } }
  10. getAllMcpConfigs() ← 合并 user/project/local 三级配置
  11. useManageMCPConnections() ← React Hook 管理连接生命周期
  12. connectToServer(name, config) ← memoize 缓存(lodash memoize)
  13. ├── 创建 Transport(stdio/sse/http/...)
  14. ├── new Client() ← @modelcontextprotocol/sdk
  15. ├── client.connect(transport) ← 超时控制(MCP_TIMEOUT, 默认 30s)
  16. └── 返回 MCPServerConnection ← { connected | failed | needs-auth | pending }
  17. fetchToolsForClient(client) ← LRU(20) 缓存
  18. ├── client.request({ method: 'tools/list' })
  19. └── 每个工具包装为 MCPTool ← 统一 Tool 接口
  20. assembleToolPool() ← 合并内置工具 + MCP 工具
  21. 工具名格式: mcp__<serverName>__<toolName> ← buildMcpToolName()
  22. ```
  23. ## 7 种传输层实现
  24. `connectToServer()`(`client.ts:596-1643`)根据 `config.type` 分发到不同的 Transport 实现:
  25. | 传输类型 | Transport 类 | 适用场景 | 认证方式 |
  26. |----------|-------------|---------|---------|
  27. | `stdio`(默认) | `StdioClientTransport` | 本地子进程 | 无 |
  28. | `sse` | `SSEClientTransport` | 远程 SSE 服务 | `ClaudeAuthProvider` + OAuth |
  29. | `http` | `StreamableHTTPClientTransport` | HTTP 流 | `ClaudeAuthProvider` + OAuth |
  30. | `sse-ide` | `SSEClientTransport` | IDE 集成 | lockfile token |
  31. | `ws-ide` | `WebSocketTransport` | IDE WebSocket | `X-Claude-Code-Ide-Authorization` |
  32. | `ws` | `WebSocketTransport` | WebSocket 服务 | session ingress token |
  33. | `claudeai-proxy` | `StreamableHTTPClientTransport` | claude.ai 代理 | OAuth bearer + 401 重试 |
  34. ### stdio 传输的进程管理
  35. stdio 类型的 MCP 服务器作为子进程运行,cleanup 时采用 **信号升级策略**(`client.ts:1431-1564`):
  36. ```
  37. SIGINT (100ms) → SIGTERM (400ms) → SIGKILL
  38. ```
  39. 总清理时间上限 600ms,防止 MCP 服务器关闭阻塞 CLI 退出。
  40. ### 远程传输的认证状态机
  41. SSE/HTTP 类型使用 `ClaudeAuthProvider` 实现 OAuth 认证流程。认证失败时进入 `needs-auth` 状态,并写入 15 分钟 TTL 的缓存文件(`mcp-needs-auth-cache.json`),避免重复弹出认证提示。
  42. ```
  43. 连接尝试 → 401 Unauthorized
  44. handleRemoteAuthFailure()
  45. ├── logEvent('tengu_mcp_server_needs_auth')
  46. ├── setMcpAuthCacheEntry(name) ← 写入 15min TTL 缓存
  47. └── return { type: 'needs-auth' } ← UI 显示认证提示
  48. ```
  49. ## 连接缓存与重连机制
  50. `connectToServer` 使用 lodash `memoize` 缓存连接对象,缓存 key 为 `${name}-${JSON.stringify(config)}`。
  51. ### 缓存失效触发
  52. 当连接关闭时(`client.onclose`),清除所有相关缓存(`client.ts:1376-1404`):
  53. ```typescript
  54. client.onclose = () => {
  55. const key = getServerCacheKey(name, serverRef)
  56. fetchToolsForClient.cache.delete(name) // 工具缓存
  57. fetchResourcesForClient.cache.delete(name) // 资源缓存
  58. fetchCommandsForClient.cache.delete(name) // 命令缓存
  59. connectToServer.cache.delete(key) // 连接缓存
  60. }
  61. ```
  62. ### 连接降级检测
  63. 远程传输有 **连续错误计数器**(`client.ts:1229`):
  64. ```typescript
  65. let consecutiveConnectionErrors = 0
  66. const MAX_ERRORS_BEFORE_RECONNECT = 3
  67. ```
  68. 遇到终端错误(ECONNRESET、ETIMEDOUT、EPIPE 等)连续 3 次后,主动关闭 transport 触发重连。对于 HTTP 传输,还检测 session 过期(404 + JSON-RPC code -32001)。
  69. ### 请求级超时保护
  70. 每个 HTTP 请求使用独立的 `setTimeout` 超时(`wrapFetchWithTimeout`,`client.ts:493`),而非共享 `AbortSignal.timeout()`。原因是 Bun 对 AbortSignal.timeout 的 GC 是惰性的——每个请求约 2.4KB 原生内存,即使请求毫秒级完成也要等 60s 才回收。
  71. ```typescript
  72. const controller = new AbortController()
  73. const timer = setTimeout(c => c.abort(...), MCP_REQUEST_TIMEOUT_MS, controller)
  74. timer.unref?.() // 不阻止进程退出
  75. ```
  76. ## 工具发现:从 MCP 到 Tool 接口
  77. `fetchToolsForClient()`(`client.ts:1745-2000`)使用 `memoizeWithLRU` 缓存(上限 20),将 MCP 工具转换为 Claude Code 的统一 Tool 接口:
  78. ```typescript
  79. const fullyQualifiedName = buildMcpToolName(client.name, tool.name)
  80. // 结果: "mcp__my-db__query"
  81. ```
  82. ### 工具描述截断
  83. MCP 工具描述上限 2048 字符(`MAX_MCP_DESCRIPTION_LENGTH`)。OpenAPI 生成的 MCP 服务器曾观察到 15-60KB 的描述文档。
  84. ### 工具能力标注
  85. 每个 MCP 工具根据 `tool.annotations` 自动标注:
  86. | 注解 | 映射到 | 含义 |
  87. |------|--------|------|
  88. | `readOnlyHint` | `isReadOnly()` + `isConcurrencySafe()` | 只读,可并行 |
  89. | `destructiveHint` | `isDestructive()` | 破坏性操作 |
  90. | `openWorldHint` | `isOpenWorld()` | 开放世界(不可枚举) |
  91. | `title` | `userFacingName()` | 显示名称 |
  92. ### MCP 工具的权限检查
  93. MCP 工具默认返回 `{ behavior: 'passthrough' }`(`client.ts:1816-1834`),意味着它们始终进入权限确认流程。工具名使用 `mcp__` 前缀精确匹配权限规则。
  94. ## MCP 工具的执行链路
  95. ```
  96. AI 生成 tool_use: { name: "mcp__my-db__query", input: { sql: "..." } }
  97. MCPTool.call() ← client.ts:1835
  98. ├── ensureConnectedClient() ← 确保连接有效(重连)
  99. ├── callMCPToolWithUrlElicitationRetry() ← 带 Elicitation 重试
  100. │ ├── client.request({ method: 'tools/call' })
  101. │ ├── 处理图片结果(resize + persist)
  102. │ └── 内容截断(mcpContentNeedsTruncation)
  103. ├── McpSessionExpiredError → 重试一次
  104. └── 返回 { data: content, mcpMeta }
  105. ```
  106. ### Session 过期自动重试
  107. HTTP 传输的 MCP session 可能过期。检测到 `McpSessionExpiredError` 后自动重试一次(`client.ts:1862`),因为 `ensureConnectedClient()` 已经清除了缓存并建立了新连接。
  108. ### 内容截断与持久化
  109. 大型 MCP 工具输出通过 `truncateMcpContentIfNeeded` 截断,二进制内容(图片)通过 `persistBinaryContent` 写入文件并返回文件路径。图片自动 resize(`maybeResizeAndDownsampleImageBuffer`)。
  110. ## MCP 连接的并发控制
  111. ```typescript
  112. // 本地服务器并发连接数
  113. getMcpServerConnectionBatchSize() // 默认 3
  114. // 远程服务器并发连接数
  115. getRemoteMcpServerConnectionBatchSize() // 默认 20
  116. ```
  117. 本地 MCP 服务器(stdio)是重量级的子进程,默认限制 3 个并发连接。远程服务器是轻量级 HTTP 请求,允许 20 个并发。
  118. ## 实际配置示例
  119. ```json
  120. // settings.json 中的 MCP 配置
  121. {
  122. "mcpServers": {
  123. "my-database": {
  124. "command": "npx",
  125. "args": ["@my-org/db-mcp-server"],
  126. "env": { "DB_URL": "postgres://..." }
  127. },
  128. "remote-api": {
  129. "type": "http",
  130. "url": "https://api.example.com/mcp"
  131. }
  132. }
  133. }
  134. ```
  135. 配置后,AI 的工具列表中会出现 `mcp__my-database__query` 和 `mcp__remote-api__*` 工具——与内置工具使用相同的权限检查链路和 UI 渲染。