21-MCP服务

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-proxyws-ide 两种内部专用类型,每种都有独立的 schema:

  • McpStdioServerConfigSchema ------ command + args + env,最常用的本地子进程模式;
  • McpSSEServerConfigSchema / McpHTTPServerConfigSchema ------ url + headers + headersHelper + oauth,远程服务,可附带 OAuth 配置;
  • McpSSEIDEServerConfigSchema / McpWebSocketIDEServerConfigSchema ------ IDE 扩展专用,带 ideNameideRunningInWindows
  • McpSdkServerConfigSchema ------ name,进程内 SDK 服务(如 Chrome MCP);
  • McpClaudeAIProxyServerConfigSchema ------ url + idClaude.ai 托管的代理服务。

ConfigScopeSchema 定义了 7 种配置来源:localuserprojectdynamicenterpriseclaudeaimanaged,这个 scope 决定了配置优先级与可见性。ScopedMcpServerConfigMcpServerConfig 上加了 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 流程:

  1. Discovery ------ 调用 discoverAuthorizationServerMetadata 从 server 的 /.well-known/oauth-authorization-server 拉取 AS 元数据(authorization_endpoint、token_endpoint、registration_endpoint 等);
  2. DCR(Dynamic Client Registration) ------ 如果 AS 不支持 client_id_metadata_document_supported(CIMD),则调用 register 动态注册客户端,获取 clientId / clientSecret,存到 secure storage;
  3. Authorization Code + PKCE ------ 生成 code_verifiercode_challenge,构造 authorization URL 并通过本地 HTTP server(buildRedirectUri 找可用端口)接收回调;
  4. Token Exchange ------ 用 code + code_verifier 换取 access_token + refresh_token
  5. Refresh ------ tokens() 方法在 access_token 过期时用 refresh_token 自动刷新,刷新失败按原因分类(invalid_grant / transient_retries_exhausted / metadata_discovery_failed)触发不同的清理逻辑;
  6. Step-Up ------ wrapFetchWithStepUpDetection 检测 403 insufficient_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:

  1. RFC 8693 Token Exchange ------ 在企业 IdP 用 id_token 换取 ID-JAG(Identity JWT Authorization Grant);
  2. 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.tsfetchClaudeAIMcpConfigsIfEligibleBASE_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 会话只拉取一次。

关键设计要点

  1. 传输抽象统一 :6 种 transport + claudeai-proxy 都被 connectToServer 收敛到同一个 ConnectedMCPServer 类型,上层 fetchToolsForClient / callMCPToolWithUrlElicitatingRetry 无需感知传输差异;
  2. 三层缓存隔离connectToServer / fetchToolsForClient / fetchResourcesForClient 各自独立 memoize,连接断开时三层缓存联动清理,既避免重复握手又保证重连后立即拉到新工具列表;
  3. OAuth 兼容性"打补丁"normalizeOAuthErrorBody 修正 Slack 等 server 的 200+error 非标准行为,NONSTANDARD_INVALID_GRANT_ALIASES 归一化错误码,wrapFetchWithStepUpDetection 处理 scope 不足------这些兼容层让 Claude Code 能对接各类不严格遵守 RFC 的 OAuth server;
  4. 配置多源 + 签名去重 :7 种 ConfigScope 按优先级合并,plugin 与用户手动配置通过 computeDedupSignature 去重,CCR 代理 URL 被 unwrapCcrProxyUrl 还原后再签名,避免同一服务被注册两次;
  5. 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_URLgetOauthConfig 来自 constants/oauth.ts,与第一方 OAuth 共享配置;
  • secureStorage :OAuth token、client secret 通过 getSecureStorage() 持久化到 macOS Keychain / Windows Credential Manager / Linux libsecret;
  • compact 服务 :MCP 工具结果可能很大,truncateMcpContentIfNeededpersistBinaryContent 把大输出落地到磁盘,避免撑爆 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/ 默默承担。

相关推荐
绝世番茄1 小时前
HarmonyOS List 上拉加载更多(LoadMore)深度实战指南
华为·list·harmonyos·鸿蒙
勇踏前人未索之境1 小时前
Unity打包运行于鸿蒙手机
unity·智能手机·harmonyos
●VON2 小时前
鸿蒙 PC Markdown 编辑器离线专业渲染管线
华为·编辑器·harmonyos·鸿蒙
Helen_cai2 小时前
OpenHarmony 项目统一全局样式、尺寸、色彩主题封装 ThemeUtil(API23)
开发语言·前端·javascript·华为·harmonyos
youtootech2 小时前
HarmonyOS《柚兔学伴》项目实战12-Lottie 动画集成
华为·harmonyos
l134062082352 小时前
HarmonyOS应用开发实战:小事记 - 多媒体文件上传:@ohos.net.http 的 multipart/form-data 请求构造
后端·华为·harmonyos·鸿蒙系统
youtootech2 小时前
HarmonyOS《柚兔学伴》项目实战16-录音功能与权限管理
华为·harmonyos
xd1855785553 小时前
睡眠质量评估 —— 鸿蒙AI智能助手开发全流程解析
人工智能·华为·harmonyos·鸿蒙
Jcc3 小时前
鸿蒙 Navigation 模块化实践
harmonyos