从Claude Code泄露源码看工程架构:第八章 —— MCP 接入层设计

本文深入剖析 Claude Code 的 MCP接入层架构。通过分析统一入口设计、六种传输协议的差异化处理、认证缓存机制以及资源发现流程,揭示其"统一入口,不抹掉差异"的设计哲学。该设计在支持多协议的同时,保留了每种传输方式的关键特性,为长期维护提供了坚实基础。

1. 问题定义

在 AI 辅助编程系统中,MCP(Model Context Protocol) 允许模型访问外部工具和服务。然而,MCP 接入面临三个核心挑战:

  1. 协议多样性:如何支持 SSE、WebSocket、HTTP、stdio 等多种传输方式?
  2. 认证复杂性:如何处理 OAuth、session ingress token、代理认证等多套认证机制?
  3. 长期维护性:如何避免每种协议各写一套独立接入流程导致维护困难?

Claude Code 通过统一入口 + 差异化分支的架构系统性解决了这些问题。它摒弃了传统插件系统中的常见的做法:

  • 每种协议各写一套独立接入流程,最后维护成一团
  • 强行抽象得太早,把不同传输的关键差异也抹平

2. 架构概览:MCP 接入层全景

graph TD A[connectToServer] --> B{serverRef.type 判断} B -->|sse| C[SSE 传输] C --> C1[认证提供者] C --> C2[长连接特殊处理] C --> C3[超时包装区分] B -->|ws| D[WebSocket 传输] D --> D1[Header 脱敏日志] D --> D2[Proxy/TLS 配置] D --> D3[Bun vs Node 适配] B -->|http| E[HTTP 传输] E --> E1[OAuth Token 检测] E --> E2[认证优先级协调] E --> E3[Session Ingress 备选] B -->|claudeai-proxy| F[代理传输] F --> F1[复用 HTTP Transport] F --> F2[Claude.ai OAuth] F --> F3[URL 重写] B -->|stdio| G[Stdio 传输] G --> G1[默认子进程] G --> G2[Chrome MCP in-process] G --> G3[Computer Use in-process] G --> G4[Stderr 监控] B -->|sdk| H[SDK 外派] H --> H1[明确不归这层处理] I[认证缓存] --> A J[资源发现] --> K[ListMcpResourcesTool] J --> L[ReadMcpResourceTool] style A fill:#e1f5ff style C fill:#fff4e1 style D fill:#fff4e1 style E fill:#fff4e1 style F fill:#fff4e1 style G fill:#fff4e1 style H fill:#ffe1e1
组件 文件位置 职责
统一入口 client.ts:595-607 所有协议连接的总闸口
认证缓存 client.ts:257-287 15分钟 TTL 的认证结果缓存
SSE 传输 client.ts:619-677 长连接特殊处理
WebSocket client.ts:735-783 Header 脱敏与 Proxy 配置
HTTP 传输 client.ts:784-865 OAuth 与 session ingress 协调
Claude.ai Proxy client.ts:868-904 身份代理与 URL 重写
Stdio 传输 client.ts:905-959 子进程管理与 in-process 优化
SDK 外派 client.ts:866-867 明确不归这层处理

3. 关键常量:认证缓存 TTL

文件位置services/mcp/client.ts:257-287

typescript 复制代码
257:const MCP_AUTH_CACHE_TTL_MS = 15 * 60 * 1000 // 15 min
...
282:  const entry = cache[serverId]
283:  if (!entry) {
284:    return false
285:  }
286:  return Date.now() - entry.timestamp < MCP_AUTH_CACHE_TTL_MS
287:}

认证缓存 TTL 被定成 15 分钟,不算长,也绝不算短。这在以下三个维度可以取得平衡点:

考量维度 过短 TTL(如 1 分钟) 过长 TTL(如 1 小时) 15 分钟 TTL
重新认证频率 频繁,用户体验差 稀少 适中
陈旧风险 可控
401 处理成本 每次都要完整流程 可能错过 token 刷新 合理折中

这类参数看着不起眼,实际很能说明接入层的思路:MCP 连接不是"连上就完了",而是一个会持续经历鉴权与重连摩擦的系统。


4. 统一入口:connectToServer() 总闸口

文件位置services/mcp/client.ts:595-607

typescript 复制代码
595:export const connectToServer = memoize(
596:  async (
597:    name: string,
598:    serverRef: ScopedMcpServerConfig,
599:    serverStats?: {
600:      totalServers: number
601:      stdioCount: number
602:      sseCount: number
603:      httpCount: number
604:      sseIdeCount: number
605:      wsIdeCount: number
606:    },
607:  ): Promise<MCPServerConnection> => {

整个入口使用了 memoize 装饰器,这里已经告诉我们两件事:

  1. 所有协议连接都要从这一个入口进
  2. 同一个 server 配置不会随便重复建连接

这就让 connectToServer() 变成了 MCP 世界的总闸口。你要支持再多协议,最终都得在这里过一遍。

Memoize 的工程价值主要为:

优势 说明
避免重复连接 相同配置的 server 只建立一次连接
资源节约 减少不必要的网络握手和认证流程
状态一致性 确保同一 server 在系统中只有一个活跃连接
性能优化 后续调用直接返回缓存的连接对象

5. 第一类传输:SSE ------ 长连接的特殊处理

文件位置services/mcp/client.ts:619-677

typescript 复制代码
619:      if (serverRef.type === 'sse') {
620:        const authProvider = new ClaudeAuthProvider(name, serverRef)
624:        const combinedHeaders = await getMcpServerHeaders(name, serverRef)
627:        const transportOptions: SSEClientTransportOptions = {
628:          authProvider,
632:          fetch: wrapFetchWithTimeout(
633:            wrapFetchWithStepUpDetection(createFetchWithInit(), authProvider),
634:          ),
635:          requestInit: {
636:            headers: {
637:              'User-Agent': getMCPUserAgent(),
638:              ...combinedHeaders,
639:            },
640:          },
641:        }
643:        // IMPORTANT: Always set eventSourceInit with a fetch that does NOT use the timeout wrapper.
648:        transportOptions.eventSourceInit = {
649:          fetch: async (url: string | URL, init?: RequestInit) => {
667:                Accept: 'text/event-stream',
668:              },
669:            })
670:          },
671:        }
673:        transport = new SSEClientTransport(
674:          new URL(serverRef.url),
675:          transportOptions,
676:        )

主要针对SSE主要做了长连接与普通请求的超时(timeout选项)区分。


6. 第二类传输:WebSocket ------ 脱敏日志与运行时适配

6.1 Authorization Header 脱敏

文件位置services/mcp/client.ts:735-783

typescript 复制代码
735:      } else if (serverRef.type === 'ws') {
741:        const combinedHeaders = await getMcpServerHeaders(name, serverRef)
743:        const tlsOptions = getWebSocketTLSOptions()
744:        const wsHeaders = {
745:          'User-Agent': getMCPUserAgent(),
746:          ...(sessionIngressToken && {
747:            Authorization: `Bearer ${sessionIngressToken}`,
748:          }),
749:          ...combinedHeaders,
750:        }
752:        const wsHeadersForLogging = mapValues(wsHeaders, (value, key) =>
753:          key.toLowerCase() === 'authorization' ? '[REDACTED]' : value,
754:        )
766:        let wsClient: WsClientLike
767:        if (typeof Bun !== 'undefined') {
770:          wsClient = new globalThis.WebSocket(serverRef.url, {
773:            proxy: getWebSocketProxyUrl(serverRef.url),
774:            tls: tlsOptions || undefined,
775:          } as unknown as string[])
776:        } else {
777:          wsClient = await createNodeWsClient(serverRef.url, {
778:            headers: wsHeaders,
779:            agent: getWebSocketProxyAgent(serverRef.url),
780:            ...(tlsOptions || {}),
781:          })
782:        }
783:        transport = new WebSocketTransport(wsClient)

注意 wsHeadersForLogging 这一步。作者不是简单打印连接参数,而是先把 authorization 脱敏再写日志。

脱敏逻辑

typescript 复制代码
key.toLowerCase() === 'authorization' ? '[REDACTED]' : value

6.2 Bun vs Node.js 运行时适配

代码位置client.ts:767-781

typescript 复制代码
767:        if (typeof Bun !== 'undefined') {
770:          wsClient = new globalThis.WebSocket(serverRef.url, {...})
776:        } else {
777:          wsClient = await createNodeWsClient(serverRef.url, {...})
782:        }

设计意图:根据运行时环境选择不同的 WebSocket 客户端实现。

运行时 客户端 特点
Bun globalThis.WebSocket 内置支持,无需额外依赖
Node.js createNodeWsClient 需要 ws 库,支持更多配置项

这种适配确保了 Claude Code 可以在不同 JavaScript 运行时中运行。


7. 第三类传输:HTTP ------ OAuth 与 Session Ingress 的协调

文件位置services/mcp/client.ts:784-865

typescript 复制代码
784:      } else if (serverRef.type === 'http') {
801:        const authProvider = new ClaudeAuthProvider(name, serverRef)
805:        const combinedHeaders = await getMcpServerHeaders(name, serverRef)
812:        const hasOAuthTokens = !!(await authProvider.tokens())
821:        const transportOptions: StreamableHTTPClientTransportOptions = {
822:          authProvider,
826:          fetch: wrapFetchWithTimeout(
827:            wrapFetchWithStepUpDetection(createFetchWithInit(), authProvider),
828:          ),
829:          requestInit: {
830:            ...proxyOptions,
831:            headers: {
832:              'User-Agent': getMCPUserAgent(),
833:              ...(sessionIngressToken &&
834:                !hasOAuthTokens && {
835:                  Authorization: `Bearer ${sessionIngressToken}`,
836:                }),
837:              ...combinedHeaders,
838:            },
839:          },
840:        }
861:        transport = new StreamableHTTPClientTransport(
862:          new URL(serverRef.url),
863:          transportOptions,
864:        )

HTTP请求使用了StreamableHTTP。另外还处理了认证冲突的问题,保证HTTP请求时认证来源优先级。如果这个 server 自己有 OAuth token,那就别再用 session ingress token 抢着塞 Authorization。只有在没有 OAuth tokens 的情况下,才使用 session ingress token。


8. 第四类传输:Claude.ai Proxy ------ 身份代理

文件位置services/mcp/client.ts:868-904

typescript 复制代码
868:      } else if (serverRef.type === 'claudeai-proxy') {
874:        const tokens = getClaudeAIOAuthTokens()
875:        if (!tokens) {
876:          throw new Error('No claude.ai OAuth token found')
877:        }
879:        const oauthConfig = getOauthConfig()
880:        const proxyUrl = `${oauthConfig.MCP_PROXY_URL}${oauthConfig.MCP_PROXY_PATH.replace('{server_id}', serverRef.id)}`
885:        const fetchWithAuth = createClaudeAiProxyFetch(globalThis.fetch)
888:        const transportOptions: StreamableHTTPClientTransportOptions = {
890:          fetch: wrapFetchWithTimeout(fetchWithAuth),
891:          requestInit: {
892:            ...proxyOptions,
893:            headers: {
894:              'User-Agent': getMCPUserAgent(),
895:              'X-Mcp-Client-Session-Id': getSessionId(),
896:            },
897:          },
898:        }
900:        transport = new StreamableHTTPClientTransport(
901:          new URL(proxyUrl),
902:          transportOptions,
903:        )

针对Claude.ai Proxy并没有重新造一个"代理专用 transport",而是继续复用 StreamableHTTPClientTransport,只是在认证和 URL 生成上换了入口。

差异化处理

维度 普通 HTTP Claude.ai Proxy
Transport StreamableHTTPClientTransport StreamableHTTPClientTransport(复用)
认证方式 OAuth / Session Ingress Claude.ai OAuth Tokens
URL 生成 直接使用 serverRef.url 拼接 proxy URL + server_id
特殊 Header - X-Mcp-Client-Session-Id

9. 第五类传输:Stdio ------ 子进程管理与 In-Process 优化

9.1 重型 Server 的 In-Process 特判

文件位置services/mcp/client.ts:905-959

typescript 复制代码
905:      } else if (
906:        (serverRef.type === 'stdio' || !serverRef.type) &&
907:        isClaudeInChromeMCPServer(name)
908:      ) {
909:        // Run the Chrome MCP server in-process to avoid spawning a ~325 MB subprocess
...
923:        transport = clientTransport
924:        logMCPDebug(name, `In-process Chrome MCP server started`)
925:      } else if (
926:        feature('CHICAGO_MCP') &&
927:        (serverRef.type === 'stdio' || !serverRef.type) &&
928:        isComputerUseMCPServer!(name)
929:      ) {
930:        // Run the Computer Use MCP server in-process
...
942:        transport = clientTransport
943:        logMCPDebug(name, `In-process Computer Use MCP server started`)
944:      } else if (serverRef.type === 'stdio' || !serverRef.type) {
945:        const finalCommand =
946:          process.env.CLAUDE_CODE_SHELL_PREFIX || serverRef.command
947:        const finalArgs = process.env.CLAUDE_CODE_SHELL_PREFIX
948:          ? [[serverRef.command, ...serverRef.args].join(' ')]
949:          : serverRef.args
950:        transport = new StdioClientTransport({
951:          command: finalCommand,
952:          args: finalArgs,
953:          env: {
954:            ...subprocessEnv(),
955:            ...serverRef.env,
956:          },
957:          stderr: 'pipe',
958:        })

性能分流点

stdio 分支理论上最朴素,就是起一个子进程、接标准输入输出。但源码在这里又塞了两个特判:

Server 类型 优化策略 原因
Chrome MCP Server In-process 运行 避免启动 ~325 MB 的重型子进程
Computer Use MCP Server In-process 运行 降低资源开销
普通 Stdio Server 子进程运行 标准处理方式

为什么?注释写得很直白:为了避免起一个很重的子进程。

这说明 Claude Code 接 MCP 时不只是"功能能连上就行",它已经开始按成本给不同 server 做优化分流。换句话说,stdio 不是一个简单协议分支,它还是一个性能分流点。

9.2 Stderr 监控与内存保护

文件位置client.ts:963-974

typescript 复制代码
963:      // Set up stderr logging for stdio transport before connecting
966:      let stderrHandler: ((data: Buffer) => void) | undefined
967:      let stderrOutput = ''
968:      if (serverRef.type === 'stdio' || !serverRef.type) {
969:        const stdioTransport = transport as StdioClientTransport
970:        if (stdioTransport.stderr) {
971:          stderrHandler = (data: Buffer) => {
972:            // Cap stderr accumulation to prevent unbounded memory growth
973:            if (stderrOutput.length < 64 * 1024 * 1024) {
974:              try {

Claude Code在 stderr 日志累积都加了上限,避免 debug 日志自己把内存吃炸。这种内存保护机制也是一个架构成熟的体现,在实现时既要保证正常路径要通,同时也要保证故障路径也得可控。


10. 第六类传输:SDK ------ 明确外派

文件位置services/mcp/client.ts:866-867

typescript 复制代码
866:      } else if (serverRef.type === 'sdk') {
867:        throw new Error('SDK servers should be handled in print.ts')

关键观察点 :这个分支很短,sdk 类型不归这层处理。该在哪处理,就去哪里处理。这也体现了统一是为了收口,不是为了把所有东西硬塞进一层。


11 完整连接流程总结

如果只保留骨架,它其实像这样:

text 复制代码
connectToServer(name, serverRef)
  → 看 serverRef.type
    → sse:认证 + 长连接专门处理(client.ts:619-677)
      → EventSource 不使用超时包装
    → ws:headers / proxy / tls / 脱敏日志(client.ts:735-783)
      → Authorization header 脱敏
      → Bun vs Node 运行时适配
    → http:OAuth 与 ingress token 协调(client.ts:784-865)
      → hasOAuthTokens 检查决定认证优先级
    → claudeai-proxy:复用 HTTP transport,改 URL 和身份(client.ts:868-904)
      → Claude.ai OAuth tokens
      → X-Mcp-Client-Session-Id header
    → stdio:默认子进程传输(client.ts:905-959)
      → 特判:Chrome MCP in-process
      → 特判:Computer Use MCP in-process
      → Stderr 监控与 64MB 上限
    → sdk:明确外派到别处处理(client.ts:866-867)
  → 建立 transport
  → 继续连接、记录日志、处理失败与清理

你看,统一入口的价值这时候就很明显了:

  • 所有连接语义都从这里收口
  • 每种协议的特殊处理也都写在自己那一枝上
  • 上层不用知道每个 server 该怎么握手
  • 下层也没有被强迫抽象成失真的同构代码

12. 假设实验:修改影响评估

实验一:把 SSE 的 eventSourceInit.fetch 也套上普通 timeout

修改位置client.ts:648-670,移除特殊的 fetch 配置

影响分析

维度 影响
长连接稳定性 会被周期性误杀
错误表象 "网络不稳定"
根本原因 接入层自己在定时切断流
排查难度 非常折磨人,难以定位

结论:长连接会被周期性误杀。最恶心的是,这类 bug 表面像"网络不稳定",实际上是接入层自己在定时切断流,排查起来非常折磨人。


实验二:HTTP 分支不区分 hasOAuthTokens

修改位置client.ts:833-836,移除 !hasOAuthTokens 条件

影响分析

场景 后果
OAuth + Session Ingress 共存 Session ingress token 可能覆盖 OAuth token
鉴权语义 变得不稳定:有时走对,有时走错
UI 表现 很难从 UI 直接看出来
调试难度 间歇性认证失败,难以复现

结论:那 session ingress token 和 OAuth token 就可能互相覆盖。连是能发起,但鉴权语义会变得不稳定:有时走对,有时走错,还很难从 UI 直接看出来。


实验三:一律用 stdio 起重型 MCP server,不做 in-process 特判

修改方案 :移除 client.ts:907-943 的两个特判分支

影响分析

维度 影响
功能正确性 未必坏
资源成本 肉眼上涨
Chrome MCP Server 每次连接启动 ~325 MB 子进程
系统负载 多个重型 server 同时运行时内存压力巨大
启动速度 变慢

结论:功能未必坏,但资源成本会肉眼上涨。尤其一些体积大的 server,会把"接一个工具"变成"顺手拉起一个很重的子进程"。


13. 设计原则总结

基于以上分析,提炼出以下可复用的设计原则:

原则一:统一入口,保留差异

  • 所有协议从 connectToServer() 进入
  • 每种协议的特殊需求在各自分支处理
  • 不因统一而抹杀关键差异

原则二:认证优先级明确

  • OAuth tokens 优先于 session ingress
  • 认证缓存设置合理 TTL(15分钟)
  • 避免认证头相互覆盖

原则三:长连接特殊对待

  • EventSource 不使用普通超时包装
  • WebSocket 区分 Bun 和 Node 运行时
  • 承认不同传输的本质差异

原则四:性能意识内建

  • 重型 server in-process 优化
  • Stderr 日志 64MB 上限保护
  • Memoize 避免重复连接

原则五:边界清晰

  • SDK 类型明确外派
  • 每层只处理自己的职责
  • 不强行抽象失真

14. 结论

Claude Code 的 MCP 接入层通过统一入口 + 差异化分支的架构,成功解决了协议多样性、认证复杂性和长期维护性三大挑战。其核心设计哲学是:

  1. 统一入口 :所有协议从 connectToServer() 进入
  2. 保留差异:每种传输的关键特性不被抹平
  3. 认证协调:OAuth 与 session ingress 优先级明确
  4. 性能优化:重型 server in-process 运行
  5. 边界清晰:不属于本层的明确外派

这套设计不仅适用于 MCP 协议接入,也为其他多协议系统集成提供了参考范式。

相关推荐
深海鱼在掘金2 小时前
从Claude Code泄露源码看工程架构:第七章 —— 多 Agent 协作机制与上下文隔离策略
人工智能·设计模式·架构
盟接之桥2 小时前
打破全球供应链“黑盒”:盟接之桥®如何用标准化EDI重塑中国制造的数据主权与交付底气
大数据·网络·人工智能·汽车·制造
深海鱼在掘金2 小时前
从Claude Code泄露源码看工程架构:第三章 — CLI 启动链路的分流策略与按需加载机制
前端·人工智能·设计模式
颜酱2 小时前
提示词强化1:三个让大模型更「听话」的习惯
前端·javascript·人工智能
jedi-knight2 小时前
Qwen3.5-27B 64K-Tools:一个面向本地部署的改进版大模型
大数据·数据库·人工智能
萤丰信息2 小时前
AI + 物联网在智慧园区的深度应用:落地场景 + 技术要点
人工智能·物联网
颜酱2 小时前
提示词强化 2:元提示(Meta-Prompt)与动态提示词
前端·javascript·人工智能
深海鱼在掘金2 小时前
从 Claude Code 泄露源码看工程架构:第五章 —— 工具框架的三层装配线
人工智能·设计模式·架构
刘~浪地球2 小时前
零信任架构设计与实现
安全·架构·安全架构