s-blog

深扒 Claude Code 源码(三):API 客户端与 MCP 集成

四 provider 分流 / beta header sticky latch / 七种 MCP 传输与 OAuth-XAA

ssssmy · 2026-06-11 · 10 min · LLM

本文是《深扒 Claude Code 源码》系列第三篇,聚焦与推理后端和外部工具的通信层。承接第二篇·工具与安全。本篇两章原文极密集,已适度浓缩极细节、保留全部小节与核心设计;完整 file:line 见原报告。

6. API 客户端与请求构造

本章剖析 Claude Code 与 Anthropic 推理后端之间的核心通信层。代码主要位于 client.ts(390 行)、claude.ts(3419 行)与 withRetry.ts(822 行)。

6.1 客户端工厂:四 provider 分流与认证头注入

getAnthropicClientclient.ts:88-316)是所有出站请求的唯一入口,返回被统一断言为 Anthropic 类型的 SDK 实例(注释 client.ts:188 坦言"我们一直在对返回类型撒谎"——Bedrock/Vertex/Foundry SDK 并不真支持 batching/models,但调用方按 Anthropic 接口使用)。

认证的关键分叉在 isClaudeAISubscriber():先刷新 OAuth token;若不是 Claude.ai 订阅用户,才注入 Authorization: Bearer <token>。订阅用户走 OAuth,不注入 Bearer。

四个 provider 由环境变量互斥决定,优先级 Bedrock > Vertex > Foundry > firstParty:

  • firstParty(默认):订阅用户 apiKey: null + authToken: OAuth accessToken;非订阅用户用 API key。
  • Bedrock:动态 import bedrock-sdk。AWS_BEARER_TOKEN_BEDROCK 存在时 skipAuth: true,否则取 AWS 凭据。
  • Foundry(Azure):无 API key 时用 DefaultAzureCredential 构造 azureADTokenProvider
  • Vertex:一段长注释说明只在用户设置 GCP 发现途径时才把 ANTHROPIC_VERTEX_PROJECT_ID 作兜底——目的是规避 GCE metadata server 在非 GCP 环境下的 12 秒超时。

buildFetch 仅在 firstParty 且 baseUrl 命中白名单时注入 x-client-request-id——3P provider 不记录该头,严格代理可能拒绝(inc-4029)。该 ID 用于超时(无 server request ID)时与服务端日志关联。

6.2 请求构造主循环:queryModel 与 .withResponse()

实际发起请求anthropic.beta.messages.create({...params, stream:true}).withResponse()。注释解释为何用原生 Stream 而非 SDK 的 BetaMessageStream:后者在每个 input_json_delta 上调用 partialParse() 造成 O(n²) 解析,而 CC 自己累积 tool input。

normalizeModelStringForAPI 极简:model.replace(/\[(1|2)m\]/gi, ''),剥离用户可见的 [1m]/[2m] 上下文窗口标记后再上线。

getMaxOutputTokensForModel:若 tengu_otk_slot_v1 gate 开启则把默认压到 8k(注释给出 BQ p99=4911 token 的依据,避免 slot 超额预留 8-16 倍)。

6.3 beta header 体系

beta 常量集中在 betas.ts,每个都带日期版本(interleaved-thinking-2025-05-14context-1m-2025-08-07effort-2025-11-24fast-mode-2026-02-01 等)。设计要点:provider 差异化(tool search header 1P 用 advanced-tool-use-2025-11-20、Vertex/Bedrock 用 tool-search-tool-2025-10-19);构建期门控(部分 header 用 feature() 在外部构建中 tree-shake 成空串);Bedrock 特例(部分 beta 只能经 extraBodyParams.anthropic_beta 传)。

Sticky latch(粘滞闩锁)是护 prompt cache 的关键设计。动态 beta header(afk-mode、fast-mode、cache-editing、thinking-clear)一旦首次发送,就经 bootstrap state 锁定,整个会话保持发送,避免会话中途 toggle 改变服务端 cache key 而冲掉 50-70K token。闩锁仅在 /clear/compact 时清空。fast-mode 的精妙之处:header 闩锁住保证 cache 安全,但 speed='fast' 字段保持动态,cooldown 期间仍能抑制真实 fast-mode 请求而不改 cache key。

6.4 thinking 模式:adaptive vs enabled,effort,与 temperature 门控

  • adaptive 路径:若 modelSupportsAdaptiveThinking(仅 opus-4-6/sonnet-4-6 及未知新模型默认 true)则发 thinking: {type:'adaptive'}——不带 budget,由模型自适应。注释三次强调改动需通知 model launch DRI 和 research。
  • enabled 路径:取默认 budget,Math.min(maxOutputTokens-1, budget) 钳制,发 thinking: {budget_tokens, type:'enabled'}

effortconfigureEffortParams 处理:undefined 仅加 beta header 让 API 自决;字符串值写入 outputConfig.effort;数值型 override 仅 ant 用户。

temperature 门控仅当 thinking 关闭时才发 temperature;thinking 开启时 temperature = undefined,因为 API 要求 thinking 时 temperature 必须为 1。

6.5 withRetry:手动重试机

主路径传 maxRetries: 0禁用 SDK 自动重试,全部交由 withRetry 手控。它是个 AsyncGenerator,yield 系统错误消息、return 最终结果,默认 maxRetries=10

  • 429/529 与 fast-mode 冷却:fast mode 遇 429/529——overage-disabled 则永久关 fast mode;retry-after < 20s 则短睡后保持 fast mode 重试以护 cache;否则进入 cooldown(floor 10 分钟)切回标准速。
  • 529 的差异化策略:非前台 querySource 遇 529 立即退出——容量级联时每次重试对网关是 3-10× 放大,且用户看不见这些失败(摘要/标题/分类器);前台源累计达 MAX_529_RETRIES=3 时抛 FallbackTriggeredError(由 query.ts 切模型)。
  • 退避:优先 retry-after 头,否则 500ms × 2^(attempt-1) 上限 32s + 25% jitter。
  • unattended 持久重试(ant-only):429/529 无限重试,退避上限 5 分钟。窗口型限流读 anthropic-ratelimit-unified-reset 头直接等到重置时刻而非空转轮询。长睡眠按 30s 分块,每块 yield keep-alive 防止宿主把会话标记为 idle。

6.6 流式消费:空闲看门狗、stall 检测与原生内存释放

原生内存释放是反复强调的重点。Response 对象持有 V8 堆外的 TLS/socket 缓冲(GH #32920),必须无论生成器如何退出都释放——它被放在 finally、各错误分支、正常完成处多次幂等调用。

空闲看门狗:每收一个 chunk 就重置计时器,超 90s 无 chunk 则主动杀流。这弥补 SDK 请求超时只覆盖初始 fetch、不覆盖 streaming body 的盲区——静默断连否则会无限挂起。

事件累积message_delta 携带最终 usage/stop_reason,直接属性 mutation 回写到已 yield 的 last message(注释解释 transcript 写队列持有 message.message 引用并惰性序列化,对象替换会断开引用)。

非流式回退:流式失败时进入,max_tokens 钳至 MAX_NON_STREAMING_TOKENS=64000FallbackTriggeredError 必须穿透所有 catch 上抛 query.ts 执行真正的模型切换。CLAUDE_CODE_DISABLE_NONSTREAMING_FALLBACK 可禁用回退,因 mid-stream 回退会致工具双执行(inc-4258)。


7. MCP 集成

MCP 子系统以 client.ts(3348 行)为执行核心、auth.ts(2465 行)为 OAuth 核心、config.ts 为配置合并核心。整体架构是"配置七层合并 → 按本地/远程分组并发连接 → 拉取工具并前缀化 → 运行期按需重连/重授权"。

7.1 connectToServer:七种传输的统一连接器

连接入口被 memoize 包裹,缓存键由 ${name}-${jsonStringify(serverRef)} 生成——同名但配置不同的 server 落在不同缓存槽,配置变更天然导致 cache miss。源码作者在 client.ts:589 留了 TODO 质疑这层 memoize “增加了大量复杂度且不确定真的提升性能”——诚实的设计权衡注记。

传输分派按 serverRef.type 落入七种 + 两种 in-process:

  • sse / http:构造 ClaudeAuthProvider,fetch 链是三层洋葱 wrapFetchWithTimeout(wrapFetchWithStepUpDetection(createFetchWithInit(), authProvider))。step-up 检测必须裹在最内层,这样 403 在 SDK 的处理器调用前就被看到。SSE 的 fetch 故意不套 timeout 包装,因为 EventSource 是长寿命流,60 秒 timeout 会杀掉它。
  • sse-ide / ws-ide:IDE 内部传输,不走 OAuth。
  • ws:常规 WebSocket,Bun 与 Node 两条创建路径,日志前对 Authorization 头做 [REDACTED]
  • sdk:直接 throw——SDK MCP 由独立的 SdkControlTransport 在打印模式处理。
  • claudeai-proxy:用 createClaudeAiProxyFetch 包装,携带 claude.ai OAuth Bearer 并在 401 时单次重试。
  • in-process Chrome / Computer Use:不 spawn ~325MB 子进程,而是在本进程内用 createLinkedTransportPair() 连。InProcessTransportsend()queueMicrotask 投递避免同步栈深。

stdio stderr 64MB 上限:stderr handler 累加数据但硬性 cap 在 64MB,超过则静默丢弃,连接成功后立即清空释放内存。

连接超时竞速Promise.race([connectPromise, timeoutPromise]),默认 30000ms。capability 声明:声明 roots: {}elicitation: {},注释点明 elicitation 必须用空对象 {},否则会击穿 Java MCP SDK(Spring AI)。

并发分组:按 isLocalMcpServer 分 local(默认并发 3)/remote(默认并发 20),用 pMap 而非固定大小批——旧实现里 batch N 的一个慢 server 会卡住整个 batch N+1。

7.2 配置七层作用域合并

优先级合并靠 JS 对象 spread 的"后者覆盖前者"语义实现

  • enterprise 独占:若企业 MCP 配置存在,直接返回仅 enterprise 的过滤结果,其余所有层全部被忽略——企业客户通常不希望用户自带 MCP。
  • plugin < user < project < localObject.assign({}, dedupedPluginServers, userServers, approvedProjectServers, localServers),后写入者胜。project server 需用户批准才参与合并。
  • claudeai 最低:在更外层 Object.assign({}, dedupedClaudeAi, claudeCodeServers)

因此生效优先级从高到低是 enterprise(独占) > local > project > user > dynamic > claudeai > plugin

去重是精细点:plugin server key 被命名空间化,永不与 manual key 碰撞,所以必须做基于 URL 签名的内容去重。规则是"manual 胜 plugin,plugin 间先加载者胜",且只有真正会连接的 server 才是有效去重目标——一个被禁用的 manual server 不能压制 plugin server。

7.3 MCP OAuth:ClaudeAuthProvider + CIMD + 步进重授权

ClaudeAuthProvider 实现 SDK 的 OAuthClientProvider。核心设计:

  • public clienttoken_endpoint_auth_method = 'none'
  • CIMD / SEP-991 URL-based client_id:当 AS 广告 client_id_metadata_document_supported: true 时,SDK 直接拿 metadata URL 当 client_id,绕过 Dynamic Client Registration
  • token 读取的性能权衡tokens() 故意不调 clearKeychainCache()——SDK 的 _commonHeaders 每请求都调 tokens()(30-40 次/秒),强制 cache miss 会触发 spawnSync(security find-generic-password),曾占 7.2% CPU。
  • discovery 持久化的 keychain 限制saveDiscoveryState 只存 URL 不存完整 metadata blob,因为 macOS keychain 经 security -i 有 4096 字节行限制,两个 OAuth MCP server 存全量 metadata 就溢出、损坏凭据库(issue #30337)。

403 insufficient_scope 步进重授权是这块最精巧的逻辑。问题(github #28258):SDK 见到有 refresh_token 就去刷新,但 RFC 6749 §6 禁止用 refresh 提升 scope,于是刷出同 scope token → 又 403 → SDK 放弃,永远到不了持久化 step-up scope 的地方。解决方案 wrapFetchWithStepUpDetection:在 fetch 层先看 403 + WWW-Authenticate: insufficient_scope,抽出请求的 scope 调 markStepUpPending(scope)。这个标志使 tokens() 在检测到当前 token 缺该 scope 时省略 refresh_token,逼 SDK 跳过无用刷新、落入 PKCE 流。

重授权失败处理:连接期失败把 server 写入 mcp-needs-auth-cache.json(15 分钟 TTL),下次启动跳过、代之以 McpAuthTool 伪工具——避免每 15 分钟无谓重探。

7.4 XAA(Cross-App Access)无浏览器令牌交换

XAA(SEP-990)为企业场景提供零浏览器交互的 MCP 令牌获取,链路是四步:

  1. RFC 9728 PRM 发现:拿 resource + authorization_servers,做 resource-mismatch 校验(mix-up 防护)。
  2. RFC 8414 AS metadata 发现:issuer-mismatch 校验 + 强制 token_endpoint 为 https——防恶意 PRM 把 id_token+client_secret 明文 POST 出去。
  3. RFC 8693 Token Exchange @ IdPid_token → ID-JAG
  4. RFC 7523 JWT Bearer @ ASID-JAG → access_token,默认 client_secret_basic,按 AS 支持动态切到 client_secret_post

错误语义很讲究:4xx/invalid_grant → id_token 坏了清掉;5xx → IdP 宕机、id_token 可能仍有效保留。日志用正则脱敏所有 token。

XAA 的 UX 价值在于一次 IdP 浏览器登录(按 issuer 缓存)被该 issuer 下所有 XAA server 复用,后续 server 命中缓存全程静默。设计上无静默回退:配了 XAA 就只走 XAA,不退化到 consent flow。作者诚实标注 TODO(xaa-ga):跨进程去重 lockfile 尚未补。

7.5 McpAuthTool 伪工具机制

createMcpAuthTool 为"已安装但未认证"的 server 生成一个模型可调用的伪工具 mcp__<server>__authenticate,顶替真实工具出现在列表里,让模型知道 server 存在并能代用户发起 OAuth。

call() 的关键设计是两个 promise 竞速Promise.race([authUrlPromise, oauthPromise.then(()=>null)])——要么拿到授权 URL 返回给模型转交用户,要么 flow 直接静默完成(如 XAA 命中缓存)。完成后把真实工具以前缀替换方式注入 appState:reject(prev.mcp.tools, t => t.name?.startsWith(prefix)) 抹掉一切 mcp__<server>__*(包括伪工具自己),再拼上 result——伪工具因共享前缀被自动移除,无需特判。

7.6 MCP 工具名脱敏遥测

由于 mcp__<server>__<tool> 会暴露用户特定的 server 配置(PII-medium),基线规则把任何 mcp__ 前缀的工具名一律替换为常量 'mcp_tool'只在三种情况下才放行真实名:Cowork 场景、claudeai-proxy 传输、或 server URL 命中官方 MCP 注册表(fail-closed:注册表未加载即返回 false)。这把"通过 claude mcp add 加的目录连接器"与"用户自定义私有 MCP"区分开。工具描述统一截到 MAX_MCP_DESCRIPTION_LENGTH = 2048——针对 OpenAPI 生成的 MCP server 往 description 里塞 15-60KB 端点文档的 p95 长尾。


下一篇(四)进入配置与运行时:settings 五源合并与权限引擎、认证与秘钥存储。

原文链接:https://www.ssssmy.com/notes/claude-code-deep-dive-api-mcp