Claude Code 源码分析Claude Code 源码分析
首页
源码统计
系统架构
UML 图表
工具系统
CodeGraph
首页
源码统计
系统架构
UML 图表
工具系统
CodeGraph
  • 概览

    • Claude Code 源码分析
    • 源码统计
    • CodeGraph 图谱
  • 架构

    • 系统架构
    • UML 图表索引
    • 查询引擎
    • 核心流程
    • 消息系统
    • 状态管理
  • 功能模块

    • 工具系统
    • 斜杠命令
    • 服务层
    • MCP 协议
    • Skills 技能
    • 子代理系统
  • 分层深度

    • 入口层
    • UI / Ink 层
    • utils 基础设施
    • 桥接 / 远程
    • 上下文压缩
  • 原理与安全

    • 底层原理
    • 技术难点
    • 权限与安全
    • 内部机制
    • 遥测与分析
  • 深度专题

    • Hooks 系统
    • 插件系统
    • 记忆系统
    • API 通信层
    • Ink 终端 UI
    • 认证系统
    • 构建与发布
    • 术语表
  • 调用分析

    • 调用链分析
    • 核心文件索引
  • 模块详解

    • utils

      • 模块: utils
      • messages · 消息工厂与规范化
      • session-storage · JSONL 会话持久化
      • permissions · 工具权限决策
      • shell-hooks · 用户 Shell Hook 系统
    • components

      • 模块: components
      • REPL · 主屏编排
      • messages · 消息行渲染
      • PermissionRequest · 权限弹窗
      • PromptInput · 底部输入
    • services

      • 模块: services
      • api-claude · Anthropic API 流式与重试
      • mcp-client · MCP 连接与工具调用
      • compact · 上下文压缩与自动触发
      • analytics · GrowthBook、Datadog 与 1P 事件
    • tools

      • 模块: tools
      • tool-interface · Tool 契约与注册表
      • bash-tool · Shell 执行与权限
      • streaming-executor · 流式工具并发调度
      • agent-tool · 子 Agent 委派
    • commands

      • 模块: commands
      • command-registry · commands.ts 注册与分派
      • model-command · /model 模型选择
      • mcp-commands · /mcp 服务器管理
      • compact-memory-commands · /compact 与 /memory
    • ink

      • 模块: ink
      • Ink 渲染管线 · Screen 与终端输出
      • 终端事件 · resize、paste、stdin
      • Ink Hooks · 输入、搜索、终端状态
      • Ink 组件 · Box、Text、ScrollBox 原语
    • hooks

      • 模块: hooks
      • useCanUseTool · 权限 UI 接缝
      • 输入与快捷键 Hook
      • 合并态 Hook(MCP + 本地)
      • notifs 通知 Hook
    • bridge

      • 模块: bridge
      • repl-bridge · REPL 桥初始化与传输
      • bridge-messaging · 桥消息路由与入站处理
      • remote-bridge-core · env-less 核心与守护主循环
      • bridge-permissions-ui · 权限、API 与 TUI
    • cli

      • 模块: cli
      • Structured IO · NDJSON SDK 协议
      • CLI Transports · Session Ingress 传输层
      • CLI Handlers · 子命令懒加载实现
      • Update & Upload · 自更新与串行上传原语
    • screens

      • 模块: screens
      • REPL 屏 · Screen 类型与顶层路由
      • ResumeConversation · 会话恢复选择器
      • Doctor · 安装诊断全屏
    • entrypoints

      • 模块: entrypoints
      • cli-entrypoint · Bootstrap 与快路径
      • sdk-types · core / control / runtime 类型体系
      • mcp-entrypoint · MCP stdio 服务器
      • sandbox-types · 沙箱配置单一真相源
    • skills

      • 模块: skills
      • skills-loading · 磁盘加载与 bundled 注册表
      • bundled-skills · 内置 skill 与 initBundledSkills
      • mcp-skills · MCP prompt 转 skill
      • skill-tool-integration · SkillTool 与命令注册
    • types

      • 模块: types
      • message-types · Message 联合与 content blocks
      • tool-permission-types · Tool、Permission、Command 类型
      • api-sdk-types · API 与 Hooks 协议类型
      • misc-types · ids、plugin、generated 与其余类型
    • tasks

      • 模块: tasks
      • local-agent-task · 本地 Agent 与主会话后台化
      • remote-agent-task · 远程 CCR 与 In-Process Teammate
      • shell-workflow-tasks · Bash 后台、Workflow 与 stopTask
      • dream-monitor-tasks · Dream、Monitor MCP 与 pill 文案
    • keybindings

      • 模块: keybindings
      • keybinding-registry · 注册、Provider 与 useKeybinding
      • default-bindings · 默认键位表与平台差异
      • command-bindings · command:* 动态斜杠命令绑定
      • vim-bindings · Vim 模式与 keybindings 边界
    • memdir

      • 模块: memdir
      • memdir-core · 路径、加载与 MEMORY.md
      • memory-extraction · extractMemories 与 SessionMemory
      • memdir-commands · /memory、/remember 与命令集成
    • state

      • 模块: state
      • app-state-core · store、AppState 类型与 Provider
      • app-state-selectors · selectors 与 onChangeAppState
      • teammate-state · 队友视图与 swarm 状态
      • state-boundaries · bootstrap、sessionStorage、FileStateCache
    • query

      • 模块: query
      • query config 与 deps · 配置快照与依赖注入
      • query tokenBudget · +500k 自动续跑
      • query transitions · Continue / Terminal 状态机
      • query stopHooks · Stop 事件与 turn 结束编排
  • 模块详解(扩展)

    • messages · 消息工厂与规范化
    • session-storage · JSONL 会话持久化
    • permissions · 工具权限决策
    • shell-hooks · 用户 Shell Hook 系统
    • REPL · 主屏编排
    • messages · 消息行渲染
    • PermissionRequest · 权限弹窗
    • PromptInput · 底部输入
    • api-claude · Anthropic API 流式与重试
    • mcp-client · MCP 连接与工具调用
    • compact · 上下文压缩与自动触发
    • analytics · GrowthBook、Datadog 与 1P 事件
    • tool-interface · Tool 契约与注册表
    • bash-tool · Shell 执行与权限
    • streaming-executor · 流式工具并发调度
    • agent-tool · 子 Agent 委派
    • command-registry · commands.ts 注册与分派
    • model-command · /model 模型选择
    • mcp-commands · /mcp 服务器管理
    • compact-memory-commands · /compact 与 /memory
    • Ink 渲染管线 · Screen 与终端输出
    • 终端事件 · resize、paste、stdin
    • Ink Hooks · 输入、搜索、终端状态
    • Ink 组件 · Box、Text、ScrollBox 原语
    • useCanUseTool · 权限 UI 接缝
    • 输入与快捷键 Hook
    • 合并态 Hook(MCP + 本地)
    • notifs 通知 Hook
    • repl-bridge · REPL 桥初始化与传输
    • bridge-messaging · 桥消息路由与入站处理
    • remote-bridge-core · env-less 核心与守护主循环
    • bridge-permissions-ui · 权限、API 与 TUI
    • Structured IO · NDJSON SDK 协议
    • CLI Transports · Session Ingress 传输层
    • CLI Handlers · 子命令懒加载实现
    • Update & Upload · 自更新与串行上传原语
    • REPL 屏 · Screen 类型与顶层路由
    • ResumeConversation · 会话恢复选择器
    • Doctor · 安装诊断全屏
    • cli-entrypoint · Bootstrap 与快路径
    • sdk-types · core / control / runtime 类型体系
    • mcp-entrypoint · MCP stdio 服务器
    • sandbox-types · 沙箱配置单一真相源
    • skills-loading · 磁盘加载与 bundled 注册表
    • bundled-skills · 内置 skill 与 initBundledSkills
    • mcp-skills · MCP prompt 转 skill
    • skill-tool-integration · SkillTool 与命令注册
    • message-types · Message 联合与 content blocks
    • tool-permission-types · Tool、Permission、Command 类型
    • api-sdk-types · API 与 Hooks 协议类型
    • misc-types · ids、plugin、generated 与其余类型
    • local-agent-task · 本地 Agent 与主会话后台化
    • remote-agent-task · 远程 CCR 与 In-Process Teammate
    • shell-workflow-tasks · Bash 后台、Workflow 与 stopTask
    • dream-monitor-tasks · Dream、Monitor MCP 与 pill 文案
    • keybinding-registry · 注册、Provider 与 useKeybinding
    • default-bindings · 默认键位表与平台差异
    • command-bindings · command:* 动态斜杠命令绑定
    • vim-bindings · Vim 模式与 keybindings 边界
    • memdir-core · 路径、加载与 MEMORY.md
    • memory-extraction · extractMemories 与 SessionMemory
    • memdir-commands · /memory、/remember 与命令集成
    • app-state-core · store、AppState 类型与 Provider
    • app-state-selectors · selectors 与 onChangeAppState
    • teammate-state · 队友视图与 swarm 状态
    • state-boundaries · bootstrap、sessionStorage、FileStateCache
    • query config 与 deps · 配置快照与依赖注入
    • query tokenBudget · +500k 自动续跑
    • query transitions · Continue / Terminal 状态机
    • query stopHooks · Stop 事件与 turn 结束编排
  • 工具详解

    • tool-interface · Tool 契约与注册表
    • tool-permission-types · Tool、Permission、Command 类型
    • 工具: Bash
    • 工具: PowerShell
    • 工具: Agent
    • 工具: LSP
    • 工具: FileEdit
    • 工具: FileRead
    • 工具: Skill
    • 工具: WebFetch
    • 工具: MCP
    • 工具: SendMessage
    • 工具: FileWrite
    • 工具: Config
    • 工具: Grep
    • 工具: Brief
    • 工具: ExitPlanMode
    • 工具: ToolSearch
    • 工具: NotebookEdit
    • 工具: TaskOutput
    • 工具: WebSearch
    • 工具: ScheduleCron

本章总览

services/mcp/client.ts(约 3300 行)是 Claude Code 接入 Model Context Protocol 的核心:按配置选择 SSE / stdio / WebSocket / HTTP 传输,OAuth 刷新、session 过期重连、tools/list 转 MCPTool,并在 REPL 侧由 MCPConnectionManager 管理动态配置。本章要求你能从 mcp__github__create_issue 工具名反查到 connectToServer 与 fetchToolsForClient 的缓存键。

学完本章你应该能

  • 列举 MCP 传输类型(sse、stdio、ws、http、sdk)及选型条件
  • 解释 connectToServer memoize 与 ensureConnectedClient 重连语义
  • 说明 McpAuthError / McpSessionExpiredError 的处理边界
  • 理解 fetchToolsForClient 如何把 MCP schema 映射为应用 Tool
  • 能在 UI 层定位 MCPConnectionManager 与 useManageMCPConnections

核心概念(先读懂这些)

MCP 工具名是三层命名

对外暴露给模型的名字通常是 mcp__{serverName}__{toolName}(buildMcpToolName)。权限系统用 mcpInfo: { serverName, toolName } 做细粒度规则。SDK 内嵌 MCP 在 CLAUDE_AGENT_SDK_MCP_NO_PREFIX 时可省略前缀以覆盖内置工具名。读 config.json 的 mcpServers 时,scope(project/user)不影响连接键,areMcpConfigsEqual 会 strip scope 再 JSON 比较。

SSE EventSource 不能套请求 timeout

connectToServer 对 SSE 显式区分:POST/auth 走 wrapFetchWithTimeout,而 eventSourceInit.fetch 不带 60s timeout,否则长连接会被误杀。这是 MCP 连接「刚连上就断」的常见根因之一。

Session 404 与 JSON-RPC -32001

isMcpSessionExpiredError 要求 HTTP 404 且 body 含 Session not found(code -32001),避免把错误 URL 当成 session 过期。过期后 clear cache + ensureConnectedClient 重试是标准恢复路径。

建议学习步骤

  1. 阅读源码块 A:错误类型 McpAuthError / session expired
  2. 阅读源码块 B:connectToServer 传输分支
  3. 阅读源码块 C:ensureConnectedClient 与 config 比较
  4. 阅读源码块 D:fetchToolsForClient 映射
  5. 阅读源码块 E:callMCPToolWithUrlElicitationRetry
  6. 阅读源码块 F:MCPConnectionManager React 上下文
  7. 在源码树打开 services/mcp/client.ts 对照行号

常见误区

注意

不要把 services/mcp/config.ts 的策略过滤与 client.ts 连接逻辑混读

注意

memoize 的 connectToServer 配置变更需 clearServerCache 才会重建

注意

IDE ws/sse-ide transport 通常无 OAuth,与远程 sse 路径不同

在架构中的位置

MCP 在 Claude Code 中的生命周期:

启动 /settings 变更 → getAllMcpConfigs + policy filter
  → connectToServer (client.ts) 并行 batch
  → fetchToolsForClient / fetchResourcesForClient / fetchCommandsForClient
  → 写入 AppState.mcp.clients
  → query 循环 tools 数组含 MCPTool
  → callMCPTool → transformMCPResult → tool_result 回 transcript

services/mcp/ 还包含 config.ts、auth.ts、elicitationHandler.ts、MCPConnectionManager.tsx 等。本章以 client.ts 为主,连接编排看 useManageMCPConnections.ts。

错误类型与可恢复边界

client.ts 定义若干 可区分 的错误类,供 tool 层与 UI 决策:

  1. McpAuthError:OAuth token 过期或 401,serverName 字段供状态机切到 needs-auth
  2. McpSessionExpiredError(内部):session ID 失效,调用方应 clear cache 后 ensureConnectedClient 重试
  3. McpToolCallError:MCP 返回 isError: true 但需保留 _meta 给 SDK 消费者(符合 MCP spec)

isMcpSessionExpiredError 双信号检测避免误判普通 404。读 tool 执行栈时,auth 错误应更新 client status 而非无限 retry tool call。

源码引用: src/services/mcp/client.ts · 第 146–210 行(共 3349 行)

 146| /**
 147|  * Custom error class to indicate that an MCP tool call failed due to
 148|  * authentication issues (e.g., expired OAuth token returning 401).
 149|  * This error should be caught at the tool execution layer to update
 150|  * the client's status to 'needs-auth'.
 151|  */
 152| export class McpAuthError extends Error {
 153|   serverName: string
 154|   constructor(serverName: string, message: string) {
 155|     super(message)
 156|     this.name = 'McpAuthError'
 157|     this.serverName = serverName
 158|   }
 159| }
 160| 
 161| /**
 162|  * Thrown when an MCP session has expired and the connection cache has been cleared.
 163|  * The caller should get a fresh client via ensureConnectedClient and retry.
 164|  */
 165| class McpSessionExpiredError extends Error {
 166|   constructor(serverName: string) {
 167|     super(`MCP server "${serverName}" session expired`)
 168|     this.name = 'McpSessionExpiredError'
 169|   }
 170| }
 171| 
 172| /**
 173|  * Thrown when an MCP tool returns `isError: true`. Carries the result's `_meta`
 174|  * so SDK consumers can still receive it — per the MCP spec, `_meta` is on the
 175|  * base Result type and is valid on error results.
 176|  */
 177| export class McpToolCallError_I_VERIFIED_THIS_IS_NOT_CODE_OR_FILEPATHS extends TelemetrySafeError_I_VERIFIED_THIS_IS_NOT_CODE_OR_FILEPATHS {
 178|   constructor(
 179|     message: string,
 180|     telemetryMessage: string,
 181|     readonly mcpMeta?: { _meta?: Record<string, unknown> },
 182|   ) {
 183|     super(message, telemetryMessage)
 184|     this.name = 'McpToolCallError'
 185|   }
 186| }
 187| 
 188| /**
 189|  * Detects whether an error is an MCP "Session not found" error (HTTP 404 + JSON-RPC code -32001).
 190|  * Per the MCP spec, servers return 404 when a session ID is no longer valid.
 191|  * We check both signals to avoid false positives from generic 404s (wrong URL, server gone, etc.).
 192|  */
 193| export function isMcpSessionExpiredError(error: Error): boolean {
 194|   const httpStatus =
 195|     'code' in error ? (error as Error & { code?: number }).code : undefined
 196|   if (httpStatus !== 404) {
 197|     return false
 198|   }
 199|   // The SDK embeds the response body text in the error message.
 200|   // MCP servers return: {"error":{"code":-32001,"message":"Session not found"},...}
 201|   // Check for the JSON-RPC error code to distinguish from generic web server 404s.
 202|   return (
 203|     error.message.includes('"code":-32001') ||
 204|     error.message.includes('"code": -32001')
 205|   )
 206| }
 207| 
 208| /**
 209|  * Default timeout for MCP tool calls (effectively infinite - ~27.8 hours).
 210|  */

connectToServer:传输与认证

connectToServer 用 lodash memoize 缓存 Promise,键来自 getServerCacheKey(name, serverRef)(name + JSON config)。

传输分支概要:

  • sse:ClaudeAuthProvider + SSEClientTransport;session ingress JWT 存在时可能走 proxy 而非直连
  • sse-ide / ws-ide:IDE 集成,常无 OAuth;ws 支持 Bun 与 Node 两套 WebSocket 客户端
  • stdio:StdioClientTransport + subprocessEnv 注入环境
  • http / streamableHttp:StreamableHTTPClientTransport
  • sdk / in-process:SdkControlClientTransport 或 InProcessTransport

wrapFetchWithStepUpDetection 在 OAuth step-up 403 时触发 re-auth。getMcpServerHeaders 合并静态与动态 header;日志中对 Authorization 做 [REDACTED]。

连接成功后 register MCP notification handler(tool/list changed 等),并 logEvent 连接耗时与 transport 类型。

源码引用: src/services/mcp/client.ts · 第 581–677 行(共 3349 行)

 581| export function getServerCacheKey(
 582|   name: string,
 583|   serverRef: ScopedMcpServerConfig,
 584| ): string {
 585|   return `${name}-${jsonStringify(serverRef)}`
 586| }
 587| 
 588| /**
 589|  * TODO (ollie): The memoization here increases complexity by a lot, and im not sure it really improves performance
 590|  * Attempts to connect to a single MCP server
 591|  * @param name Server name
 592|  * @param serverRef Scoped server configuration
 593|  * @returns A wrapped client (either connected or failed)
 594|  */
 595| export const connectToServer = memoize(
 596|   async (
 597|     name: string,
 598|     serverRef: ScopedMcpServerConfig,
 599|     serverStats?: {
 600|       totalServers: number
 601|       stdioCount: number
 602|       sseCount: number
 603|       httpCount: number
 604|       sseIdeCount: number
 605|       wsIdeCount: number
 606|     },
 607|   ): Promise<MCPServerConnection> => {
 608|     const connectStartTime = Date.now()
 609|     let inProcessServer:
 610|       | { connect(t: Transport): Promise<void>; close(): Promise<void> }
 611|       | undefined
 612|     try {
 613|       let transport
 614| 
 615|       // If we have the session ingress JWT, we will connect via the session ingress rather than
 616|       // to remote MCP's directly.
 617|       const sessionIngressToken = getSessionIngressAuthToken()
 618| 
 619|       if (serverRef.type === 'sse') {
 620|         // Create an auth provider for this server
 621|         const authProvider = new ClaudeAuthProvider(name, serverRef)
 622| 
 623|         // Get combined headers (static + dynamic)
 624|         const combinedHeaders = await getMcpServerHeaders(name, serverRef)
 625| 
 626|         // Use the auth provider with SSEClientTransport
 627|         const transportOptions: SSEClientTransportOptions = {
 628|           authProvider,
 629|           // Use fresh timeout per request to avoid stale AbortSignal bug.
 630|           // Step-up detection wraps innermost so the 403 is seen before the
 631|           // SDK's handler calls auth() → tokens().
 632|           fetch: wrapFetchWithTimeout(
 633|             wrapFetchWithStepUpDetection(createFetchWithInit(), authProvider),
 634|           ),
 635|           requestInit: {
 636|             headers: {
 637|               'User-Agent': getMCPUserAgent(),
 638|               ...combinedHeaders,
 639|             },
 640|           },
 641|         }
 642| 
 643|         // IMPORTANT: Always set eventSourceInit with a fetch that does NOT use the
 644|         // timeout wrapper. The EventSource connection is long-lived (stays open indefinitely
 645|         // to receive server-sent events), so applying a 60-second timeout would kill it.
 646|         // The timeout is only meant for individual API requests (POST, auth refresh), not
 647|         // the persistent SSE stream.
 648|         transportOptions.eventSourceInit = {
 649|           fetch: async (url: string | URL, init?: RequestInit) => {
 650|             // Get auth headers from the auth provider
 651|             const authHeaders: Record<string, string> = {}
 652|             const tokens = await authProvider.tokens()
 653|             if (tokens) {
 654|               authHeaders.Authorization = `Bearer ${tokens.access_token}`
 655|             }
 656| 
 657|             const proxyOptions = getProxyFetchOptions()
 658|             // eslint-disable-next-line eslint-plugin-n/no-unsupported-features/node-builtins
 659|             return fetch(url, {
 660|               ...init,
 661|               ...proxyOptions,
 662|               headers: {
 663|                 'User-Agent': getMCPUserAgent(),
 664|                 ...authHeaders,
 665|                 ...init?.headers,
 666|                 ...combinedHeaders,
 667|                 Accept: 'text/event-stream',
 668|               },
 669|             })
 670|           },
 671|         }
 672| 
 673|         transport = new SSEClientTransport(
 674|           new URL(serverRef.url),
 675|           transportOptions,
 676|         )
 677|         logMCPDebug(name, `SSE transport initialized, awaiting connection`)

源码引用: src/services/mcp/client.ts · 第 595–620 行(共 3349 行)

 595| export const connectToServer = memoize(
 596|   async (
 597|     name: string,
 598|     serverRef: ScopedMcpServerConfig,
 599|     serverStats?: {
 600|       totalServers: number
 601|       stdioCount: number
 602|       sseCount: number
 603|       httpCount: number
 604|       sseIdeCount: number
 605|       wsIdeCount: number
 606|     },
 607|   ): Promise<MCPServerConnection> => {
 608|     const connectStartTime = Date.now()
 609|     let inProcessServer:
 610|       | { connect(t: Transport): Promise<void>; close(): Promise<void> }
 611|       | undefined
 612|     try {
 613|       let transport
 614| 
 615|       // If we have the session ingress JWT, we will connect via the session ingress rather than
 616|       // to remote MCP's directly.
 617|       const sessionIngressToken = getSessionIngressAuthToken()
 618| 
 619|       if (serverRef.type === 'sse') {
 620|         // Create an auth provider for this server

ensureConnectedClient 与配置等价性

工具调用前应通过 ensureConnectedClient 保证 client.type === 'connected':

  • config.type === 'sdk' 的 in-process 服务器直接返回,不走 connectToServer
  • 否则 await connectToServer;失败抛 TelemetrySafeError

areMcpConfigsEqual 用于检测 settings 变更:先比 type,再 strip scope 字段后 JSON stringify。配置变化时 useManageMCPConnections 会 disconnect + reconnect,并 clear fetchToolsForClient 的 LRU cache(键为 server name,上限 MCP_FETCH_CACHE_SIZE = 20)。

调试「工具列表陈旧」时,确认是否收到 ToolListChangedNotification 以及 reconnect 是否 clear cache。

源码引用: src/services/mcp/client.ts · 第 1688–1722 行(共 3349 行)

1688| export async function ensureConnectedClient(
1689|   client: ConnectedMCPServer,
1690| ): Promise<ConnectedMCPServer> {
1691|   // SDK MCP servers run in-process and are handled separately via setupSdkMcpClients
1692|   if (client.config.type === 'sdk') {
1693|     return client
1694|   }
1695| 
1696|   const connectedClient = await connectToServer(client.name, client.config)
1697|   if (connectedClient.type !== 'connected') {
1698|     throw new TelemetrySafeError_I_VERIFIED_THIS_IS_NOT_CODE_OR_FILEPATHS(
1699|       `MCP server "${client.name}" is not connected`,
1700|       'MCP server not connected',
1701|     )
1702|   }
1703|   return connectedClient
1704| }
1705| 
1706| /**
1707|  * Compares two MCP server configurations to determine if they are equivalent.
1708|  * Used to detect when a server needs to be reconnected due to config changes.
1709|  */
1710| export function areMcpConfigsEqual(
1711|   a: ScopedMcpServerConfig,
1712|   b: ScopedMcpServerConfig,
1713| ): boolean {
1714|   // Quick type check first
1715|   if (a.type !== b.type) return false
1716| 
1717|   // Compare by serializing - this handles all config variations
1718|   // We exclude 'scope' from comparison since it's metadata, not connection config
1719|   const { scope: _scopeA, ...configA } = a
1720|   const { scope: _scopeB, ...configB } = b
1721|   return jsonStringify(configA) === jsonStringify(configB)
1722| }

fetchToolsForClient:MCP → Tool 适配

fetchToolsForClient 对 connected client 发 tools/list,结果经 recursivelySanitizeUnicode 清洗后映射为应用 Tool 对象:

  • 继承 MCPTool 原型,设置 isMcp: true
  • name:默认 mcp__server__tool,skip-prefix 模式用原始 tool.name
  • searchHint / alwaysLoad 来自 _meta['anthropic/searchHint'] 等扩展字段
  • isReadOnly / isConcurrencySafe 读 MCP annotations.readOnlyHint
  • description/prompt 超长截断(MAX_MCP_DESCRIPTION_LENGTH)

mcpToolInputToAutoClassifierInput 把 tool input 编码成 auto-mode 分类器字符串(key=value),eval 脚本与生产共用。

工具 discovery 与 ToolSearchTool 的 deferred loading 协作:collapse 分类见 classifyMcpToolForCollapse。

源码引用: src/services/mcp/client.ts · 第 1728–1800 行(共 3349 行)

1728| /**
1729|  * Encode MCP tool input for the auto-mode security classifier.
1730|  * Exported so the auto-mode eval scripts can mirror production encoding
1731|  * for `mcp__*` tool stubs without duplicating this logic.
1732|  */
1733| export function mcpToolInputToAutoClassifierInput(
1734|   input: Record<string, unknown>,
1735|   toolName: string,
1736| ): string {
1737|   const keys = Object.keys(input)
1738|   return keys.length > 0
1739|     ? keys.map(k => `${k}=${String(input[k])}`).join(' ')
1740|     : toolName
1741| }
1742| 
1743| export const fetchToolsForClient = memoizeWithLRU(
1744|   async (client: MCPServerConnection): Promise<Tool[]> => {
1745|     if (client.type !== 'connected') return []
1746| 
1747|     try {
1748|       if (!client.capabilities?.tools) {
1749|         return []
1750|       }
1751| 
1752|       const result = (await client.client.request(
1753|         { method: 'tools/list' },
1754|         ListToolsResultSchema,
1755|       )) as ListToolsResult
1756| 
1757|       // Sanitize tool data from MCP server
1758|       const toolsToProcess = recursivelySanitizeUnicode(result.tools)
1759| 
1760|       // Check if we should skip the mcp__ prefix for SDK MCP servers
1761|       const skipPrefix =
1762|         client.config.type === 'sdk' &&
1763|         isEnvTruthy(process.env.CLAUDE_AGENT_SDK_MCP_NO_PREFIX)
1764| 
1765|       // Convert MCP tools to our Tool format
1766|       return toolsToProcess
1767|         .map((tool): Tool => {
1768|           const fullyQualifiedName = buildMcpToolName(client.name, tool.name)
1769|           return {
1770|             ...MCPTool,
1771|             // In skip-prefix mode, use the original name for model invocation so MCP tools
1772|             // can override builtins by name. mcpInfo is used for permission checking.
1773|             name: skipPrefix ? tool.name : fullyQualifiedName,
1774|             mcpInfo: { serverName: client.name, toolName: tool.name },
1775|             isMcp: true,
1776|             // Collapse whitespace: _meta is open to external MCP servers, and
1777|             // a newline here would inject orphan lines into the deferred-tool
1778|             // list (formatDeferredToolLine joins on '\n').
1779|             searchHint:
1780|               typeof tool._meta?.['anthropic/searchHint'] === 'string'
1781|                 ? tool._meta['anthropic/searchHint']
1782|                     .replace(/\s+/g, ' ')
1783|                     .trim() || undefined
1784|                 : undefined,
1785|             alwaysLoad: tool._meta?.['anthropic/alwaysLoad'] === true,
1786|             async description() {
1787|               return tool.description ?? ''
1788|             },
1789|             async prompt() {
1790|               const desc = tool.description ?? ''
1791|               return desc.length > MAX_MCP_DESCRIPTION_LENGTH
1792|                 ? desc.slice(0, MAX_MCP_DESCRIPTION_LENGTH) + '… [truncated]'
1793|                 : desc
1794|             },
1795|             isConcurrencySafe() {
1796|               return tool.annotations?.readOnlyHint ?? false
1797|             },
1798|             isReadOnly() {
1799|               return tool.annotations?.readOnlyHint ?? false
1800|             },

callMCPTool 与 URL Elicitation

部分 MCP 服务器在需要用户浏览器授权时返回 UrlElicitationRequired(JSON-RPC -32042)。callMCPToolWithUrlElicitationRetry 循环:

  1. 调用 callMCPTool
  2. 捕获 McpError 且 code 匹配 UrlElicitationRequired
  3. 通过 elicitation handler / REPL queue 展示 URL,等待用户完成
  4. 最多 MAX_URL_ELICITATION_RETRIES = 3 次

transformMCPResult / processMCPResult 负责大输出截断(truncateMcpContentIfNeeded)、二进制 blob 落盘(persistBinaryContent)、以及 tool_result 持久化(persistToolResult)。

读 Chrome MCP 或 enterprise SSO MCP 时,此路径与 mcpServerApproval.tsx UI 叠加。

源码引用: src/services/mcp/client.ts · 第 2812–2875 行(共 3349 行)

2812| /** @internal Exported for testing. */
2813| export async function callMCPToolWithUrlElicitationRetry({
2814|   client: connectedClient,
2815|   clientConnection,
2816|   tool,
2817|   args,
2818|   meta,
2819|   signal,
2820|   setAppState,
2821|   onProgress,
2822|   callToolFn = callMCPTool,
2823|   handleElicitation,
2824| }: {
2825|   client: ConnectedMCPServer
2826|   clientConnection: MCPServerConnection
2827|   tool: string
2828|   args: Record<string, unknown>
2829|   meta?: Record<string, unknown>
2830|   signal: AbortSignal
2831|   setAppState: (f: (prev: AppState) => AppState) => void
2832|   onProgress?: (data: MCPProgress) => void
2833|   /** Injectable for testing. Defaults to callMCPTool. */
2834|   callToolFn?: (opts: {
2835|     client: ConnectedMCPServer
2836|     tool: string
2837|     args: Record<string, unknown>
2838|     meta?: Record<string, unknown>
2839|     signal: AbortSignal
2840|     onProgress?: (data: MCPProgress) => void
2841|   }) => Promise<MCPToolCallResult>
2842|   /** Handler for URL elicitations when no hook handles them.
2843|    * In print/SDK mode, delegates to structuredIO. In REPL, falls back to queue. */
2844|   handleElicitation?: (
2845|     serverName: string,
2846|     params: ElicitRequestURLParams,
2847|     signal: AbortSignal,
2848|   ) => Promise<ElicitResult>
2849| }): Promise<MCPToolCallResult> {
2850|   const MAX_URL_ELICITATION_RETRIES = 3
2851|   for (let attempt = 0; ; attempt++) {
2852|     try {
2853|       return await callToolFn({
2854|         client: connectedClient,
2855|         tool,
2856|         args,
2857|         meta,
2858|         signal,
2859|         onProgress,
2860|       })
2861|     } catch (error) {
2862|       // The MCP SDK's Protocol creates plain McpError (not UrlElicitationRequiredError)
2863|       // for error responses, so we check the error code instead of instanceof.
2864|       if (
2865|         !(error instanceof McpError) ||
2866|         error.code !== ErrorCode.UrlElicitationRequired
2867|       ) {
2868|         throw error
2869|       }
2870| 
2871|       // Limit the number of URL elicitation retries
2872|       if (attempt >= MAX_URL_ELICITATION_RETRIES) {
2873|         throw error
2874|       }
2875| 

MCPConnectionManager 与 useManageMCPConnections

React 层 MCPConnectionManager 提供 Context:

  • useMcpReconnect() → reconnectMcpServer
  • useMcpToggleEnabled() → 启用/禁用单个 server(写 config + reconnect)

内部 useManageMCPConnections 监听 dynamicMcpConfig 与 isStrictMcpConfig:

  • 初次 mount 批量 getMcpToolsCommandsAndResources
  • 注册 elicitation、channel permission、list_changed notification
  • 失败指数退避:MAX_RECONNECT_ATTEMPTS = 5,INITIAL_BACKOFF_MS = 1000,上限 30s
  • plugin 错误 dedup 写入 AppState.plugins.errors

TODO 注释提到未来可能把 reconnect/toggle 挪到 AppState,减少 Context 层级。

源码引用: src/services/mcp/MCPConnectionManager.tsx · 第 7–72 行(共 75 行)

   7| import type { Command } from '../../commands.js'
   8| import type { Tool } from '../../Tool.js'
   9| import type {
  10|   MCPServerConnection,
  11|   ScopedMcpServerConfig,
  12|   ServerResource,
  13| } from './types.js'
  14| import { useManageMCPConnections } from './useManageMCPConnections.js'
  15| 
  16| interface MCPConnectionContextValue {
  17|   reconnectMcpServer: (serverName: string) => Promise<{
  18|     client: MCPServerConnection
  19|     tools: Tool[]
  20|     commands: Command[]
  21|     resources?: ServerResource[]
  22|   }>
  23|   toggleMcpServer: (serverName: string) => Promise<void>
  24| }
  25| 
  26| const MCPConnectionContext = createContext<MCPConnectionContextValue | null>(
  27|   null,
  28| )
  29| 
  30| export function useMcpReconnect() {
  31|   const context = useContext(MCPConnectionContext)
  32|   if (!context) {
  33|     throw new Error('useMcpReconnect must be used within MCPConnectionManager')
  34|   }
  35|   return context.reconnectMcpServer
  36| }
  37| 
  38| export function useMcpToggleEnabled() {
  39|   const context = useContext(MCPConnectionContext)
  40|   if (!context) {
  41|     throw new Error(
  42|       'useMcpToggleEnabled must be used within MCPConnectionManager',
  43|     )
  44|   }
  45|   return context.toggleMcpServer
  46| }
  47| 
  48| interface MCPConnectionManagerProps {
  49|   children: ReactNode
  50|   dynamicMcpConfig: Record<string, ScopedMcpServerConfig> | undefined
  51|   isStrictMcpConfig: boolean
  52| }
  53| 
  54| // TODO (ollie): We may be able to get rid of this context by putting these function on app state
  55| export function MCPConnectionManager({
  56|   children,
  57|   dynamicMcpConfig,
  58|   isStrictMcpConfig,
  59| }: MCPConnectionManagerProps): React.ReactNode {
  60|   const { reconnectMcpServer, toggleMcpServer } = useManageMCPConnections(
  61|     dynamicMcpConfig,
  62|     isStrictMcpConfig,
  63|   )
  64|   const value = useMemo(
  65|     () => ({ reconnectMcpServer, toggleMcpServer }),
  66|     [reconnectMcpServer, toggleMcpServer],
  67|   )
  68| 
  69|   return (
  70|     <MCPConnectionContext.Provider value={value}>
  71|       {children}
  72|     </MCPConnectionContext.Provider>

源码引用: src/services/mcp/useManageMCPConnections.ts · 第 87–120 行(共 1142 行)

  87| // Constants for reconnection with exponential backoff
  88| const MAX_RECONNECT_ATTEMPTS = 5
  89| const INITIAL_BACKOFF_MS = 1000
  90| const MAX_BACKOFF_MS = 30000
  91| 
  92| /**
  93|  * Create a unique key for a plugin error to enable deduplication
  94|  */
  95| function getErrorKey(error: PluginError): string {
  96|   const plugin = 'plugin' in error ? error.plugin : 'no-plugin'
  97|   return `${error.type}:${error.source}:${plugin}`
  98| }
  99| 
 100| /**
 101|  * Add errors to AppState, deduplicating to avoid showing the same error multiple times
 102|  */
 103| function addErrorsToAppState(
 104|   setAppState: (updater: (prev: AppState) => AppState) => void,
 105|   newErrors: PluginError[],
 106| ): void {
 107|   if (newErrors.length === 0) return
 108| 
 109|   setAppState(prevState => {
 110|     // Build set of existing error keys
 111|     const existingKeys = new Set(
 112|       prevState.plugins.errors.map(e => getErrorKey(e)),
 113|     )
 114| 
 115|     // Only add errors that don't already exist
 116|     const uniqueNewErrors = newErrors.filter(
 117|       error => !existingKeys.has(getErrorKey(error)),
 118|     )
 119| 
 120|     if (uniqueNewErrors.length === 0) {

源码目录与关联文件

强关联:services/mcp/config.ts、tools/MCPTool/、utils/mcpValidation.ts、services/mcp/auth.ts(ClaudeAuthProvider)。点击 client.ts 跳回本章源码块。

动手练习

  1. 在 mcp.json 添加 stdio server,观察 connect 日志中的 transport 计数
  2. 故意让 OAuth token 过期,确认 client status 变为 needs-auth 且出现 McpAuthTool
  3. 对比 mcpToolInputToAutoClassifierInput 输出与 auto_mode 分类器 deny 文案
  4. 触发 tool list changed notification,验证 fetchToolsForClient cache 是否 invalidate

本章小结与延伸

client.ts = MCP 运行时。下一章 compact,理解 MCP instructions delta 在压缩后如何 re-inject。 继续学习:

  • api-claude
  • compact
Prev
api-claude · Anthropic API 流式与重试
Next
compact · 上下文压缩与自动触发