Appearance
第11章 MCP 协议集成
在前面的章节中,我们详细剖析了 Claude Code 内建的工具体系——从 BashTool 到 FileWriteTool,从权限模型到沙箱隔离。这些内建工具覆盖了绝大多数编程场景,但软件世界的边界远不止于此。当用户需要与 Slack 交互、查询 GitHub Issues、调用私有 API,或者连接任何一个第三方服务时,仅凭内建工具是不够的。
这正是 Model Context Protocol(MCP)协议要解决的核心问题。MCP 定义了一套标准化的协议,让 AI 助手能够通过统一的接口发现、连接和调用任意第三方工具。Claude Code 对 MCP 的集成不是一个简单的适配层,而是一个涵盖连接管理、工具发现、权限控制、认证流程和生命周期管理的完整子系统。
本章将深入 Claude Code 的 MCP 集成实现,从协议架构出发,逐层解析服务器生命周期、工具发现与注册、工具调用流程、资源管理以及 OAuth 认证体系。通过源码分析,我们将看到 Claude Code 如何将一个开放协议转化为安全、可靠、可扩展的工具集成平台。
本章要点
- MCP 协议定位:理解 Model Context Protocol 如何解决第三方工具集成标准化问题,以及它与内建工具系统的关系
- 多传输层架构:stdio、SSE、HTTP Streamable、WebSocket 四种传输层的适用场景与实现差异
- 服务器生命周期:从配置加载、连接建立、心跳检测到错误恢复的完整生命周期管理
- 工具发现与注册:MCP 工具如何通过 MCPTool 包装器无缝接入 Claude Code 的工具体系
- 工具调用流程:tools/call 的请求-响应链路,包括超时控制、会话恢复和大结果处理
- 资源系统:resources/list 与 resources/read 如何将外部数据嵌入对话上下文
- OAuth 认证体系:ClaudeAuthProvider 如何实现完整的 OAuth 2.0 PKCE 流程,包括令牌刷新和跨应用认证
11.1 什么是 MCP,为什么它重要
11.1.1 Model Context Protocol 的定位
Model Context Protocol(MCP)是 Anthropic 提出的开放协议标准,目标是为 AI 助手与外部工具和数据源之间建立统一的通信接口。在 MCP 出现之前,每个 AI 产品都需要为每个第三方服务编写专门的集成代码——这意味着 N 个 AI 产品连接 M 个服务需要 NM 种集成实现。MCP 将这个 NM 问题简化为 N+M:每个 AI 产品实现一次 MCP 客户端,每个服务实现一次 MCP 服务器,两者即可自由组合。
从协议层面看,MCP 基于 JSON-RPC 2.0 构建,定义了三类核心能力:
- 工具(Tools):服务器暴露的可调用函数,类似于 REST API 的端点,但带有自描述的 JSON Schema 参数定义
- 资源(Resources):服务器提供的可读取数据,类似于文件系统中的文件,支持文本和二进制内容
- 提示(Prompts):服务器预定义的交互模板,可以携带参数化的上下文信息
11.1.2 MCP 在 Claude Code 中解决的核心问题
对 Claude Code 而言,MCP 解决了三个关键问题:
第一,工具集成的标准化。 在没有 MCP 的情况下,每增加一个外部工具都需要修改 Claude Code 的源码——定义 Tool 类型、实现 call 方法、编写权限检查逻辑。有了 MCP,第三方开发者只需编写一个 MCP 服务器,Claude Code 就能自动发现和使用其中的工具。
第二,工具生态的去中心化。 MCP 服务器可以由任何人开发和部署,运行在用户的本地机器上(stdio 传输)或远程服务器上(HTTP/SSE 传输)。用户通过配置文件声明要连接的服务器,Claude Code 负责连接管理。这种架构让工具生态可以独立于 Claude Code 本身演进。
第三,安全边界的明确化。 MCP 工具经过 Claude Code 的权限系统审查,用户对每个 MCP 工具的调用都需要明确授权。同时,MCP 的认证协议(OAuth 2.0)确保了与远程服务的安全通信。
11.2 MCP 架构总览
11.2.1 目录结构
Claude Code 的 MCP 集成代码集中在 src/services/mcp/ 目录下,包含约 25 个文件,总计超过 40 万字符的源码。这个规模反映了 MCP 集成的复杂度——它不仅仅是一个协议适配器,更是一个完整的分布式系统客户端:
src/services/mcp/
types.ts # 核心类型定义:配置 Schema、连接状态、资源类型
client.ts # MCP 客户端核心:连接、工具发现、工具调用
config.ts # 配置管理:多作用域配置加载与合并
auth.ts # OAuth 认证:ClaudeAuthProvider 实现
MCPConnectionManager.tsx # React 上下文:连接管理的组件化封装
useManageMCPConnections.ts # React Hook:连接生命周期管理
normalization.ts # 名称规范化:确保服务器名符合 API 约束
mcpStringUtils.ts # 字符串工具:MCP 工具名的构建与解析
envExpansion.ts # 环境变量展开:配置中的 ${VAR} 语法支持
headersHelper.ts # 动态请求头:通过外部命令获取认证头
channelNotification.ts # 通道通知:MCP 服务器的消息推送机制
channelPermissions.ts # 通道权限:推送消息的权限控制
channelAllowlist.ts # 通道白名单:允许推送的服务器列表
elicitationHandler.ts # 引出处理:MCP 服务器向用户请求输入
utils.ts # 工具函数:过滤、匹配、状态管理
oauthPort.ts # OAuth 端口:本地回调服务器端口管理
xaa.ts # 跨应用认证:XAA 令牌交换
xaaIdpLogin.ts # XAA IdP 登录:身份提供商交互
claudeai.ts # Claude.ai 集成:云端 MCP 连接器
InProcessTransport.ts # 进程内传输:用于内嵌 MCP 服务器
SdkControlTransport.ts # SDK 控制传输:SDK 模式的专用传输层
officialRegistry.ts # 官方注册表:MCP 服务器注册中心
vscodeSdkMcp.ts # VS Code 集成:IDE 内的 MCP 支持11.2.2 核心文件职责
types.ts 定义了 MCP 子系统的所有核心类型。其中最关键的是传输类型枚举和服务器配置 Schema:
typescript
// 源码文件:src/services/mcp/types.ts
export const TransportSchema = lazySchema(() =>
z.enum(['stdio', 'sse', 'sse-ide', 'http', 'ws', 'sdk']),
)
export type MCPServerConnection =
| ConnectedMCPServer // 已连接:持有 Client 实例
| FailedMCPServer // 连接失败:记录错误信息
| NeedsAuthMCPServer // 需要认证:等待 OAuth 流程
| PendingMCPServer // 连接中:含重连尝试次数
| DisabledMCPServer // 已禁用:用户主动关闭这个五状态联合类型是理解 MCP 生命周期的钥匙。一个 MCP 服务器在其生命周期内会在这五种状态之间转换,每种状态对应不同的 UI 展示和行为逻辑。
client.ts 是整个 MCP 子系统中最大的文件(约 3300 行),承担了连接建立、工具发现、工具调用和结果处理的核心逻辑。它的复杂度来自于对多种传输层的统一抽象,以及对各种边界情况的细致处理。
config.ts 实现了多层级配置合并,支持从本地(.mcp.json)、用户级、项目级、企业级、动态注入和 Claude.ai 云端等多个来源加载 MCP 服务器配置,并按优先级合并。
auth.ts 实现了完整的 OAuth 2.0 PKCE 认证流程,包括动态客户端注册、令牌存储、令牌刷新和跨应用认证(XAA)。
11.2.3 架构分层
Claude Code 的 MCP 集成采用清晰的分层架构,从配置加载到工具调用形成完整链路。下图展示了各层之间的依赖关系:
+-----------------------------------------------------------------+
| Claude Code 主进程 |
| +-----------------------------------------------------------+ |
| | MCPConnectionManager (React Context) | |
| | 提供 reconnect / toggle 操作给 UI 组件 |
| +-----------------------------------------------------------+ |
| | useManageMCPConnections (React Hook) | |
| | 生命周期管理: 初始化 / 重连 / 状态同步 / 批量更新 |
| +-----------------------------------------------------------+ |
| | client.ts (核心客户端层) | |
| | connectToServer / fetchToolsForClient / callMCPTool | |
| +-----------------------------------------------------------+ |
| | config.ts (配置层) | auth.ts (认证层) | |
| | 多作用域配置加载与合并 | OAuth PKCE + XAA | |
| +-----------------------------------------------------------+ |
| | 传输层适配 | |
| | StdioTransport | SSETransport | HTTPTransport | WSTransport| |
| +-----------------------------------------------------------+ |
+-----------------------------------------------------------------+
| | | |
[stdio 子进程] [SSE 服务器] [HTTP 服务器] [WS 服务器]这个分层架构有一个重要的设计特点:传输层对上层完全透明。无论底层是 stdio 子进程还是远程 HTTP 服务器,上层的工具发现和调用逻辑都使用完全相同的 MCP Client 接口。这种抽象让 Claude Code 能够在不修改业务逻辑的情况下支持新的传输协议。
11.3 服务器生命周期管理
11.3.1 配置加载:多源合并的优先级策略
MCP 服务器配置可以来自多个来源,Claude Code 通过 getClaudeCodeMcpConfigs() 函数实现了一套精细的配置合并策略:
typescript
// 源码文件:src/services/mcp/config.ts
export async function getClaudeCodeMcpConfigs(
dynamicServers: Record<string, ScopedMcpServerConfig> = {},
extraDedupTargets: Promise<Record<string, ScopedMcpServerConfig>>
= Promise.resolve({}),
): Promise<{
servers: Record<string, ScopedMcpServerConfig>
errors: PluginError[]
}> {
const { servers: enterpriseServers } = getMcpConfigsByScope('enterprise')
// 如果存在企业级配置,则独占控制所有 MCP 服务器
if (doesEnterpriseMcpConfigExist()) {
const filtered: Record<string, ScopedMcpServerConfig> = {}
for (const [name, serverConfig] of Object.entries(enterpriseServers)) {
if (!isMcpServerAllowedByPolicy(name, serverConfig)) continue
filtered[name] = serverConfig
}
return { servers: filtered, errors: [] }
}
// 按优先级加载各层级配置
const { servers: userServers } = getMcpConfigsByScope('user')
const { servers: projectServers } = getMcpConfigsByScope('project')
const { servers: localServers } = getMcpConfigsByScope('local')
// 合并顺序:plugin < user < project < local
const configs = Object.assign(
{}, dedupedPluginServers, userServers,
approvedProjectServers, localServers,
)
// ...
}配置来源的优先级从低到高为:插件 < 用户级 < 项目级 < 本地。这意味着本地的 .mcp.json 文件拥有最高优先级,可以覆盖用户级和项目级的同名配置。企业级配置是一个特例——当它存在时,会完全替代其他所有配置源,这是为了满足企业安全管控需求。
每个配置项都带有一个 scope 标记,记录它来自哪个层级。这个元数据在后续的权限检查和 UI 展示中发挥作用——例如,项目级配置的 MCP 服务器需要用户显式批准后才能使用。
配置合并还包含一个精巧的去重机制。当一个插件提供的 MCP 服务器与用户手动配置的服务器实际上指向同一个底层进程或 URL 时,系统会通过签名比较来检测并消除重复:
typescript
// 源码文件:src/services/mcp/config.ts
export 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
}对于 stdio 类型,签名是命令行数组的序列化;对于远程类型,签名是 URL(如果经过代理包装,会先解包还原出原始 URL)。签名相同的服务器只保留优先级较高的那个。
11.3.2 连接建立:多传输层的统一抽象
连接建立的核心是 connectToServer 函数,它根据配置中的 type 字段选择相应的传输层实现:
typescript
// 源码文件:src/services/mcp/client.ts
export const connectToServer = memoize(
async (
name: string,
serverRef: ScopedMcpServerConfig,
): Promise<MCPServerConnection> => {
let transport
if (serverRef.type === 'sse') {
const authProvider = new ClaudeAuthProvider(name, serverRef)
const combinedHeaders = await getMcpServerHeaders(name, serverRef)
transport = new SSEClientTransport(
new URL(serverRef.url),
{ authProvider, fetch: wrapFetchWithTimeout(...), ... },
)
} else if (serverRef.type === 'http') {
// HTTP Streamable 传输
transport = new StreamableHTTPClientTransport(...)
} else if (serverRef.type === 'ws') {
// WebSocket 传输
transport = new WebSocketTransport(wsClient)
} else if (serverRef.type === 'stdio' || !serverRef.type) {
// stdio 传输(默认)
transport = new StdioClientTransport({
command: finalCommand,
args: finalArgs,
env: { ...subprocessEnv(), ...serverRef.env },
stderr: 'pipe',
})
}
const client = new Client(
{ name: 'claude-code', version: MACRO.VERSION ?? 'unknown' },
{ capabilities: { roots: {}, elicitation: {} } },
)
// 带超时的连接尝试
await Promise.race([
client.connect(transport),
timeoutPromise,
])
// ...
},
getServerCacheKey,
)这里有几个关键的设计决策值得注意:
连接缓存(Memoization)。 connectToServer 使用 lodash 的 memoize 进行缓存,缓存键由服务器名称和配置的序列化结果组合而成。这意味着对同一个服务器的重复连接请求会直接返回缓存的连接实例,避免了重复的进程创建或网络握手。
超时控制。 每次连接都有超时保护。通过 Promise.race 将连接操作与超时 Promise 竞争,如果在指定时间内(默认约 30 秒)未能建立连接,则抛出超时错误。
stderr 管道。 stdio 传输的 stderr 配置为 'pipe',这意味着 MCP 服务器进程的错误输出不会直接打印到用户终端,而是被捕获到内存缓冲区中供调试使用。缓冲区大小被限制在 64MB 以防止内存溢出。
能力声明。 Client 在初始化时声明了两个能力:roots(允许服务器查询工作目录)和 elicitation(允许服务器向用户请求输入)。注释中特别说明了 elicitation 声明使用空对象而非详细结构——因为某些 MCP 服务器实现(如 Spring AI)在遇到未知属性时会抛出错误。
11.3.3 stdio 传输层详解
stdio 传输是最常用的传输类型,也是默认类型。它通过启动一个子进程来运行 MCP 服务器,使用标准输入/输出(stdin/stdout)进行 JSON-RPC 消息交换:
typescript
// 源码文件:src/services/mcp/client.ts
transport = new StdioClientTransport({
command: finalCommand,
args: finalArgs,
env: {
...subprocessEnv(), // 继承清理后的环境变量
...serverRef.env, // 覆盖用户指定的环境变量
} as Record<string, string>,
stderr: 'pipe',
})环境变量的处理体现了安全意识:subprocessEnv() 提供了一个经过清理的基础环境,服务器配置中的 env 字段允许用户覆盖特定变量。配置文件中的环境变量支持 ${VAR} 和 ${VAR:-default} 两种展开语法,由 envExpansion.ts 中的 expandEnvVarsInString 函数实现:
typescript
// 源码文件:src/services/mcp/envExpansion.ts
export function expandEnvVarsInString(value: string): {
expanded: string
missingVars: string[]
} {
const missingVars: string[] = []
const expanded = value.replace(/\$\{([^}]+)\}/g, (match, varContent) => {
const [varName, defaultValue] = varContent.split(':-', 2)
const envValue = process.env[varName]
if (envValue !== undefined) return envValue
if (defaultValue !== undefined) return defaultValue
missingVars.push(varName)
return match
})
return { expanded, missingVars }
}这个设计既支持灵活的变量注入,又能追踪缺失的变量以便报告错误。
11.3.4 错误恢复与自动重连
当一个已连接的 MCP 服务器断开时,Claude Code 会根据传输类型采取不同的恢复策略。这个逻辑在 useManageMCPConnections 中实现:
typescript
// 源码文件:src/services/mcp/useManageMCPConnections.ts
const MAX_RECONNECT_ATTEMPTS = 5
const INITIAL_BACKOFF_MS = 1000
const MAX_BACKOFF_MS = 30000
client.client.onclose = () => {
// stdio 和 sdk 类型不支持自动重连
if (configType !== 'stdio' && configType !== 'sdk') {
const reconnectWithBackoff = async () => {
for (let attempt = 1; attempt <= MAX_RECONNECT_ATTEMPTS; attempt++) {
if (isMcpServerDisabled(client.name)) return
updateServer({
...client, type: 'pending',
reconnectAttempt: attempt,
maxReconnectAttempts: MAX_RECONNECT_ATTEMPTS,
})
try {
const result = await reconnectMcpServerImpl(client.name, client.config)
if (result.client.type === 'connected') {
onConnectionAttempt(result)
return
}
} catch (error) { /* ... */ }
// 指数退避
const backoffMs = Math.min(
INITIAL_BACKOFF_MS * Math.pow(2, attempt - 1),
MAX_BACKOFF_MS,
)
await new Promise(resolve => setTimeout(resolve, backoffMs))
}
}
void reconnectWithBackoff()
} else {
updateServer({ ...client, type: 'failed' })
}
}