21. MCP 服务
所属分组:服务层
概述
MCP(Model Context Protocol,模型上下文协议)是 Anthropic 推出的开放协议,用于让 LLM 客户端以统一方式接入外部工具、资源与 prompt。Claude Code 在 services/mcp/ 目录下实现了完整的 MCP 客户端栈,能够同时管理本地 stdio 子进程、远程 SSE/HTTP/WebSocket 服务、IDE 扩展、Claude.ai 托管代理、以及进程内 SDK 服务等多种传输方式,并把它们统一暴露为 mcp__<server>__<tool> 形式的工具供主循环调用。
MCP 服务的复杂度远超普通 HTTP 客户端:它需要处理协议握手(capabilities 协商)、OAuth 2.1 授权码流程(含 PKCE、Dynamic Client Registration、Step-Up 检测、Token 刷新与吊销)、企业级 Cross-App Access(XAA / SEP-990,无浏览器场景下的 token 交换)、会话过期重连、工具结果截断与二进制持久化、多源配置合并(local / user / project / enterprise / claudeai / plugin / managed)等大量边界情况。Claude Code 把这些复杂度封装在 connectToServer / getMcpToolsCommandsAndResources / callMCPToolWithUrlElicitationRetry 等入口背后,使上层只需关心 Tool 抽象。
本篇聚焦 MCP 客户端的连接管理、配置解析、OAuth 认证三大主线,并通过 types.ts 理解其类型模型。
源码位置
- client.ts(file:///e:/2026plan/AI_Lab/claude-code-sourcemap-main/restored-src/src/services/mcp/client.ts) --- MCP 客户端核心,
connectToServer/getMcpToolsCommandsAndResources/callMCPToolWithUrlElicitingRetry - config.ts(file:///e:/2026plan/AI_Lab/claude-code-sourcemap-main/restored-src/src/services/mcp/config.ts) --- 多源配置合并与
.mcp.json写入 - auth.ts(file:///e:/2026plan/AI_Lab/claude-code-sourcemap-main/restored-src/src/services/mcp/auth.ts) ---
ClaudeAuthProvider、OAuth 流程、token 刷新与 Step-Up 检测 - types.ts(file:///e:/2026plan/AI_Lab/claude-code-sourcemap-main/restored-src/src/services/mcp/types.ts) --- Zod schema 与连接状态类型
- claudeai.ts(file:///e:/2026plan/AI_Lab/claude-code-sourcemap-main/restored-src/src/services/mcp/claudeai.ts) --- Claude.ai 托管 MCP 服务器拉取
- utils.ts(file:///e:/2026plan/AI_Lab/claude-code-sourcemap-main/restored-src/src/services/mcp/utils.ts) --- 工具/命令过滤工具函数
- xaa.ts(file:///e:/2026plan/AI_Lab/claude-code-sourcemap-main/restored-src/src/services/mcp/xaa.ts) --- Cross-App Access token 交换
- MCPConnectionManager.tsx(file:///e:/2026plan/AI_Lab/claude-code-sourcemap-main/restored-src/src/services/mcp/MCPConnectionManager.tsx) --- React 层连接管理器
核心实现分析
1. 类型模型 types.ts
types.ts 用 Zod schema 定义了所有支持的 MCP server 配置形态。TransportSchema 列举了 6 种传输:stdio / sse / sse-ide / http / ws / sdk,外加 claudeai-proxy 与 ws-ide 两种内部专用类型,每种都有独立的 schema:
McpStdioServerConfigSchema------command + args + env,最常用的本地子进程模式;McpSSEServerConfigSchema/McpHTTPServerConfigSchema------url + headers + headersHelper + oauth,远程服务,可附带 OAuth 配置;McpSSEIDEServerConfigSchema/McpWebSocketIDEServerConfigSchema------ IDE 扩展专用,带ideName与ideRunningInWindows;McpSdkServerConfigSchema------name,进程内 SDK 服务(如 Chrome MCP);McpClaudeAIProxyServerConfigSchema------url + id,Claude.ai 托管的代理服务。
ConfigScopeSchema 定义了 7 种配置来源:local、user、project、dynamic、enterprise、claudeai、managed,这个 scope 决定了配置优先级与可见性。ScopedMcpServerConfig 在 McpServerConfig 上加了 scope 与可选的 pluginSource,后者用于插件去重。
连接状态用一组 discriminated union 表示:ConnectedMCPServer / FailedMCPServer / NeedsAuthMCPServer / PendingMCPServer / DisabledMCPServer,UI 据此渲染不同的状态徽标。
2. 配置合并 config.ts
getAllMcpConfigs 是 MCP 配置的统一入口,它按优先级合并多个来源:managed(企业下发,managed-mcp.json)→ user(全局 ~/.claude.json)→ project(.mcp.json)→ claudeai(远程拉取)→ dynamic(运行时动态注册)→ plugin(插件提供)→ enterprise(设置同步)。每个 server config 都被打上对应的 scope 标签。
.mcp.json 的写入采用了"临时文件 + fsync + atomic rename"模式以保证安全:
ts
const tempPath = `${mcpJsonPath}.tmp.${process.pid}.${Date.now()}`
const handle = await open(tempPath, 'w', existingMode ?? 0o644)
await handle.writeFile(jsonStringify(config, null, 2), { encoding: 'utf8' })
await handle.datasync()
// ... chmod + rename
这种写法避免了写入中途崩溃导致配置文件损坏,并保留了原文件的权限位(如 0o600)。
config.ts 还实现了 plugin 去重:通过 computeDedupSignature 为每个 server 生成一个签名(stdio 用 command+args,远程用 unwrapCcrProxyUrl(url)),同一签名的 server 只保留一个,避免插件与用户手动配置重复注册同一个 MCP 服务。unwrapCcrProxyUrl 会剥离 CCR(Claude Code Remote)代理路径,把 /v2/session_ingress/shttp/mcp/...?mcp_url=<原始 URL> 还原成原始 vendor URL,使签名能跨"直连 vs CCR 代理"匹配。
3. 连接管理 client.ts
connectToServer 是 MCP 客户端的核心,被 memoize 包装以避免重复连接。函数内部根据 serverRef.type 分支选择 Transport:
- stdio ------
new StdioClientTransport({ command, args, env }),会启动子进程并通过 stderr 监听日志; - sse ------
new SSEClientTransport(url, { authProvider, fetch, eventSourceInit }),注意eventSourceInit的 fetch 不能套 timeout 包装,因为 SSE 是长连接; - http ------
new StreamableHTTPClientTransport(url, { authProvider, fetch }),是 MCP 推荐的流式 HTTP 传输; - ws / ws-ide ------ 自实现
WebSocketTransport,支持 proxy 与 mTLS; - sdk ------ 进程内 SDK,通过
InProcessTransport直接内存通信,无需序列化; - claudeai-proxy ------ 通过
createClaudeAiProxyFetch包装 fetch,注入 session ingress JWT。
每个 transport 在连接后都会调用 client.request({ method: 'initialize' }, ...) 完成协议握手,协商 capabilities(tools / resources / prompts / logging),并把 server info 与 instructions 缓存到 ConnectedMCPServer。
连接关闭时(client.onclose)会清理三层缓存:fetchToolsForClient.cache / fetchResourcesForClient.cache / fetchCommandsForClient.cache,并删除 connectToServer.cache 中对应的 key,确保下次访问时重新连接。对 stdio 服务还会显式发送 SIGINT 信号并等待 500ms 优雅退出,否则 Docker 容器等场景下子进程可能无法触发 graceful shutdown。
getMcpToolsCommandsAndResources 是上层的批量入口,它先把所有 server 分成 local(stdio/sdk,低并发)和 remote(sse/http/ws,高并发)两组,用 p-map 并行连接,并对最近 15 分钟内返回 401 的 server 跳过连接(直接标记为 needs-auth 并附上 createMcpAuthTool),避免每次启动都打一遍无意义的 OAuth 探测。
4. 工具与资源拉取
fetchToolsForClient 调用 tools/list 拉取工具列表,把每个 MCP tool 转换成 Claude Code 内部的 Tool 对象:
ts
return {
...MCPTool,
name: skipPrefix ? tool.name : fullyQualifiedName,
mcpInfo: { serverName: client.name, toolName: tool.name },
isMcp: true,
searchHint: tool._meta?.['anthropic/searchHint'],
alwaysLoad: tool._meta?.['anthropic/alwaysLoad'] === true,
isConcurrencySafe: () => tool.annotations?.readOnlyHint ?? false,
isReadOnly: () => tool.annotations?.readOnlyHint ?? false,
isDestructive: () => tool.annotations?.destructiveHint ?? false,
isOpenWorld: () => tool.annotations?.openWorldHint ?? false,
// ...
}
这里关键设计是把 MCP 标准的 annotations(readOnlyHint / destructiveHint / openWorldHint)映射到 Claude Code 的权限/调度接口:isConcurrencySafe 决定是否可与其他工具并行执行,isDestructive 决定是否需要额外确认,isOpenWorld 影响 auto-mode 分类器。tool._meta['anthropic/searchHint'] 与 anthropic/alwaysLoad 是 Anthropic 私有扩展,分别用于工具搜索与强制加载。
工具描述被截断到 MAX_MCP_DESCRIPTION_LENGTH = 2048 字符,避免 OpenAPI 生成的 MCP server 把几十 KB 的文档塞进 prompt。工具名通过 buildMcpToolName(serverName, toolName) 拼成 mcp__<server>__<tool> 形式,server 名先经 normalizeNameForMCP 处理(去特殊字符、空格转下划线)。
5. OAuth 认证 auth.ts
ClaudeAuthProvider 实现了 MCP SDK 的 OAuthClientProvider 接口,是远程 MCP server 鉴权的核心。它支持完整的 OAuth 2.1 + PKCE 流程:
- Discovery ------ 调用
discoverAuthorizationServerMetadata从 server 的/.well-known/oauth-authorization-server拉取 AS 元数据(authorization_endpoint、token_endpoint、registration_endpoint 等); - DCR(Dynamic Client Registration) ------ 如果 AS 不支持
client_id_metadata_document_supported(CIMD),则调用register动态注册客户端,获取clientId/clientSecret,存到 secure storage; - Authorization Code + PKCE ------ 生成
code_verifier与code_challenge,构造 authorization URL 并通过本地 HTTP server(buildRedirectUri找可用端口)接收回调; - Token Exchange ------ 用
code + code_verifier换取access_token + refresh_token; - Refresh ------
tokens()方法在 access_token 过期时用 refresh_token 自动刷新,刷新失败按原因分类(invalid_grant/transient_retries_exhausted/metadata_discovery_failed)触发不同的清理逻辑; - Step-Up ------
wrapFetchWithStepUpDetection检测 403insufficient_scope,调用markStepUpPending标记需要升级授权,下次tokens()会故意省略 refresh_token,强制 SDK 走完整的 authorization 流程(RFC 6749 §6 禁止通过 refresh 提升 scope)。
normalizeOAuthErrorBody 是一个有意思的兼容层:有些 OAuth server(如 Slack)在 token 失效时返回 HTTP 200 + {"error":"invalid_grant"},而 SDK 只在 !response.ok 时才解析错误,导致 Zod 校验失败被当成 request_failed。这个 wrapper 会 peek 2xx 响应体,若匹配 OAuthErrorResponseSchema 则改写成 400 响应,并把 Slack 的非标准错误码(invalid_refresh_token / expired_refresh_token / token_expired)归一化为 invalid_grant,使 token 失效逻辑能正确触发。
6. XAA(Cross-App Access)
xaa.ts 实现了企业场景下的无浏览器认证(SEP-990)。它通过两步 token 交换获得 MCP access_token:
- RFC 8693 Token Exchange ------ 在企业 IdP 用 id_token 换取 ID-JAG(Identity JWT Authorization Grant);
- RFC 7523 JWT Bearer Grant ------ 在 MCP 的 AS 用 ID-JAG 换取 access_token。
这样企业用户在 IdP 已登录后,无需再打开浏览器走 MCP 的 OAuth 同意页,即可直接访问 MCP 服务。xaaIdpLogin.ts 负责 IdP 侧的 OIDC discovery、client secret 管理与 id_token 缓存。
7. Claude.ai 托管 MCP
claudeai.ts 的 fetchClaudeAIMcpConfigsIfEligible 从 BASE_API_URL/v1/mcp_servers 拉取用户在 Claude.ai 网页端配置的 MCP 服务列表,使用 mcp-servers-2025-12-04 beta header。它直接检查 user:mcp_servers scope 而非 isClaudeAISubscriber(),因为非交互模式下设置 ANTHROPIC_API_KEY 会让 isClaudeAISubscriber() 返回 false,但 OAuth token 仍可用。返回结果被 memoize 以保证一次 CLI 会话只拉取一次。
关键设计要点
- 传输抽象统一 :6 种 transport + claudeai-proxy 都被
connectToServer收敛到同一个ConnectedMCPServer类型,上层fetchToolsForClient/callMCPToolWithUrlElicitatingRetry无需感知传输差异; - 三层缓存隔离 :
connectToServer/fetchToolsForClient/fetchResourcesForClient各自独立 memoize,连接断开时三层缓存联动清理,既避免重复握手又保证重连后立即拉到新工具列表; - OAuth 兼容性"打补丁" :
normalizeOAuthErrorBody修正 Slack 等 server 的 200+error 非标准行为,NONSTANDARD_INVALID_GRANT_ALIASES归一化错误码,wrapFetchWithStepUpDetection处理 scope 不足------这些兼容层让 Claude Code 能对接各类不严格遵守 RFC 的 OAuth server; - 配置多源 + 签名去重 :7 种 ConfigScope 按优先级合并,plugin 与用户手动配置通过
computeDedupSignature去重,CCR 代理 URL 被unwrapCcrProxyUrl还原后再签名,避免同一服务被注册两次; - stdio 进程优雅退出 :cleanup 时显式发
SIGINT并轮询process.kill(pid, 0)检测进程存活,500ms 内未退出才升级信号,兼顾 Docker 容器的 graceful shutdown 需求与 CLI 响应性。
与其他模块的关系
- Tool 系统 :
MCPTool是所有 mcp 工具共享的 Tool 实现,fetchToolsForClient把每个 MCP tool 包装成带mcpInfo的 Tool 注入主循环; - 权限系统 :
channelPermissions.ts/channelAllowlist.ts决定 MCP 工具是否需要用户确认,依赖isDestructive/isOpenWorld等 annotation; - analytics 服务 :
logEvent('tengu_mcp_*')贯穿连接、工具调用、OAuth 流程,是 MCP 可靠性归因的主要数据源; - oauth 服务 :
MCP_CLIENT_METADATA_URL、getOauthConfig来自constants/oauth.ts,与第一方 OAuth 共享配置; - secureStorage :OAuth token、client secret 通过
getSecureStorage()持久化到 macOS Keychain / Windows Credential Manager / Linux libsecret; - compact 服务 :MCP 工具结果可能很大,
truncateMcpContentIfNeeded与persistBinaryContent把大输出落地到磁盘,避免撑爆 context; - plugins 服务 :
getPluginMcpServers让插件能注册 MCP server,与用户配置走相同的去重逻辑; - IDE 集成 :
sse-ide/ws-ide传输与 IDE 扩展(VSCode、JetBrains)通信,maybeNotifyIDEConnected在连接成功后通知 IDE。
小结
MCP 服务是 Claude Code 扩展性的基石:通过一套统一的传输抽象与 OAuth 认证框架,它把"接入任意外部工具"这件事简化成了"写一个 MCP server 并在 .mcp.json 里登记"。其实现细节体现了大量工程经验------从 OAuth 兼容性补丁到 stdio 优雅退出,从三层缓存联动到 CCR 代理 URL 还原,每一处都对应着真实场景中踩过的坑。理解 MCP 服务之后,再看 mcp__xxx__yyy 形式的工具调用就会清晰得多:那背后是一次完整的握手、一次 OAuth token 刷新、一次 tools/list 拉取、以及一次 tools/call RPC,全部由 services/mcp/ 默默承担。