第 23 课:MCP 基础 — 传输层与客户端

4 阅读5分钟

模块八:MCP 协议 | 前置依赖:第 10 课 | 预计学习时间:70 分钟


学习目标

完成本课后,你将能够:

  1. 解释 MCP(Model Context Protocol)的设计目标及其在 Claude Code 中的角色
  2. 列出 Claude Code 支持的所有传输层类型及其适用场景
  3. 说明 services/mcp/client.ts 中连接管理、工具注册、资源注册的完整流程
  4. 理解 .mcp.json 配置文件的层级合并机制与 config.ts 的实现

23.1 什么是 MCP

MCP(Model Context Protocol)是 Anthropic 制定的开放协议标准,目的是让 AI 模型以统一方式访问外部工具外部数据源。你可以把它理解为 AI 世界的 "USB 接口":

┌────────────────┐       MCP 协议        ┌────────────────┐
│                │  ─────────────────────▶ │                │
│  Claude Code   │  JSON-RPC over         │  MCP Server    │
│  (MCP Client)  │  Stdio / SSE / HTTP    │  (工具提供者)   │
│                │  ◀───────────────────── │                │
└────────────────┘                        └────────────────┘

核心价值:

  • 统一接口:无论 MCP Server 提供的是 Slack 消息、GitHub PR、数据库查询还是浏览器操作,Claude Code 用同一套协议与之通信
  • 动态发现:客户端连接后自动发现服务器提供的工具(tools)、资源(resources)、提示(prompts)和命令(commands)
  • 安全隔离:MCP Server 在独立进程中运行,Claude Code 通过传输层与之通信

services/mcp/ 目录概览

services/mcp/ 包含 25 个文件,是 Claude Code 中最大的服务模块之一:

文件大小职责
client.ts119KB连接管理、工具注册、资源注册、工具调用
auth.ts89KBOAuth 认证、Token 刷新、凭证存储
config.ts51KB配置加载、合并、校验、.mcp.json 管理
useManageMCPConnections.ts45KBReact Hook,连接生命周期管理
types.ts7KB类型定义与 Zod Schema
InProcessTransport.ts1.8KB进程内传输层
SdkControlTransport.ts4.5KBSDK 控制传输桥接
channelPermissions.ts9KBChannel 权限中继
elicitationHandler.ts10KB交互式认证流
officialRegistry.ts2KB官方注册表查询
normalization.ts879B名称标准化
MCPConnectionManager.tsx8KBReact Context Provider

23.2 类型系统与连接状态

传输层类型

types.ts 定义了所有支持的传输层:

export const TransportSchema = lazySchema(() =>
  z.enum(['stdio', 'sse', 'sse-ide', 'http', 'ws', 'sdk']),
)

每种传输对应一个配置 Schema:

// Stdio — 本地子进程通信
McpStdioServerConfigSchema = z.object({
  type: z.literal('stdio').optional(), // 可省略,向后兼容
  command: z.string().min(1),
  args: z.array(z.string()).default([]),
  env: z.record(z.string(), z.string()).optional(),
})

// SSE — Server-Sent Events(远程)
McpSSEServerConfigSchema = z.object({
  type: z.literal('sse'),
  url: z.string(),
  headers: z.record(z.string(), z.string()).optional(),
  oauth: McpOAuthConfigSchema().optional(),
})

// HTTP — Streamable HTTP(MCP 2025-03-26 规范推荐)
McpHTTPServerConfigSchema = z.object({
  type: z.literal('http'),
  url: z.string(),
  headers: z.record(z.string(), z.string()).optional(),
  oauth: McpOAuthConfigSchema().optional(),
})

// WebSocket — 双向实时通信
McpWebSocketServerConfigSchema = z.object({
  type: z.literal('ws'),
  url: z.string(),
  headers: z.record(z.string(), z.string()).optional(),
})

// SDK — SDK 进程内服务器
McpSdkServerConfigSchema = z.object({
  type: z.literal('sdk'),
  name: z.string(),
})

// Claude.ai Proxy — Claude.ai 代理连接
McpClaudeAIProxyServerConfigSchema = z.object({
  type: z.literal('claudeai-proxy'),
  url: z.string(),
  id: z.string(),
})

连接状态机

每个 MCP Server 的连接状态是一个判别联合类型:

type MCPServerConnection =
  | ConnectedMCPServer    // 连接成功
  | FailedMCPServer       // 连接失败
  | NeedsAuthMCPServer    // 需要认证
  | PendingMCPServer      // 连接中
  | DisabledMCPServer     // 已禁用

状态转换图:

                  ┌──────────┐
    ┌────────────▶│ pending  │◀─────────────┐
    │             └────┬─────┘              │
    │                  │                    │
    │         ┌────────┼────────┐           │
    │         ▼        ▼        ▼           │
    │   ┌──────────┐ ┌──────┐ ┌──────────┐ │
    │   │connected │ │failed│ │needs-auth│ │ reconnect
    │   └──────────┘ └──────┘ └──────────┘ │
    │         │                     │       │
    │         │ auth失败/session过期 │ 认证完成│
    │         ▼                     │       │
    │   ┌──────────┐                └───────┘
    │   │needs-auth│
    │   └──────────┘
    │
    │          ┌──────────┐
    └──────────│ disabled │  (用户手动禁用)
               └──────────┘

ConnectedMCPServer 包含完整的连接信息:

type ConnectedMCPServer = {
  client: Client              // MCP SDK 客户端实例
  name: string                // 服务器名称
  type: 'connected'
  capabilities: ServerCapabilities  // 服务器能力声明
  serverInfo?: {
    name: string
    version: string
  }
  instructions?: string       // 服务器提供的使用说明
  config: ScopedMcpServerConfig
  cleanup: () => Promise<void>  // 清理函数
}

23.3 传输层实现

Stdio 传输

最常见的传输方式 — 启动一个子进程,通过 stdin/stdout 通信:

// client.ts 中的 Stdio 连接逻辑
const finalCommand = process.env.CLAUDE_CODE_SHELL_PREFIX || serverRef.command
const finalArgs = process.env.CLAUDE_CODE_SHELL_PREFIX
  ? [[serverRef.command, ...serverRef.args].join(' ')]
  : serverRef.args

transport = new StdioClientTransport({
  command: finalCommand,
  args: finalArgs,
  env: {
    ...subprocessEnv(),
    ...serverRef.env,         // 用户配置的环境变量
  },
  stderr: 'pipe',             // 防止 MCP Server 的错误输出污染 UI
})

stderr 处理:MCP 规范仅定义 stdout 上的 JSON-RPC 通信。stderr 被独立捕获并限制为 64MB,用于调试失败的连接。

SSE 传输

SSE(Server-Sent Events)用于远程 MCP Server。这是一个长连接,服务器可以主动推送消息:

const transportOptions: SSEClientTransportOptions = {
  authProvider,
  fetch: wrapFetchWithTimeout(
    wrapFetchWithStepUpDetection(createFetchWithInit(), authProvider),
  ),
  requestInit: {
    headers: {
      'User-Agent': getMCPUserAgent(),
      ...combinedHeaders,
    },
  },
  // EventSource 连接是长期存活的,不应用 60 秒超时
  eventSourceInit: {
    fetch: async (url, init) => {
      const authHeaders = {}
      const tokens = await authProvider.tokens()
      if (tokens) authHeaders.Authorization = `Bearer ${tokens.access_token}`
      return fetch(url, {
        ...init,
        headers: { ...authHeaders, ...init?.headers, Accept: 'text/event-stream' },
      })
    },
  },
}
transport = new SSEClientTransport(new URL(serverRef.url), transportOptions)

关键设计:SSE 有两个独立的 fetch 函数 — 一个用于普通 POST 请求(带 60 秒超时),另一个用于 EventSource 长连接(无超时)。

HTTP 传输(Streamable HTTP)

MCP 2025-03-26 规范推荐的新传输方式:

// 要求客户端在 POST 请求中声明 Accept 头
const MCP_STREAMABLE_HTTP_ACCEPT = 'application/json, text/event-stream'

transport = new StreamableHTTPClientTransport(
  new URL(serverRef.url),
  {
    authProvider,
    fetch: wrapFetchWithTimeout(
      wrapFetchWithStepUpDetection(createFetchWithInit(), authProvider),
    ),
    requestInit: { headers: { 'User-Agent': getMCPUserAgent(), ...headers } },
  },
)

InProcessTransport — 进程内传输

对于某些内置 MCP Server(如 Claude-in-Chrome、Computer Use),启动一个单独的子进程太重(~325 MB)。InProcessTransport 允许 Server 和 Client 在同一进程内通信:

class InProcessTransport implements Transport {
  private peer: InProcessTransport | undefined
  private closed = false

  onclose?: () => void
  onerror?: (error: Error) => void
  onmessage?: (message: JSONRPCMessage) => void

  _setPeer(peer: InProcessTransport): void {
    this.peer = peer
  }

  async send(message: JSONRPCMessage): Promise<void> {
    if (this.closed) throw new Error('Transport is closed')
    // 使用 queueMicrotask 异步投递,避免同步调用导致栈溢出
    queueMicrotask(() => {
      this.peer?.onmessage?.(message)
    })
  }

  async close(): Promise<void> {
    if (this.closed) return
    this.closed = true
    this.onclose?.()
    if (this.peer && !this.peer.closed) {
      this.peer.closed = true
      this.peer.onclose?.()
    }
  }
}

// 创建成对的传输通道
function createLinkedTransportPair(): [Transport, Transport] {
  const a = new InProcessTransport()
  const b = new InProcessTransport()
  a._setPeer(b)
  b._setPeer(a)
  return [a, b]  // [clientTransport, serverTransport]
}

使用场景

// Chrome MCP Server 的进程内连接
const { createLinkedTransportPair } = await import('./InProcessTransport.js')
const [clientTransport, serverTransport] = createLinkedTransportPair()
await inProcessServer.connect(serverTransport)
transport = clientTransport

SdkControlTransport — SDK 桥接传输

当 MCP Server 运行在 SDK 进程中时,需要通过 stdout/stdin 的控制消息桥接通信:

CLI 进程                              SDK 进程
┌──────────┐    控制消息 (stdout)     ┌──────────┐
│MCP Client│ ───────────────────────▶ │MCP Server│
│          │ ◀─────────────────────── │          │
└──────────┘    控制响应 (stdin)      └──────────┘
     ▲                                     ▲
     │                                     │
SdkControlClientTransport      SdkControlServerTransport

23.4 连接管理(client.ts)

client.ts 是 119KB 的核心模块,包含连接建立、工具注册、资源管理的完整逻辑。

连接建立 — connectToServer

使用 lodash memoize 缓存连接,避免重复连接同一服务器:

export const connectToServer = memoize(
  async (name, serverRef, serverStats?) => {
    // 1. 根据 serverRef.type 选择传输层
    // 2. 创建 Client 实例
    const client = new Client(
      {
        name: 'claude-code',
        title: 'Claude Code',
        version: MACRO.VERSION ?? 'unknown',
      },
      {
        capabilities: {
          roots: {},
          elicitation: {},  // 声明支持交互式认证
        },
      },
    )
    // 3. 注册 elicitation handler(交互式认证)
    // 4. 注册 roots handler(工作目录通知)
    // 5. 连接(带超时,默认 30 秒)
    await client.connect(transport)
    // 6. 返回 ConnectedMCPServer 或错误状态
  },
  getServerCacheKey,  // 缓存键 = 名称 + JSON(配置)
)

连接批量控制

// 本地服务器(Stdio)默认并发 3 个
function getMcpServerConnectionBatchSize(): number {
  return parseInt(process.env.MCP_SERVER_CONNECTION_BATCH_SIZE || '', 10) || 3
}

// 远程服务器默认并发 20 个(网络 I/O 为主,可以更高)
function getRemoteMcpServerConnectionBatchSize(): number {
  return parseInt(process.env.MCP_REMOTE_SERVER_CONNECTION_BATCH_SIZE || '', 10) || 20
}

工具注册 — fetchToolsForClient

连接成功后,自动获取服务器提供的工具并包装为 Claude Code 的 Tool 接口:

ConnectedMCPServer
      │
      ▼
client.listTools()
      │
      ▼
┌─────────────────────────────────┐
│ 对每个 MCP Tool:                │
│ 1. 规范化名称(normalizeNameForMCP)│
│ 2. 截断描述(max 2048 chars)    │
│ 3. 包装为 MCPTool 实例           │
│ 4. 注册为 Claude Code Tool      │
└─────────────────────────────────┘
      │
      ▼
MCPTool[] → 加入 AppState.mcp.tools

MCP 工具的命名规范:mcp__<serverName>__<toolName>,使用双下划线分隔。

资源注册

同样自动发现并注册资源(如文件、数据库表):

// 通过两个内置工具暴露给模型
ListMcpResourcesTool  // 列出所有 MCP 资源
ReadMcpResourceTool   // 读取特定 MCP 资源的内容

Session 过期处理

MCP 协议定义了 Session 概念,服务器可能返回 404 + JSON-RPC code -32001 表示 Session 过期:

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

Session 过期时清除缓存的连接,下次调用自动重连。


23.5 配置系统(config.ts)

.mcp.json 文件

项目根目录的 .mcp.json 是最常见的 MCP 配置方式:

{
  "mcpServers": {
    "my-local-tool": {
      "command": "npx",
      "args": ["-y", "@my-org/mcp-server"],
      "env": {
        "API_KEY": "${API_KEY}"
      }
    },
    "remote-api": {
      "type": "http",
      "url": "https://api.example.com/mcp",
      "headers": {
        "Authorization": "Bearer ${TOKEN}"
      }
    }
  }
}

配置层级

配置来自多个 scope,按优先级合并:

优先级(高 → 低):
┌──────────────┐
│   dynamic    │  ← SDK 运行时注入
├──────────────┤
│   local      │  ← .mcp.json(项目根目录)
├──────────────┤
│   project    │  ← settings.json 中的 projectSettings
├──────────────┤
│   user       │  ← ~/.claude/settings.json
├──────────────┤
│   managed    │  ← 企业管理策略
├──────────────┤
│   enterprise │  ← managed-mcp.json
├──────────────┤
│   claudeai   │  ← Claude.ai Web 端配置的连接器
└──────────────┘

每个配置项附带 scope 标记:

type ScopedMcpServerConfig = McpServerConfig & {
  scope: ConfigScope  // 'local' | 'user' | 'project' | 'dynamic' | ...
  pluginSource?: string  // 插件来源标记
}

环境变量扩展

配置中的 ${VAR_NAME} 会在运行时被替换:

// envExpansion.ts
function expandEnvVarsInString(value: string): string

原子写入

.mcp.json 的写入使用原子操作防止数据损坏:

async function writeMcpjsonFile(config: McpJsonConfig): Promise<void> {
  // 1. 读取原文件权限
  const stats = await stat(mcpJsonPath)
  // 2. 写入临时文件
  const handle = await open(tempPath, 'w', existingMode ?? 0o644)
  await handle.writeFile(jsonStringify(config, null, 2))
  await handle.datasync()  // 刷到磁盘
  // 3. 原子重命名
  await rename(tempPath, mcpJsonPath)
}

去重机制

当插件和手动配置指向同一 MCP Server 时,config.ts 通过签名去重:

function getMcpServerSignature(config: McpServerConfig): string | null {
  const cmd = getServerCommandArray(config)
  if (cmd) return `stdio:${jsonStringify(cmd)}`
  const url = getServerUrl(config)
  if (url) return `url:${unwrapCcrProxyUrl(url)}`
  return null
}

规则:手动配置 > 插件配置 > Claude.ai 连接器。


23.6 MCPConnectionManager — React 集成

MCPConnectionManager.tsx 将 MCP 连接管理集成到 React 组件树:

function MCPConnectionManager({ children, dynamicMcpConfig, isStrictMcpConfig }) {
  const { reconnectMcpServer, toggleMcpServer } =
    useManageMCPConnections(dynamicMcpConfig, isStrictMcpConfig)

  return (
    <MCPConnectionContext.Provider value={{ reconnectMcpServer, toggleMcpServer }}>
      {children}
    </MCPConnectionContext.Provider>
  )
}

useManageMCPConnections(45KB)是最重要的 Hook,负责:

  1. 启动时批量连接所有配置的 MCP Server
  2. 监听配置变化,自动重连/断开
  3. 将连接状态、工具、资源同步到 AppState
  4. 处理重连逻辑(指数退避)

23.7 名称规范化

MCP 工具名称需要符合 API 模式 ^[a-zA-Z0-9_-]{1,64}$

function normalizeNameForMCP(name: string): string {
  let normalized = name.replace(/[^a-zA-Z0-9_-]/g, '_')
  // Claude.ai 服务器名称做额外清理
  if (name.startsWith('claude.ai ')) {
    normalized = normalized.replace(/_+/g, '_').replace(/^_|_$/g, '')
  }
  return normalized
}

示例:"My Cool Server""My_Cool_Server"


课后练习

练习 1:传输层对比

画一个表格,对比 Stdio、SSE、HTTP、InProcess 四种传输层的适用场景、性能特点、安全边界。思考为什么 Chrome MCP Server 选择了 InProcess 而不是 Stdio。

练习 2:连接状态追踪

阅读 types.ts 中的 5 种连接状态类型,画出完整的状态转换图。标注每种转换的触发条件(如认证失败、网络超时、用户操作)。

练习 3:配置合并模拟

给定以下配置:

  • .mcp.json(local): { "slack": { "command": "npx slack-mcp" } }
  • ~/.claude/settings.json(user): { "mcpServers": { "slack": { "command": "npx old-slack" }, "github": { ... } } }

根据层级优先级规则,推导最终的服务器列表。

练习 4:InProcessTransport 扩展

InProcessTransport.send() 使用 queueMicrotask 进行异步投递。尝试解释为什么不能直接同步调用 this.peer?.onmessage?.(message),会导致什么问题。


本课小结

要点内容
MCP 定位AI 模型访问外部工具/数据的统一协议标准
传输层Stdio(本地)、SSE/HTTP(远程)、WebSocket、InProcess(进程内)、SDK Bridge
连接状态5 种状态的判别联合:connected / failed / needs-auth / pending / disabled
client.ts119KB,连接管理(memoized)、工具注册(MCPTool)、资源注册、Session 管理
config.ts7 层配置优先级,原子写入 .mcp.json,签名去重
名称规范化normalizeNameForMCP 确保符合 API 模式

下一课预告

第 24 课:MCP 进阶 — OAuth、沙箱与官方注册表 — 深入 89KB 的 auth.ts,理解 OAuth 认证流程、Token 刷新机制、Elicitation 交互式认证、MCP 工具的权限继承、注入防护以及官方注册表的安全审计。