## Keyboard shortcuts

Press `←` or `→` to navigate between chapters

Press `S` or `/` to search in the book

Press `?` to show this help

Press `Esc` to hide this help

# 第 15 章：MCP 协议实现

Model Context Protocol（MCP）是 Claude Code 可扩展性的核心基础设施。通过 MCP，外部服务可以将自己的工具、资源和提示注入到 Claude 的能力空间中，使其能够操作数据库、调用 API、访问设计工具等几乎任何外部系统。本章将深入 `src/services/mcp/client.ts` 这个超过 3300 行的协议引擎，解析其连接管理、工具转换和资源系统的完整实现。

## 15.1 MCP 协议概述

MCP 建立在 JSON-RPC 2.0 之上，通过多种传输层（Transport）承载消息。其核心思想是将 AI 工具调用抽象为标准化的请求-响应协议，使得工具提供者和 AI 客户端可以独立演进。

协议的基本交互模式：

    sequenceDiagram
        participant CC as Claude Code (Client)
        participant MCP as MCP Server

        CC->>MCP: initialize (capabilities, protocolVersion)
        MCP-->>CC: initialize response (serverCapabilities)
        CC->>MCP: initialized (通知)

        Note over CC,MCP: 连接已建立

        CC->>MCP: tools/list
        MCP-->>CC: tools (name, description, inputSchema)

        CC->>MCP: resources/list
        MCP-->>CC: resources (uri, name, mimeType)

        CC->>MCP: tools/call (name, arguments)
        MCP-->>CC: result (content[])

        MCP->>CC: elicitation/request (URL/Form)
        CC-->>MCP: elicitation/response

Claude Code 作为 MCP 客户端，使用 `@modelcontextprotocol/sdk` 官方 SDK 实现协议层。但在 SDK 之上，Claude Code 构建了大量的工程逻辑：连接池管理、认证处理、工具类型转换、输出截断、错误恢复等。

## 15.2 七种传输层

Claude Code 支持多种 MCP 传输层，覆盖了从本地进程到远程服务的完整场景。

### 15.2.1 stdio 传输

最基础的传输方式——启动一个子进程，通过标准输入/输出传递 JSON-RPC 消息：

    // src/services/mcp/client.ts (connectToServer 函数内)
    if (!serverRef.type || serverRef.type === 'stdio') {
      transport = new StdioClientTransport({
        command: serverRef.command,
        args: serverRef.args,
        env: { ...subprocessEnv(), ...expandedEnv },
        cwd: getOriginalCwd(),
      })
    }

stdio 是本地 MCP 服务器的首选传输。它的优势是简单、安全（进程隔离）、不需要网络端口。

### 15.2.2 SSE 传输（Server-Sent Events）

用于远程 HTTP MCP 服务器的旧式传输：

    if (serverRef.type === 'sse') {
      const authProvider = new ClaudeAuthProvider(name, serverRef)
      const combinedHeaders = await getMcpServerHeaders(name, serverRef)

      const transportOptions: SSEClientTransportOptions = {
        authProvider,
        fetch: wrapFetchWithTimeout(
          wrapFetchWithStepUpDetection(createFetchWithInit(), authProvider),
        ),
        requestInit: {
          headers: {
            'User-Agent': getMCPUserAgent(),
            ...combinedHeaders,
          },
        },
      }

      // EventSource 连接不能使用超时包装——它是长连接
      transportOptions.eventSourceInit = {
        fetch: async (url, init) => {
          const authHeaders: Record<string, string> = {}
          const tokens = await authProvider.tokens()
          if (tokens) {
            authHeaders.Authorization = `Bearer ${tokens.access_token}`
          }
          return fetch(url, { ...init, ...proxyOptions, headers: {
            'User-Agent': getMCPUserAgent(),
            ...authHeaders, ...init?.headers, ...combinedHeaders,
            Accept: 'text/event-stream',
          }})
        },
      }

      transport = new SSEClientTransport(new URL(serverRef.url), transportOptions)
    }

SSE 传输的关键设计：POST 请求使用超时包装（60 秒），但 EventSource 连接（GET）不使用——因为 SSE 流是持久连接，加超时会错误地断开它。

### 15.2.3 HTTP Streamable 传输

这是 MCP 2025-03-26 规范定义的新一代传输：

    // MCP Streamable HTTP 规范要求客户端声明接受 JSON 和 SSE
    const MCP_STREAMABLE_HTTP_ACCEPT = 'application/json, text/event-stream'

HTTP Streamable 传输统一了请求-响应和流式通知，是 SSE 传输的演进版本。

### 15.2.4 WebSocket 传输

用于需要全双工通信的场景，特别是 IDE 集成：

    if (serverRef.type === 'ws-ide') {
      const tlsOptions = getWebSocketTLSOptions()
      const wsHeaders = {
        'User-Agent': getMCPUserAgent(),
        ...(serverRef.authToken && {
          'X-Claude-Code-Ide-Authorization': serverRef.authToken,
        }),
      }
      // Bun 和 Node.js ws 模块的差异处理
      let wsClient: WsClientLike
      if (typeof Bun !== 'undefined') {
        // Bun WebSocket
      } else {
        wsClient = await createNodeWsClient(url, { headers: wsHeaders, ...tlsOptions })
      }
      transport = new WebSocketTransport(wsClient)
    }

### 15.2.5 SDK 传输

用于同进程的 MCP 服务器（如 Agent SDK 嵌入场景）：

    if (serverRef.type === 'sdk') {
      transport = new SdkControlClientTransport(serverRef)
    }

### 15.2.6 IDE 变体（sse-ide, ws-ide）

IDE 传输是 SSE 和 WebSocket 的简化版本，去掉了 OAuth 认证层，因为 IDE 服务器运行在本地，通过锁文件或 token 进行身份验证。

### 15.2.7 claudeai-proxy 传输

这是 claude.ai 网页版的代理传输，通过 Anthropic 的基础设施中转 MCP 请求：

    export function createClaudeAiProxyFetch(innerFetch: FetchLike): FetchLike {
      return async (url, init) => {
        const doRequest = async () => {
          await checkAndRefreshOAuthTokenIfNeeded()
          const currentTokens = getClaudeAIOAuthTokens()
          if (!currentTokens) throw new Error('No claude.ai OAuth token available')
          const headers = new Headers(init?.headers)
          headers.set('Authorization', `Bearer ${currentTokens.accessToken}`)
          const response = await innerFetch(url, { ...init, headers })
          return { response, sentToken: currentTokens.accessToken }
        }

        const { response, sentToken } = await doRequest()
        if (response.status !== 401) return response

        // 401 重试逻辑
        const tokenChanged = await handleOAuth401Error(sentToken).catch(() => false)
        if (!tokenChanged) {
          const now = getClaudeAIOAuthTokens()?.accessToken
          if (!now || now === sentToken) return response
        }
        try {
          return (await doRequest()).response
        } catch {
          return response // 重试失败，返回原始 401
        }
      }
    }

401 重试逻辑的设计值得研究：它精确地捕获了发送时使用的 token，避免在并发 401 场景下出现 ABA 问题——另一个连接器可能已经在你检查之前刷新了 token。

## 15.3 客户端实现

### 15.3.1 连接管理

连接建立使用了 memoize 模式，确保每个服务器只维护一个连接：

    export const connectToServer = memoize(
      async (name: string, serverRef: ScopedMcpServerConfig, serverStats?):
        Promise<MCPServerConnection> => {
        const connectStartTime = Date.now()
        try {
          let transport
          // ... 根据 serverRef.type 创建传输层

          // 创建 MCP SDK Client
          const client = new Client(
            { name: 'claude-code', version: '...' },
            { capabilities: { ... } },
          )

          await client.connect(transport)

          return {
            name, type: 'connected', client,
            capabilities: client.getServerCapabilities(),
            config: serverRef,
          }
        } catch (error) {
          // 认证失败 -> needs-auth
          // 其他失败 -> error
        }
      },
      (name, serverRef) => getServerCacheKey(name, serverRef),
    )

memoize 的 key 是服务器名称和完整配置的 JSON 序列化——这确保了同名但不同配置的服务器不会共用连接。

### 15.3.2 连接批次控制

    export function getMcpServerConnectionBatchSize(): number {
      return parseInt(process.env.MCP_SERVER_CONNECTION_BATCH_SIZE || '', 10) || 3
    }

    function getRemoteMcpServerConnectionBatchSize(): number {
      return parseInt(process.env.MCP_REMOTE_SERVER_CONNECTION_BATCH_SIZE || '', 10) || 20
    }

本地服务器（stdio）的并发连接限制为 3——因为每个连接都会启动子进程，过多的并发进程会拖慢系统。远程服务器的限制为 20——网络请求的开销更小。

### 15.3.3 会话过期与重连

    export function isMcpSessionExpiredError(error: Error): boolean {
      const httpStatus =
        'code' in error ? (error as Error & { code?: number }).code : undefined
      if (httpStatus !== 404) return false
      return (
        error.message.includes('"code":-32001') ||
        error.message.includes('"code": -32001')
      )
    }

MCP 规范要求服务器在会话过期时返回 HTTP 404 + JSON-RPC 错误码 -32001。客户端同时检查两个信号来避免误判——普通的 404（错误 URL、服务器宕机）不会触发重连逻辑。

## 15.4 工具转换

MCP 服务器返回的工具定义需要转换为 Claude Code 内部的 `Tool` 类型。这个转换过程发生在 `fetchToolsForClient` 中。

### 15.4.1 工具名称构建

    // src/services/mcp/mcpStringUtils.ts
    export function buildMcpToolName(serverName: string, toolName: string): string {
      return `${getMcpPrefix(serverName)}${normalizeNameForMCP(toolName)}`
    }

    export function getMcpPrefix(serverName: string): string {
      return `mcp__${normalizeNameForMCP(serverName)}__`
    }

MCP 工具名称遵循 `mcp__<server>__<tool>` 的三段式格式。这个设计确保了：

1. 不同服务器的同名工具不会冲突
2. 权限规则可以在服务器级别批量控制
3. 用户可以通过命名约定理解工具来源

### 15.4.2 工具属性映射

    export const fetchToolsForClient = memoizeWithLRU(
      async (client: MCPServerConnection): Promise<Tool[]> => {
        // ...
        return toolsToProcess.map((tool): Tool => {
          const fullyQualifiedName = buildMcpToolName(client.name, tool.name)
          return {
            ...MCPTool,
            name: skipPrefix ? tool.name : fullyQualifiedName,
            mcpInfo: { serverName: client.name, toolName: tool.name },
            isMcp: true,

            // MCP annotations → 内部属性
            searchHint: typeof tool._meta?.['anthropic/searchHint'] === 'string'
              ? tool._meta['anthropic/searchHint'].replace(/\s+/g, ' ').trim() || undefined
              : undefined,
            alwaysLoad: tool._meta?.['anthropic/alwaysLoad'] === true,

            isConcurrencySafe() { return tool.annotations?.readOnlyHint ?? false },
            isReadOnly() { return tool.annotations?.readOnlyHint ?? false },
            isDestructive() { return tool.annotations?.destructiveHint ?? false },
            isOpenWorld() { return tool.annotations?.openWorldHint ?? false },

            inputJSONSchema: tool.inputSchema as Tool['inputJSONSchema'],

            async checkPermissions() {
              return {
                behavior: 'passthrough' as const,
                message: 'MCPTool requires permission.',
                suggestions: [{
                  type: 'addRules' as const,
                  rules: [{ toolName: fullyQualifiedName, ruleContent: undefined }],
                  behavior: 'allow' as const,
                  destination: 'localSettings' as const,
                }],
              }
            },
            // ...
          }
        })
      },
      (client) => client.name,
      MCP_FETCH_CACHE_SIZE,
    )

转换的关键映射关系：

    flowchart LR
        subgraph "MCP Tool 定义"
            A[tool.name]
            B[tool.description]
            C[tool.inputSchema]
            D[tool.annotations.readOnlyHint]
            E[tool.annotations.destructiveHint]
            F[tool.annotations.openWorldHint]
            G["tool._meta['anthropic/searchHint']"]
            H["tool._meta['anthropic/alwaysLoad']"]
        end
        subgraph "内部 Tool 类型"
            I[name: mcp__server__tool]
            J[description/prompt]
            K[inputJSONSchema]
            L[isReadOnly / isConcurrencySafe]
            M[isDestructive]
            N[isOpenWorld]
            O[searchHint]
            P[alwaysLoad]
        end
        A --> I
        B --> J
        C --> K
        D --> L
        E --> M
        F --> N
        G --> O
        H --> P

MCP 工具的 `checkPermissions` 默认返回 `passthrough`——意味着 MCP 工具没有自定义的权限逻辑，完全依赖全局的规则系统。同时它提供了一个 `suggestion`，建议用户将该工具添加到本地设置的 allow 列表中。

### 15.4.3 工具描述截断

    const MAX_MCP_DESCRIPTION_LENGTH = 2048

    async prompt() {
      const desc = tool.description ?? ''
      return desc.length > MAX_MCP_DESCRIPTION_LENGTH
        ? desc.slice(0, MAX_MCP_DESCRIPTION_LENGTH) + '... [truncated]'
        : desc
    }

OpenAPI 生成的 MCP 服务器经常把完整的端点文档（15-60KB）塞进工具描述。2048 字符的截断限制在保留核心意图的同时，防止上下文窗口被淹没。

### 15.4.4 工具调用与会话恢复

    async call(args, context, _canUseTool, parentMessage, onProgress?) {
      const MAX_SESSION_RETRIES = 1
      for (let attempt = 0; ; attempt++) {
        try {
          const connectedClient = await ensureConnectedClient(client)
          const mcpResult = await callMCPToolWithUrlElicitationRetry({
            client: connectedClient, clientConnection: client,
            tool: tool.name, args, meta,
            signal: context.abortController.signal,
            setAppState: context.setAppState,
            onProgress: ...,
            handleElicitation: context.handleElicitation,
          })
          // 处理结果...
        } catch (error) {
          if (attempt < MAX_SESSION_RETRIES && isMcpSessionExpiredError(error)) {
            // 清除缓存，重新连接
            connectToServer.cache.delete(key)
            fetchToolsForClient.cache.delete(name)
            continue
          }
          throw error
        }
      }
    }

会话过期时自动重连一次。重连前清除所有相关缓存（连接、工具列表、资源列表），确保下次使用全新的连接状态。

## 15.5 资源系统

MCP 资源让服务器向 Claude 暴露结构化数据（文件、数据库记录、API 响应等）。

    export const fetchResourcesForClient = memoizeWithLRU(
      async (client: MCPServerConnection): Promise<ServerResource[]> => {
        if (client.type !== 'connected') return []

        try {
          if (!client.capabilities?.resources) return []

          const result = await client.client.request(
            { method: 'resources/list' },
            ListResourcesResultSchema,
          )

          if (!result.resources) return []
          return result.resources.map(resource => ({
            ...resource,
            server: client.name,  // 附加服务器名称
          }))
        } catch (error) {
          logMCPError(client.name, `Failed to fetch resources: ${errorMessage(error)}`)
          return []
        }
      },
      (client) => client.name,
      MCP_FETCH_CACHE_SIZE,
    )

资源获取使用 LRU 缓存来避免重复请求。每个资源被标注了来源服务器名称，方便后续的权限检查和 UI 展示。

MCP Prompt 同样被转换为 Claude Code 的 Command 系统：

    export const fetchCommandsForClient = memoizeWithLRU(
      async (client: MCPServerConnection): Promise<Command[]> => {
        // ...
        return promptsToProcess.map(prompt => ({
          type: 'prompt' as const,
          name: 'mcp__' + normalizeNameForMCP(client.name) + '__' + prompt.name,
          description: prompt.description ?? '',
          isMcp: true,
          source: 'mcp',
          async getPromptForCommand(args) {
            const connectedClient = await ensureConnectedClient(client)
            const result = await connectedClient.client.getPrompt({
              name: prompt.name,
              arguments: zipObject(argNames, argsArray),
            })
            return result.messages.map(message =>
              transformResultContent(message.content, connectedClient.name),
            ).flat()
          },
        }))
      },
    )

## 15.6 进度流与 Elicitation

### 15.6.1 进度报告

MCP 工具在执行过程中可以报告进度：

    if (onProgress && toolUseId) {
      onProgress({
        toolUseID: toolUseId,
        data: {
          type: 'mcp_progress',
          status: 'started',
          serverName: client.name,
          toolName: tool.name,
        },
      })
    }

    // ... 执行完成后
    onProgress({
      toolUseID: toolUseId,
      data: {
        type: 'mcp_progress',
        status: 'completed',
        serverName: client.name,
        toolName: tool.name,
        elapsedTimeMs: Date.now() - startTime,
      },
    })

### 15.6.2 Elicitation 机制

Elicitation 是 MCP 的一种交互模式，允许服务器在工具执行过程中请求用户输入。有两种模式：

    // src/services/mcp/elicitationHandler.ts
    function getElicitationMode(params: ElicitRequestParams): 'form' | 'url' {
      return params.mode === 'url' ? 'url' : 'form'
    }

URL 模式：服务器提供一个 URL（通常是 OAuth 回调页面），Claude Code 打开浏览器让用户完成交互，然后等待完成通知：

    export type ElicitationRequestEvent = {
      serverName: string
      requestId: string | number
      params: ElicitRequestParams
      signal: AbortSignal
      respond: (response: ElicitResult) => void
      waitingState?: ElicitationWaitingState
      onWaitingDismiss?: (action: 'dismiss' | 'retry' | 'cancel') => void
      completed?: boolean
    }

Form 模式：服务器定义一个表单 Schema，Claude Code 在终端中渲染交互式表单，收集用户输入后返回。

Elicitation 还支持 Hook 拦截，允许自动化环境在不需要人工交互的情况下处理 elicitation 请求：

    export function registerElicitationHandler(
      client: Client,
      serverName: string,
      setAppState: (f: (prevState: AppState) => AppState) => void,
    ): void {
      try {
        client.setRequestHandler(ElicitRequestSchema, async (request, extra) => {
          // 首先运行 Hook
          // 如果 Hook 提供了响应，直接返回
          // 否则显示 UI 让用户交互
        })
      } catch { /* 服务器未声明 elicitation 能力 */ }
    }

完成通知使用专门的 `ElicitationCompleteNotificationSchema` 处理，通过 `elicitationId` 将完成事件关联到等待中的 elicitation 请求。

    sequenceDiagram
        participant Claude as Claude Code
        participant Server as MCP Server
        participant Browser as 浏览器
        participant Hook as Elicitation Hook

        Claude->>Server: tools/call
        Server->>Claude: elicitation/request (URL mode)

        alt Hook 处理
            Claude->>Hook: 执行 Hook
            Hook-->>Claude: 返回响应
        else 用户交互
            Claude->>Browser: 打开 URL
            Browser->>Server: 用户完成交互
            Server->>Claude: elicitation/complete 通知
        end

        Claude-->>Server: elicitation/response
        Server-->>Claude: tools/call result

这种设计实现了 MCP 协议的完整交互能力——工具不仅可以被动地接收输入、返回结果，还可以在执行过程中主动请求额外信息，打开浏览器进行身份验证，或展示表单收集配置参数。