MCP 集成
Claude Code 如何发现、连接、代理与鉴权 MCP 服务器:从 stdio 子进程,到 SSE / HTTP / WebSocket,再到进程内桥接。
01 概览
MCP(Model Context Protocol)是 Claude Code 接入外部工具和数据源的标准方式。对 Claude 来说,一个 MCP 服务器最终会被折叠成一组普通工具;对工程实现来说,它却是一整套独立子系统:配置装配、连接建链、OAuth、能力协商、工具代理和失败重连。
所以这节课真正要回答的问题不是“Claude 能不能调 MCP”,而是“Claude Code 如何把一个远程协议端点,变成和内置 Bash / Read / Write 同级的本地工具体验”。
连接生命周期、配置加载、OAuth 身份验证、传输构建、诱导、重复数据删除。
包装每个远程 MCP 调用的瘦代理工具。 在运行时用服务器工具列表中的真实姓名、模式和 call() 覆盖。
User-facing /mcp 斜杠命令:重新连接、启用/禁用、设置 UI。
React UI 面板:MCPSettings、MCPListPanel、ElicitationDialog、MCPReconnect、CapabilitySection。
2 运输类型
MCP 在 Claude Code 里本质上是一层传输抽象:stdio 适合本地子进程,SSE 和可流式 HTTP 适合远端服务,WebSocket 面向长连接场景,IDE 变体用于编辑器桥接,而 in-process 则是内部高性能通道。
所以“连上 MCP 服务器”这件事,在不同传输下其实意味着完全不同的生命周期成本:进程管理、鉴权方式、网络超时、重连行为,都会随传输类型而变化。
Subprocess spawn] B -->|sse| D[SSEClientTransport
Persistent EventSource +
OAuth / ClaudeAuthProvider] B -->|http| E[StreamableHTTPClientTransport
JSON + SSE on same POST
OAuth / session ingress] B -->|ws| F[WebSocketTransport
ws module or Bun WS] B -->|sse-ide| G[SSEClientTransport
IDE extension only
no auth] B -->|ws-ide| H[WebSocketTransport
IDE extension with
optional auth token] B -->|sdk| I[SdkControlClientTransport
In-process / control msgs] B -->|in-process| J[InProcessTransport
Linked pair
Chrome / Computer Use] style C fill:#22201d,stroke:#7d9ab8,color:#b8b0a4 style D fill:#22201d,stroke:#6e9468,color:#b8b0a4 style E fill:#22201d,stroke:#6e9468,color:#b8b0a4 style F fill:#22201d,stroke:#b8965e,color:#b8b0a4 style G fill:#22201d,stroke:#c47a50,color:#b8b0a4 style H fill:#22201d,stroke:#c47a50,color:#b8b0a4 style I fill:#22201d,stroke:#8e82ad,color:#b8b0a4 style J fill:#22201d,stroke:#8e82ad,color:#b8b0a4
生成一个子进程,通过 stdin/stdout 进行通信。 默认类型(省略 type 此处默认)。 Stderr 通过管道传输并限制为 64 MB。
HTTP 服务器发送的事件。 用途 ClaudeAuthProvider 对于 OAuth。 GET(SSE 流)有意跳过 60 秒的请求超时 — 只有 POST 才能获取。
可流式传输的 HTTP(MCP 2025-03-26 规范)。 做广告 Accept: application/json, text/event-stream 在每个帖子上。 支持 OAuth + 会话入口 JWT。
WebSocket 与 protocols: ['mcp']。 用途 ws 节点上的模块,本机 Bun WS。 支持代理和 mTLS。
IDE 扩展的 SSE 变体(VS Code 等)。 没有 OAuth。 基于锁定文件的身份验证令牌已计划但尚未连接。
IDE 扩展的 WebSocket 变体。 接受可选 authToken 发送为 X-Claude-Code-Ide-Authorization header.
到 SDK 进程的控制消息桥。 工具通过 stdout/stdin 调用路由。 从不直接生成进程。
由 Chrome MCP 和计算机使用使用。 createLinkedTransportPair() 创建两个 InProcessTransport 实例; 消息通过传递 queueMicrotask 以避免堆栈溢出。
代码:InProcessTransport链接对
// services/mcp/InProcessTransport.ts
class InProcessTransport implements Transport {
private peer: InProcessTransport | undefined
async send(message: JSONRPCMessage): Promise<void> {
// Deliver async to avoid stack depth in sync req/resp cycles
queueMicrotask(() => { this.peer?.onmessage?.(message) })
}
}
export function createLinkedTransportPair(): [Transport, Transport] {
const a = new InProcessTransport()
const b = new InProcessTransport()
a._setPeer(b); b._setPeer(a)
return [a, b]
}
3 配置范围级联
MCP 配置级联最大的价值,在于把‘声明来源很多’这件事变得可裁决。企业托管、命令行临时配置、项目配置、用户配置和插件配置,不再互相打架,而是进入一条明确优先级链。
这条链一旦确定,重复声明、覆盖关系和策略禁用都会变得可预测。换句话说,MCP 配置系统不是在“收集配置”,而是在“解决配置冲突”。
| Priority | Scope | 源文件/位置 | Notes |
|---|---|---|---|
| 1 最高 | enterprise |
managed-mcp.json (MDM 管理的路径) |
如果存在,则阻止所有用户管理的添加/删除。 独家控制。 |
| 2 | dynamic |
CLI 标志 --mcp-config <path> |
启动时通过; 使用前经过策略过滤。 |
| 3 | claudeai |
Claude.ai 连接器 API(远程获取) | 通过 URL 签名对手动配置的服务器进行重复数据删除。 |
| 4 | project |
.mcp.json (CWD 和父目录,根目录下) |
最接近 cwd 的获胜。 还搜索父目录; 孩子优先于父母。 |
| 5 | local |
~/.claude/projects/<hash>/ |
每个项目的本地状态,未签入 git。 |
| 6 | user |
~/.claude/settings.json (全局配置) |
用户范围的默认值。 |
| 7 | managed |
插件提供的服务器 | Namespaced plugin:name:server。 通过签名(URL 或命令数组)对手动服务器进行内容重复数据删除。 |
managed-mcp.json 存在,调用 addMcpConfig() 立即抛出: “企业 MCP 配置处于活动状态并具有独占控制权”。 类似的工具 claude mcp add 被完全封锁。
代码:作用域级联组装(由getAllMcpConfigs简化而来)
// services/mcp/config.ts — conceptual merge order
const allConfigs = {
...enterpriseServers, // wins if present
...dynamicServers, // --mcp-config flag
...claudeAiServers, // deduplicated by URL sig
...projectServers, // .mcp.json, root-down
...localServers, // ~/.claude/projects/…
...userServers, // ~/.claude/settings.json
...pluginServers, // namespaced, sig-deduplicated
}
// Env vars expanded in all configs before connection
// e.g. command: "npx", args: ["$MY_SERVER_PATH"]
策略允许/拒绝列表
企业政策可以定义 allowedMcpServers and deniedMcpServers 在设置中。 拒绝名单具有绝对优先权。 匹配可以通过:
- Name — 确切的服务器名称字符串
- Command — stdio 服务器的完整命令+参数数组
- 网址模式 — 与
*远程服务器的通配符
4 连接生命周期
从配置对象到真正可用的 MCP 工具,中间要经过一整条连接状态机:配置展开、批量连接、传输建立、超时竞争、认证处理、能力协商、工具/资源拉取,以及后续的实时变化订阅。
这也是为什么 MCP 集成看起来像“多了几把工具”,源码里却是一大块独立系统。Claude Code 实际上是在运行一个小型连接管理器,而不只是发几次 JSON-RPC 请求。
配置装配
所有范围均已合并并经过策略过滤。 环境变量扩展($VAR / ${VAR})。 缺少变量记录为警告,但不会阻止连接。
批量连接
Stdio 服务器以 3 个为一组进行连接(MCP_SERVER_CONNECTION_BATCH_SIZE)。 远程服务器批处理为 20。每次调用 connectToServer() 被记忆为 name + JSON(config).
交通建设
基于 serverRef.type,正确的 SDK 传输类被实例化。 此处附有身份验证提供程序、代理和 mTLS 选项。
client.connect() 超时
默认 30 秒(MCP_TIMEOUT 环境变量)。 比赛 connectPromise vs timeoutPromise。 超时还会关闭进程内服务器(如果已启动)。
授权处理
If UnauthorizedError (401):服务器移至 needs-auth 状态。 一个 McpAuthTool 注入伪工具,以便模型可以触发 OAuth。 15 分钟后,needs-auth 缓存条目将过期。
能力谈判
Claude Code 声明 roots: {} and elicitation: {} 能力。 服务器的功能读取通过 getServerCapabilities()。 服务器指令被截断为 2048 个字符。
工具/资源/提示获取
并行获取的工具、资源、提示。 标准化工具名称 (mcp__server__tool)。 每个工具都是克隆的 MCPTool 与覆盖 name, description, inputSchema, 和 call().
实时通知
订阅 ToolListChanged, ResourceListChanged, PromptListChanged 通知。 任何更改都会触发重新获取和 AppState 更新。 指数退避重新连接(1 秒 → 30 秒上限,最多 5 次尝试)。
代码:与超时竞赛的连接
// services/mcp/client.ts (simplified)
const connectPromise = client.connect(transport)
const timeoutPromise = new Promise<never>((_, reject) => {
const id = setTimeout(() => {
transport.close().catch(() => {})
reject(new Error(`MCP server "${name}" timed out after ${timeout}ms`))
}, getConnectionTimeoutMs())
connectPromise.then(() => clearTimeout(id), () => clearTimeout(id))
})
await Promise.race([connectPromise, timeoutPromise])
5 工具代理
MCP 工具代理最关键的意义,是把远程工具重新包装成本地工具语义:拥有规范化名字、描述、输入 schema,以及与内置工具一致的调用界面。
这样一来,Claude 在心智模型上根本不需要知道自己在调用‘远程工具’还是‘本地工具’,因为 Claude Code 已经把这层差异吃掉了。
tool.description
inputSchema
tool_name
描述、方案
call()
可供
Claude
→ JSON-RPC
→ 运输
名称规范化
// services/mcp/normalization.ts
export function normalizeNameForMCP(name: string): string {
// Replace any char not in [a-zA-Z0-9_-] with underscore
let normalized = name.replace(/[^a-zA-Z0-9_-]/g, '_')
// For claude.ai servers: collapse consecutive underscores,
// strip leading/trailing (__ delimiter must stay clean)
if (name.startsWith('claude.ai ')) {
normalized = normalized.replace(/_+/g, '_').replace(/^_|_$/g, '')
}
return normalized
}
// Full tool name: "mcp__my_server__list_files"
export function buildMcpToolName(server: string, tool: string): string {
return `mcp__${normalizeNameForMCP(server)}__${normalizeNameForMCP(tool)}`
}
tool.description。 Claude Code 对工具描述和服务器指令进行硬限制,位于 2048 个字符 保持上下文窗口易于管理。
代码:结果处理 — 图像、二进制 blob、截断
// After client.callTool() returns a CallToolResult...
// 1. Image content items → resized/downsampled, returned as base64
if (content.type === 'image' && IMAGE_MIME_TYPES.has(mimeType)) {
const buf = maybeResizeAndDownsampleImageBuffer(rawBuf)
// → ContentBlockParam with base64 data
}
// 2. Binary blobs → persisted to disk, path returned as text
if (!IMAGE_MIME_TYPES.has(mimeType)) {
await persistBinaryContent(content)
// → getBinaryBlobSavedMessage(path)
}
// 3. Total result > 100 KB → truncation with instructions
if (mcpContentNeedsTruncation(result)) {
result = truncateMcpContentIfNeeded(result)
}
6 OAuth 身份验证
OAuth 在这里不是‘用户自己打开网页登录一下’的附属逻辑,而是 MCP 生命周期的一部分。连接失败、进入 needs-auth、注入认证工具、拿到 token、重连服务器,这是一整条标准化链路。
它之所以重要,是因为 Claude Code 要让 MCP 服务器看起来像长期可用的本地能力,而不是每次掉认证都把控制权完全交还给用户手动处理。
needs-auth state,一个名为 mcp__<server>__authenticate 被注入。 该模型可以调用它来启动 OAuth 流程并接收授权 URL 以向用户显示。 一旦回调触发,真实工具会自动替换伪工具(AppState 上基于前缀的替换)。
令牌刷新和 Slack 怪癖
标准 RFC 6749 invalid_grant 错误会触发令牌失效。 但是 Slack 返回 HTTP 200 {"error":"invalid_refresh_token"} — SDK 会将其视为 ZodError。 Claude Code 将这些非标准代码规范化为 invalid_grant 在传递到 SDK 的错误类映射器之前。
代码:Slack 200 错误标准化
// services/mcp/auth.ts
const NONSTANDARD_INVALID_GRANT_ALIASES = new Set([
'invalid_refresh_token',
'expired_refresh_token',
'token_expired',
])
// Wraps fetch: peeks at 2xx POST responses, rewrites error bodies
// matching OAuthErrorResponseSchema (but NOT OAuthTokensSchema)
// to a synthetic 400 response — so SDK error-class mapping applies.
XAA——跨应用程序访问
SSO 流程的企业扩展。 什么时候 xaa: true 在 MCP 服务器配置上设置,系统不会启动浏览器,而是以静默方式将 IdP ID 令牌交换为 MCP 服务器的 OAuth 令牌。 配置一次 settings.xaaIdp,在所有启用 XAA 的服务器之间共享。
7 Elicitation
Elicitation 让 MCP 服务器不只是被动执行工具,还可以主动向用户索取结构化输入。Claude Code 在这里扮演的是中介层:把请求排队、交给 UI 或 hooks,再把结果送回服务器。
这意味着 MCP 协议不再只是“模型请求工具返回结果”,而是能把人也正式纳入交互闭环。
服务器发送 JSON Schema; 用户填写表格。 响应是 accept 与内容,或 decline / cancel.
服务器发送 URL(例如 OAuth 升级,外部确认)。 两阶段:打开URL→等待 ElicitationComplete 通知。 用户可以忽略或重试。
请求登陆 AppState.elicitation.queue as ElicitationRequestEvent 物体具有 respond() 打回来。 这 ElicitationDialog React 组件轮询该队列。 挂钩(executeElicitationHooks) 可以通过编程方式满足请求而不显示 UI。
代码:引发处理程序注册
// services/mcp/elicitationHandler.ts
client.setRequestHandler(ElicitRequestSchema, async (request, extra) => {
// 1. Try hooks first (programmatic response)
const hookResponse = await runElicitationHooks(serverName, request.params, extra.signal)
if (hookResponse) return hookResponse
// 2. Queue for user interaction
const response = new Promise<ElicitResult>(resolve => {
setAppState(prev => ({
...prev,
elicitation: {
queue: [...prev.elicitation.queue, {
serverName, requestId: extra.requestId,
params: request.params,
respond: resolve,
}],
},
}))
})
return await response
})
8 服务器重复数据删除
MCP 去重最值得注意的一点,是它看‘底层是不是同一台服务器’,而不是看‘名字是不是一样’。stdio 看命令签名,远程连接看 URL,这样才能正确识别重复定义。
这种做法尤其适合 Claude.ai 连接器、插件默认服务器和手工配置同时存在的情况。名字可能不同,但如果它们本质上连的是同一个端点,就只该保留一个生效版本。
mcp_url 查询停止。 unwrapCcrProxyUrl() 在签名比较之前提取它,因此直接指向 Slack 的 MCP 服务器的插件仍然可以针对通过 CCR 路由的 claude.ai 连接器正确地进行重复数据删除。
去重规则:
- 手动胜过插件 — 用户配置的总是胜过插件提供的。
- 第一个插件获胜 — 如果两个插件提供相同的服务器,则首先加载的插件获胜。
- 启用手动战胜 claude.ai — 禁用的手动服务器不会抑制其连接器孪生(因此两者都不会运行)。
- 插件服务器是命名空间的
plugin:name:server即使在重复数据删除之前也可以避免键冲突。
要点
- 7 种运输类型 — stdio、sse、http、ws、sse-ide、ws-ide、sdk — 以及供 Chrome/计算机使用的内部进程内对。 公共交通支持OAuth; IDE 变体是免身份验证的或基于令牌的。
- 7层范围级联 — 企业 > 动态 > claudeai > 项目 > 本地 > 用户 > 托管。 企业存在会锁定所有手动添加/删除操作。
- 配置文件遍历目录树 —
.mcp.json从每个父目录读取到文件系统根目录; 子目录覆盖父目录。 - 所有工具名称均经过规范化 —
mcp__<server>__<tool>,任何非字母数字字符都替换为下划线。 Claude.ai 服务器名称得到额外的折叠/剥离处理以保护__delimiter. - OAuth 是模型可触发的 - 这
McpAuthTool伪工具让 Claude 自主启动身份验证流程并将 URL 返回给用户; 真正的工具在回调后自动替换。 - 工具描述的硬上限为 2048 个字符 — 防止 OpenAPI 生成的服务器发生上下文爆炸。
- 重复数据删除是基于内容的,而不是基于名称的 — 相同的命令数组或 URL = 相同的服务器,无论它们在不同的配置源中被称为什么。
知识检查
stderr 设成 pipe,而不是直接打到用户终端?wrapFetchWithTimeout() 为什么刻意跳过 SSE 的 GET 请求超时?managed-mcp.json,用户执行 claude mcp add 通常会怎样?