Skip to content

8.1 为什么要流式?——从用户体验到系统架构

想象一下:你向Claude Code提交了一个复杂的代码重构任务。模型需要思考30秒,然后一次性返回完整的回复。

在这30秒里,你的终端什么都没有显示。你不知道它在做什么——是在思考?还是在死锁?还是网络断了?

这个问题的答案,就是**流式处理(Streaming)**存在的原因。

在传统的请求-响应模型中,客户端发送请求,等待服务端处理完毕,然后一次性接收完整响应。这种模式在处理简单查询时没有问题,但当响应时间超过一两秒时,用户体验就会急剧下降。

Claude Code采用了**Server-Sent Events (SSE)**协议来实现流式对话。这不是一个简单的工程选择——它涉及从网络层到应用层的完整技术栈设计,并且深刻影响了整个系统的架构。

SSE vs WebSocket:为什么不是双向通信?

在设计流式协议时,第一个要回答的问题是:为什么不使用WebSocket?

WebSocket提供了全双工通信能力——客户端和服务端可以同时向对方发送消息。相比之下,SSE是单向的:服务端向客户端推送数据流。

Claude Code选择了SSE,原因是:

  1. 请求-响应的本质:Claude Code的对话模式本质上是"用户提问→模型回答→用户再提问"的循环。即使有多轮对话,每一轮都是用户发起的。这种模式下,全双工通信是冗余的。

  2. 基础设施兼容性:SSE基于标准HTTP协议,天然支持代理、负载均衡、SSL终止等中间件。WebSocket需要特殊的基础设施支持。

  3. 重连机制:SSE天生支持自动重连(通过Last-Event-ID头)。对于长时间运行的代码生成任务,网络抖动是常态,自动重连是必需的。

  4. 语义简洁性:SSE的事件模型(event/data/id)完美映射了Anthropic Messages API的流式响应结构——每个content_block_delta都是一个事件。

这不是说WebSocket没有优势。如果你要实现一个实时协作编辑器(像VS Code的Live Share),WebSocket的双向能力是必需的。但对于AI对话这种"请求-流式响应"模式,SSE是更合适的选择。

8.2 MessageStream:流式消息处理的核心类

在Claude Code的源码中,SSE流式消息处理的核心是MessageStream类。这个类封装了与Anthropic Messages API的流式通信,它是@anthropic-ai/sdk v0.74.0内联到bundle中的关键组件。

8.2.1 RT类:SSE响应的异步迭代器

SSE响应的数据在网络上是一个连续的字节流。在JavaScript中,处理这种流需要两个关键抽象:

  1. fromSSEResponse:将Fetch API的Response对象转换为SSE事件流
  2. fromReadableStream:将Node.js的ReadableStream转换为SSE事件流

这就是RT类的作用——它是一个异步迭代器(Async Iterator),将原始的字节流转换为结构化的SSE事件序列。

javascript
// RT类的核心设计(伪代码表示)
class RT {
  constructor(response) {
    this.response = response;
    this.buffer = "";
    this.eventType = null;
    this.data = null;
    this.lastEventId = null;
  }

  async *[Symbol.asyncIterator]() {
    const reader = this.response.body.getReader();
    const decoder = new TextDecoder();

    while (true) {
      const { done, value } = await reader.read();
      if (done) break;

      this.buffer += decoder.decode(value, { stream: true });
      yield* this.parseEvents();
    }
  }
}

这个设计的关键在于增量解析:不需要等待整个响应完成,每收到一块数据就尝试解析出完整的SSE事件。这与传统的一次性JSON解析有本质区别——我们面对的是一个永远不会"完整"的数据流。

8.2.2 YH7类:SSE事件解码器

SSE协议的格式极其简洁:

event: message_start
data: {"type":"message_start","message":{"id":"msg_xxx",...}}

event: content_block_start
data: {"type":"content_block_start","index":0,"content_block":{"type":"text","text":""}}

event: content_block_delta
data: {"type":"content_block_delta","index":0,"delta":{"type":"text_delta","text":"Hello"}}

event: content_block_stop
data: {"type":"content_block_stop","index":0}

event: message_delta
data: {"type":"message_delta","delta":{"stop_reason":"end_turn"},"usage":{"output_tokens":25}}

event: message_stop
data: {"type":"message_stop"}

YH7类负责解析这个格式。它的核心职责是:

  1. 事件类型识别:识别event:行,设置当前事件类型
  2. 数据提取:提取data:行中的JSON负载
  3. 事件边界检测:通过空行(双换行符)检测事件结束
  4. JSON反序列化:将data:行的内容解析为JSON对象
javascript
// YH7类的设计模式
class YH7 {
  parse(line) {
    if (line.startsWith("event:")) {
      this.currentEvent = line.slice(6).trim();
    } else if (line.startsWith("data:")) {
      this.currentData = line.slice(5).trim();
    } else if (line === "") {
      // 空行表示一个事件的结束
      if (this.currentData) {
        yield {
          event: this.currentEvent,
          data: JSON.parse(this.currentData)
        };
      }
      this.currentEvent = null;
      this.currentData = null;
    }
  }
}

注意一个细节:SSE协议中,一个事件的data字段可以跨多行。每一行data:都以换行符拼接。YH7类需要处理这种情况。

8.2.3 Bt类:行级缓冲解码器

网络数据是以任意大小的块(chunk)到达的。一个chunk可能包含:

  • 完整的一行
  • 一行的一部分
  • 多个完整的行
  • 多个完整的行 + 一行的一部分

Bt类(配合_H7函数)是一个行级缓冲解码器,负责将任意大小的数据块转换为完整的行。它的核心算法是双换行符检测

javascript
// _H7函数:双换行符检测
function _H7(buffer) {
  const events = [];
  let boundary;
  while ((boundary = buffer.indexOf("\n\n")) !== -1) {
    events.push(buffer.slice(0, boundary));
    buffer = buffer.slice(boundary + 2);
  }
  return { events, remainder: buffer };
}

这个函数的设计体现了流式处理中的一个核心原则:永远不要假设数据会整齐地到达。网络是不可靠的,TCP的Nagle算法、代理的缓冲策略、操作系统的网络栈都可能改变数据到达的模式。

8.2.4 完整的流式处理管线

将上述组件串联起来,Claude Code的流式处理管线如下:

HTTP Response (ReadableStream)


RT.fromSSEResponse()  ──── 将Response转为异步迭代器


Bt (行缓冲器)  ──── 将字节块转为完整行


YH7 (SSE解码器)  ──── 将SSE格式转为结构化事件


_H7 (事件边界检测)  ──── 按双换行符切分事件


MessageStream  ──── 类型化事件分发

    ├─→ message_start    → 初始化消息状态
    ├─→ content_block_start → 开始新的内容块
    ├─→ content_block_delta → 增量内容(文本/工具调用)
    ├─→ content_block_stop  → 内容块结束
    ├─→ message_delta    → 消息级元信息
    └─→ message_stop     → 消息完成

这个管线的设计遵循了Unix管道哲学——每个组件只做一件事,通过组合构建复杂功能。这种设计使得任何一个环节都可以被替换或增强,而不影响其他部分。

8.3 增量JSON解析:流式世界里的数据挑战

流式处理带来的最大挑战之一是:JSON通常不是流式友好的

考虑这样一个场景:Claude Code调用了一个工具(比如Bash),工具的输出是一个大型JSON文件。在流式模式下,这个JSON文件的返回也是增量的——模型一个token一个token地吐出来。

你可能会想:等模型生成完毕后再解析JSON不就行了吗?但问题在于,Claude Code需要在模型还在生成的同时就显示部分结果。用户不应该看到一个空白的屏幕等30秒。

8.3.1 input_json_delta:工具输入的增量构建

当Claude Code决定调用一个工具时,它会生成一个tool_use类型的content block。这个content block的input字段是一个JSON对象,它也是增量生成的。

event: content_block_start
data: {"type":"content_block_start","index":1,"content_block":{"type":"tool_use","id":"toolu_xxx","name":"Bash"}}

event: content_block_delta
data: {"type":"content_block_delta","index":1,"delta":{"type":"input_json_delta","partial_json":"{\"command\":\""}}

event: content_block_delta
data: {"type":"content_block_delta","index":1,"delta":{"type":"input_json_delta","partial_json":"npm install\"}"}}

event: content_block_delta
data: {"type":"content_block_delta","index":1,"delta":{"type":"input_json_delta","partial_json":"}"}}

每个input_json_delta事件都包含一个partial_json字段,它是当前增量片段的JSON片段。注意——这个片段可能不是合法的JSON。例如第一个片段{"command":"没有闭合的大括号和引号。

8.3.2 I38函数:实时JSON修复

这就是I38函数发挥作用的地方。它接收一个累积的partial_json字符串,尝试将其修复为一个合法的JSON对象:

javascript
// I38函数的核心逻辑(概念表示)
function I38(partialJson) {
  try {
    return JSON.parse(partialJson);
  } catch (e) {
    // JSON不完整,尝试修复
    return tryRepairJson(partialJson);
  }
}

function tryRepairJson(str) {
  // 策略1:补全未闭合的字符串
  // 策略2:补全未闭合的对象/数组
  // 策略3:移除尾部的不完整token
  // 策略4:返回null(确实无法修复)
}

这个函数的设计需要考虑多种边界情况:

  • {"command":"npm install" → 补全 "}{"command":"npm install"}
  • {"files":["a.ts","b.ts"," → 补全 ""]}{"files":["a.ts","b.ts",""]}
  • {"count":42 → 补全 }{"count":42}
  • {"nested":{"deep":{"val → 太不完整,返回null

这种实时JSON修复是一个看似简单实则复杂的问题。你需要在模型还在生成的同时,尽可能地为用户提供有意义的预览信息。

8.3.3 fH7函数:工具类型检测

Claude Code支持三种工具调用类型:

  1. tool_use:标准工具调用(Bash、FileRead、Grep等内置工具)
  2. server_tool_use:服务端工具(WebSearch、WebFetch——由Anthropic服务端执行)
  3. mcp_tool_use:MCP工具调用(通过MCP协议调用的外部工具)

fH7函数负责检测当前的工具调用类型:

javascript
function fH7(contentBlock) {
  switch (contentBlock.type) {
    case "tool_use":
      return { category: "builtin", ... };
    case "server_tool_use":
      return { category: "server", ... };
    case "mcp_tool_use":
      return { category: "mcp", ... };
  }
}

这种类型区分对于工具调度和权限管理至关重要——不同类型的工具有不同的执行路径和权限模型。

8.4 从异步迭代器到流:k38与Jx6

在Claude Code的内部架构中,SSE事件首先被解析为异步迭代器(AsyncIterator)。但很多子系统期望的是标准的Web Streams API(ReadableStream)。

8.4.1 k38函数:异步迭代器转ReadableStream

k38函数负责将异步迭代器转换为ReadableStream:

javascript
function k38(asyncIterator) {
  return new ReadableStream({
    async pull(controller) {
      const { value, done } = await asyncIterator.next();
      if (done) {
        controller.close();
      } else {
        controller.enqueue(value);
      }
    }
  });
}

这个适配器看似简单,但它解决了一个重要的架构问题:统一流抽象。通过将所有流统一为ReadableStream,下游的消费者不需要关心数据源是SSE、WebSocket、文件读取还是内存缓冲。

8.4.2 Jx6函数:流式协议适配

Jx6函数处理的是更复杂的协议适配问题。Claude Code内部有多种流式协议:

  1. SSE流:来自Anthropic API的原始SSE事件
  2. 工具结果流:工具执行产生的输出流
  3. 用户输入流:用户在终端中的实时输入

Jx6函数将这些不同的流协议适配为统一的内部消息格式:

SSE事件流 ────┐

工具结果流 ────┼──→ Jx6(协议适配)──→ 统一消息总线 ──→ UI渲染

用户输入流 ────┘

这种适配器模式使得整个系统在面对协议变化时具有更好的弹性。当Anthropic发布新的流式API版本时,只需要修改适配层,而不需要改动下游的所有消费者。

8.5 Token缓存与续写压缩:长对话的成本优化

流式处理解决了实时性的问题,但引入了新的挑战:长对话的token成本

Claude Code的一个核心使用场景是长编码会话——开发者可能和AI进行几十轮甚至上百轮的对话。每一轮对话,模型都需要接收完整的对话历史(system prompt + 历史消息 + 当前消息)。随着对话增长,input token的数量线性增长,成本也随之增长。

8.5.1 Prompt Caching:减少重复计算的token

Anthropic API提供了Prompt Caching功能。Claude Code利用了这个功能来优化长对话:

json
{
  "usage": {
    "cache_creation_input_tokens": 1024,
    "cache_read_input_tokens": 8192,
    "cache_creation": {
      "ephemeral_1h_input_tokens": 512,
      "ephemeral_5m_input_tokens": 512
    }
  }
}

这里有几个关键字段:

  • cache_creation_input_tokens:本次请求中写入缓存的token数量
  • cache_read_input_tokens:从缓存中读取的token数量(不重新计算)
  • ephemeral_1h_input_tokens:1小时有效的缓存token
  • ephemeral_5m_input_tokens:5分钟有效的缓存token

Prompt Caching的工作原理是:

  1. Claude Code将system prompt和早期对话历史标记为"可缓存"
  2. Anthropic API识别到这些前缀在多个请求间保持不变
  3. API将这部分计算的KV缓存持久化
  4. 后续请求只需读取缓存,跳过重复计算

从AgentOutput的类型定义可以看到,Claude Code在usage统计中明确区分了缓存创建和缓存读取的token数。这个数据不仅用于计费,也用于自适应缓存策略——动态决定哪些内容应该被标记为可缓存。

8.5.2 续写压缩模板:vH7的秘密

当Claude Code的会话被中断(用户关闭终端、网络断开等),后续可以通过"续写"功能恢复上下文。但直接发送完整的对话历史既昂贵又低效。

Claude Code使用vH7——一个结构化的续写压缩模板——来压缩对话历史:

vH7的压缩策略:
1. System Prompt → 保留完整
2. 工具定义 → 保留名称和schema,移除描述和示例
3. 早期对话 → 压缩为摘要
4. 近期对话 → 保留完整
5. 当前任务上下文 → 保留完整

这种分层压缩策略确保了:模型拥有足够的信息来理解"我们在做什么",同时不会因为过长的历史而浪费token或分散注意力。

8.5.3 自适应缓存策略

Claude Code的缓存策略不是静态的——它会根据对话模式动态调整:

  • 对话初期:大部分token是cache_creation(写入缓存),成本较高但为后续节省
  • 对话中期:大部分token是cache_read(读取缓存),成本显著降低
  • 工具调用密集期:工具定义占大量token,缓存命中率极高
  • 长对话后期:引入压缩策略,减少缓存大小

这种自适应策略的实现依赖于对每次API响应中usage字段的实时分析。Claude Code会在内部维护一个"缓存效率计数器",根据最近N次请求的缓存命中率来调整缓存策略。

8.6 流式处理的错误恢复与容错

网络不是可靠的。HTTP连接会超时、DNS会解析失败、代理会断开。在流式处理场景下,这些故障更加复杂——因为故障可能发生在消息传输的任意中间点。

8.6.1 断点续传:基于event ID的重连

SSE协议天然支持断点续传:

Last-Event-ID: msg_012

当连接断开时,Claude Code会在重连请求中携带Last-Event-ID头,服务端可以从上次中断的事件继续发送。

但在实践中,Claude Code面临的挑战更加复杂:

  1. Anthropic API可能不支持断点续传:并非所有SSE实现都支持Last-Event-ID
  2. 状态不一致:如果中断发生在content_block_delta之间,部分内容可能已经显示但未持久化
  3. 工具调用的原子性:如果中断发生在工具调用生成的过程中,工具可能已经被执行但结果未完整接收

8.6.2 消息完整性校验

为了处理这些边界情况,Claude Code在流式处理管线中加入了完整性校验:

javascript
// 消息完整性校验(概念表示)
function validateMessage(events) {
  // 检查message_start是否收到
  if (!events.find(e => e.type === "message_start")) {
    throw new Error("Incomplete message: missing message_start");
  }
  
  // 检查所有content_block是否成对出现
  const starts = events.filter(e => e.type === "content_block_start");
  const stops = events.filter(e => e.type === "content_block_stop");
  if (starts.length !== stops.length) {
    // 不完整的内容块——尝试恢复或标记
  }
  
  // 检查message_stop是否收到
  if (!events.find(e => e.type === "message_stop")) {
    // 消息未正常结束——标记为中断状态
  }
}

8.6.3 指数退避重连

对于临时性网络故障,Claude Code采用指数退避策略:

第1次重试:等待 1秒
第2次重试:等待 2秒
第3次重试:等待 4秒
第4次重试:等待 8秒
第5次重试:等待 16秒(最大)

同时加入随机抖动(jitter)避免雷群效应。

8.7 流式处理在多工具场景下的协调

Claude Code的一个关键能力是并行工具调用。模型可以在一次响应中同时调用多个工具。在流式模式下,这带来了额外的协调挑战。

8.7.1 内容块的交错处理

考虑这样一个场景:模型决定先写文件,再运行测试,最后读取测试结果。流式输出可能如下:

content_block_start [index=0, type=tool_use, name=FileWrite]
content_block_delta [index=0, partial_json={"file_path":"/src/main.ts",...}]
content_block_start [index=1, type=tool_use, name=Bash]
content_block_delta [index=1, partial_json={"command":"npm test"}]
content_block_stop  [index=0]  ← FileWrite的输入完成
content_block_stop  [index=1]  ← Bash的输入完成

注意content_block_startcontent_block_stop是按index交错出现的。Claude Code需要维护一个并行内容块状态表,跟踪每个index对应的工具调用的完整生命周期。

8.7.2 流式工具调度

当工具输入还在流式到达时,Claude Code不能执行工具——它必须等待输入完整。但一旦输入完成,它可以立即开始执行,而不需要等待所有工具输入都完成。

这种"流式调度"策略显著降低了端到端延迟:

时间线(传统方式):
|─────── 工具A输入 ───────|─────── 工具B输入 ───────|── 工具A执行 ──|── 工具B执行 ──|
                                        总延迟:4x

时间线(流式调度):
|──── 工具A输入 ────|-- 工具A执行 --|
    |──── 工具B输入 ────|-- 工具B执行 --|
                                        总延迟:2x

8.7.3 服务端工具(server_tool_use)的特殊处理

server_tool_use类型的工具(WebSearch、WebFetch)是由Anthropic服务端执行的,不需要Claude Code参与。但它们的执行结果仍然通过SSE流返回,需要纳入统一的流式处理管线。

这使得流式处理管线需要区分两种工具执行模式:

  • 本地执行:Claude Code执行工具 → 产生结果 → 发送给API → 继续生成
  • 服务端执行:API执行工具 → 通过SSE返回结果 → Claude Code处理结果

8.8 设计哲学:流式即默认

Claude Code的一个重要设计决策是:流式是默认的,非流式是例外

即使对于非常短的响应(比如"好的,我来修复"),Claude Code也使用流式传输。这似乎有悖常理——对于短响应,流式反而会增加延迟(HTTP连接建立的开销)。

但这个决策背后的逻辑是:

  1. 一致性:所有响应都走同一条路径,减少了分支逻辑
  2. 首token延迟:即使是短响应,用户也希望看到即时反馈(光标闪烁、第一个字的快速出现)
  3. 架构简洁性:不需要维护两套消息处理逻辑(流式/非流式)
  4. 可预测性:所有响应都遵循相同的时序模型,便于超时和错误处理

这是一个经典的工程权衡——牺牲了短响应场景下的理论最优性能,换取了整个系统的架构简洁性和行为一致性。

8.9 本章小结

Claude Code的流式对话系统是一个精心设计的工程实现,它涉及:

  • 协议选择:SSE而非WebSocket,基于对话模式的本质特征
  • 分层解析管线:RT(迭代器)→ Bt(行缓冲)→ YH7(SSE解码)→ _H7(事件切分)
  • 增量JSON处理:I38函数实现实时JSON修复,支持工具输入的流式构建
  • 类型化工具调度:fH7函数区分builtin/server/mcp工具
  • 流抽象统一:k38和Jx6函数实现异步迭代器到ReadableStream的适配
  • 成本优化:Prompt Caching + 续写压缩模板 + 自适应缓存策略
  • 错误恢复:断点续传、完整性校验、指数退避重连
  • 并行协调:交错内容块处理、流式工具调度

这个系统最精彩的地方不在于任何一个单独的技术点,而在于这些组件如何协同工作——每一个组件都接受上一个组件的输出,并产生下一个组件可以消费的格式,形成了一个优雅的处理管线。

在下一章中,我们将看到另一个更具雄心的协议设计——MCP(Model Context Protocol),它是Claude Code实现可扩展性的终极答案。


第9章:MCP协议 — 可扩展性的终极答案

9.1 一个不可能的需求

假设你正在使用Claude Code开发一个后端服务。你需要:

  1. 查询数据库中的用户数据
  2. 检查Redis缓存的状态
  3. 查阅公司内部的API文档
  4. 在Jira上创建一个bug
  5. 发送一封通知邮件

Claude Code内置了文件操作、命令执行、代码搜索等工具。但上面这些需求呢?

数据库?Claude Code没有内置数据库连接工具。Redis?Jira?邮件?都没有。

你当然可以让Claude Code通过Bash工具执行psqlredis-cli等命令。但这有几个问题:

  1. 安全性:直接暴露数据库连接串给模型
  2. 可靠性:命令行输出的解析依赖于各种边缘情况
  3. 标准化:每个外部系统的调用方式都不同
  4. 可重用性:每个项目都要重新配置

这不仅是Claude Code的问题,也是所有AI编程工具面临的根本挑战:模型的能力需要与外部世界连接,但不可能内置所有外部系统的集成

MCP(Model Context Protocol)就是Claude Code对这个问题的答案。

9.2 MCP协议概述:标准化外部能力接口

MCP是一个开放的协议标准,用于连接AI模型与外部系统。它的核心思想极其简单:定义一个标准化的"能力描述"和"调用接口",让任何外部系统都可以通过这个接口暴露自己的能力给AI模型

这和USB(Universal Serial Bus)的理念如出一辙:

  • USB定义了一个标准接口,让任何设备都可以通过它连接到计算机
  • MCP定义了一个标准接口,让任何外部系统都可以通过它连接到AI模型

9.2.1 MCP的核心概念

MCP协议定义了三个核心概念:

  1. Server(服务器):一个MCP Server封装了一个或多个外部系统的能力。例如:

    • postgres-mcp-server:封装PostgreSQL数据库操作
    • jira-mcp-server:封装Jira API
    • slack-mcp-server:封装Slack消息API
  2. Tool(工具):MCP Server暴露给AI模型的可调用函数。每个工具有:

    • 名称(name)
    • 描述(description)
    • 参数Schema(inputSchema)
  3. Resource(资源):MCP Server暴露给AI模型的可读取数据。例如:

    • 一个数据库表的结构定义
    • 一个API文档的JSON Schema
    • 一个项目的配置信息

9.2.2 Claude Code中的MCP工具接口

sdk-tools.d.ts可以看到,Claude Code为MCP提供了三个内置工具:

typescript
// 列出MCP服务器的资源
interface ListMcpResourcesInput {
  server?: string;  // 可选:按服务器名过滤
}

interface ListMcpResourcesOutput {
  uri: string;        // 资源URI
  name: string;       // 资源名称
  mimeType?: string;  // MIME类型
  description?: string; // 资源描述
  server: string;     // 来源服务器
}[];

// 读取MCP资源
interface ReadMcpResourceInput {
  server: string;  // MCP服务器名
  uri: string;     // 资源URI
}

// 通用MCP工具调用
interface McpInput {
  [k: string]: unknown;  // 完全动态的参数
}

type McpOutput = string;  // 统一返回字符串

注意McpInput的设计——它的参数是完全动态的([k: string]: unknown)。这是因为不同的MCP Server有不同的工具,每个工具的参数定义不同。Claude Code在这里选择了运行时动态调度,而不是编译时类型安全。

这是一种务实的工程选择:既然无法预先知道所有可能的MCP工具,那就让类型系统退一步,在运行时保证正确性。

9.3 配置来源:MCP的五种接入路径

Claude Code支持从多个来源配置MCP Server,这是一个精心设计的多层级配置系统:

9.3.1 CLI命令行配置

bash
claude --mcp-config /path/to/mcp.json

通过CLI参数直接指定MCP配置文件。这是最简单也最直接的方式。

9.3.2 claude.ai Connectors

Claude.ai平台可以直接配置连接器(Connectors),这些连接器会自动同步到Claude Code。这意味着企业IT管理员可以在claude.ai上集中管理MCP配置,所有使用Claude Code的开发者自动获得这些能力。

9.3.3 插件MCP

Claude Code的插件系统(详见第11章)允许插件提供MCP Server。当插件被安装时,其内置的MCP Server会自动注册。

9.3.4 企业策略

对于企业用户,Claude Code支持通过企业策略文件(如bq6()函数过滤)来控制MCP Server的可用性:

javascript
// bq6():企业安全过滤
function bq6(mcpServers) {
  return mcpServers.filter(server => {
    // 检查allowed列表
    if (policy.allowedServers.includes(server.name)) return true;
    // 检查blocked列表
    if (policy.blockedServers.includes(server.name)) return false;
    // 默认拒绝
    return false;
  });
}

这种白名单/黑名单机制确保了企业可以对AI能够访问的外部系统进行精细控制。

9.3.5 MCP CLI命令组

Claude Code内置了一组MCP管理命令:

bash
claude mcp serve          # 启动当前项目的MCP Server
claude mcp add-json       # 添加MCP Server配置(JSON格式)
claude mcp remove         # 移除MCP Server
claude mcp list           # 列出已配置的MCP Server
claude mcp get            # 获取MCP Server详情
claude mcp add-from-claude-desktop  # 从Claude Desktop导入配置
claude mcp reset-project-choices   # 重置项目级MCP选择

这组命令覆盖了MCP Server的完整生命周期管理。

9.4 MCP协议的通信机制

9.4.1 传输层

MCP协议支持两种传输方式:

  1. stdio:通过标准输入/输出通信。适用于本地运行的MCP Server。
  2. SSE:通过HTTP Server-Sent Events通信。适用于远程MCP Server。
json
{
  "mcpServers": {
    "postgres": {
      "command": "npx",
      "args": ["-y", "@anthropic/mcp-postgres"],
      "env": {
        "DATABASE_URL": "postgresql://..."
      }
    },
    "remote-api": {
      "url": "https://api.example.com/mcp",
      "headers": {
        "Authorization": "Bearer ..."
      }
    }
  }
}

本地MCP Server通过command+args+env启动,通信通过stdio进行。远程MCP Server通过url连接,通信通过HTTP SSE进行。

9.4.2 生命周期管理

MCP Server的生命周期:

1. Claude Code启动

2. 读取MCP配置(CLI / claude.ai / 插件 / 企业策略)

3. 初始化所有配置的MCP Server

4. 发送initialize请求 → 获取能力列表(工具 + 资源)

5. 发送initialized通知 → Server就绪

6. 对话过程中动态调用工具/读取资源

7. Claude Code关闭 → 发送shutdown请求 → 终止Server进程

这个生命周期管理确保了MCP Server的正确启动和优雅关闭。

9.4.3 懒去重:避免重复的MCP Server

一个有趣的设计细节是MCP Server的懒去重机制。

当同一个MCP Server从多个来源被配置时(例如:CLI配置了一个postgres Server,同时安装的插件也提供了相同的postgres Server),Claude Code需要在运行时去重。

这个去重不是在配置加载时立即执行的,而是在首次使用时才执行("懒"的)。源码中可以看到相关的日志:C3.size——这是去重后的Server数量。

javascript
// 懒去重的逻辑(概念表示)
function deduplicateMcpServers(servers) {
  const seen = new Map();  // C3
  for (const server of servers) {
    const key = server.name;
    if (!seen.has(key)) {
      seen.set(key, server);
    } else {
      // 记录冲突来源
      logger.debug(`MCP server "${key}" from ${server.source} ` +
        `overrides existing from ${seen.get(key).source}`);
      // 实际的去重策略取决于优先级
    }
  }
  return seen;  // C3
}

Plugin Server与claude.ai Connector之间的去重特别重要——如果Claude.ai已经配置了某个MCP Server,而本地插件也提供了同名Server,需要确定谁优先。

9.5 企业安全:MCP的管控框架

在企业环境中,MCP的开放性同时意味着安全风险。Claude Code通过多层安全机制来管控MCP的使用。

9.5.1 bq6():白名单/黑名单过滤

bq6()函数是Claude Code的企业级MCP安全过滤器:

javascript
// 企业策略配置
const policy = {
  allowed: ["postgres", "redis", "internal-docs"],
  blocked: ["dangerous-tool", "test-server"]
};

function bq6(servers) {
  return servers.filter(server => {
    if (policy.blocked.includes(server.name)) return false;
    if (policy.allowed.length > 0) {
      return policy.allowed.includes(server.name);
    }
    return true;  // 无白名单时默认允许
  });
}

黑名单优先:如果一个Server同时出现在白名单和黑名单中,黑名单优先。这符合安全工程的"最小权限原则"。

9.5.2 XAA(SEP-990):企业身份认证

XAA是一个与SEP-990标准相关的企业身份认证机制。SEP-990是一个(概念上的)企业安全标准,规定了AI工具在企业环境中的身份认证流程。

XAA实现了IdP(Identity Provider)认证:

1. Claude Code启动

2. 检测企业IdP配置

3. XAA() → 向IdP发起认证请求

4. IdP验证企业身份 + 用户权限

5. 返回访问令牌 + MCP策略

6. Claude Code根据策略过滤可用的MCP Server

这确保了只有经过企业认证的用户才能访问特定的MCP Server。

9.5.3 OAuth:安全的凭据管理

部分MCP Server需要OAuth认证(例如Jira、Slack、GitHub等需要OAuth授权的服务)。

Claude Code的OAuth管理流程:

  1. client-secret环境变量:MCP Server的OAuth client secret从环境变量读取,不硬编码
  2. keychain存储:OAuth token存储在操作系统的keychain中(macOS Keychain、Windows Credential Manager、Linux Secret Service)
  3. IR4()验证:每次使用token前,通过IR4()函数验证token的有效性
javascript
// IR4():Token有效性验证
async function IR4(token, serverName) {
  try {
    // 检查token是否过期
    if (token.expiresAt < Date.now()) {
      return { valid: false, reason: "expired" };
    }
    // 验证token的scope是否匹配
    if (!token.scopes.includes(serverName)) {
      return { valid: false, reason: "scope_mismatch" };
    }
    return { valid: true };
  } catch (error) {
    return { valid: false, reason: "verification_failed" };
  }
}

9.6 Channel推送:动态能力发现

Claude Code支持通过--channels参数注册额外的能力通道:

bash
claude --channels team-internal,project-specific

以及开发环境的--dangerously-load-development-channels标志。

Channel机制允许:

  1. 团队级能力共享:团队可以维护自己的Channel,成员通过注册自动获得能力
  2. 项目级能力:特定项目的MCP配置可以打包为Channel
  3. 动态更新:Channel中的MCP Server可以动态添加/更新,无需重启

--dangerously-load-development-channels的前缀dangerously暗示了其安全风险——开发Channel中的MCP Server可能未经充分测试,可能包含安全漏洞。Claude Code通过命名约定来提醒用户注意风险。

9.7 MCP的架构意义:从"工具"到"协议"

MCP最深刻的架构意义在于,它将Claude Code的扩展模型从"工具"升级为"协议"。

9.7.1 传统工具模型

在传统模型中,一个AI编程工具的扩展性取决于其内置工具集:

内置工具集 = {FileRead, FileWrite, Bash, Grep, Glob, WebSearch, ...}

如果内置工具集不够用,用户只能等待开发者添加新工具。这形成了一个瓶颈——工具的数量和质量完全取决于核心团队的资源。

9.7.2 协议模型

在MCP的协议模型中,扩展性是无限的:

可用能力 = 内置工具 ∪ MCP工具₁ ∪ MCP工具₂ ∪ ... ∪ MCP工具ₙ

任何人都可以开发MCP Server,任何外部系统都可以通过MCP暴露给AI。扩展瓶颈从"核心团队的资源"变成了"社区的创新速度"。

9.7.3 生态飞轮

这形成了一个正反馈飞轮:

  1. 更多MCP Server → Claude Code能力更强
  2. Claude Code能力更强 → 更多开发者使用
  3. 更多开发者 → 更多MCP Server需求
  4. 更多需求 → 更多MCP Server被开发

这和VS Code的扩展生态、npm的包生态、Docker的镜像生态遵循相同的增长逻辑。

9.8 MCP的资源发现与动态能力

9.8.1 ListMcpResources:能力发现

ListMcpResources工具让AI模型能够动态发现可用的MCP资源:

typescript
interface ListMcpResourcesOutput {
  uri: string;        // 如 "postgres://tables/users"
  name: string;       // 如 "users table"
  mimeType?: string;  // 如 "application/json"
  description?: string; // 如 "User accounts table schema"
  server: string;     // 如 "postgres"
}[];

这使得AI可以在对话过程中动态学习可用的数据源,而不需要预先配置所有可能的数据源。

9.8.2 ReadMcpResource:按需数据获取

typescript
interface ReadMcpResourceInput {
  server: string;  // MCP服务器名
  uri: string;     // 资源URI
}

AI可以先列出可用资源(ListMcpResources),然后按需读取感兴趣的资源(ReadMcpResource)。这种"先发现、后消费"的模式极大地提升了灵活性。

9.8.3 动态工具注册

MCP Server的能力不是静态的——一个MCP Server可以根据运行时状态动态注册/注销工具。例如:

  • 一个数据库MCP Server可以根据当前数据库schema动态生成工具
  • 一个API文档MCP Server可以根据最新文档动态更新工具描述

这种动态能力使得MCP Server能够适应不断变化的外部系统状态。

9.9 MCP与内置工具的统一调度

从AI模型的视角来看,MCP工具和内置工具是没有区别的——它们都通过统一的工具调用接口使用。

但在Claude Code的内部实现中,它们走不同的执行路径:

工具调用请求


工具路由器

     ├─→ 内置工具 → 直接执行

     ├─→ 服务端工具 → 转发给Anthropic API

     └─→ MCP工具 → 通过MCP协议转发给对应Server

                     ├─→ stdio Server → 进程stdin/stdout
                     └─→ SSE Server → HTTP请求

这种路由机制对AI模型透明——模型只需要知道工具的名称和参数schema,不需要关心工具的实现位置。

9.10 MCP的性能考量

9.10.1 进程池

本地MCP Server通过子进程运行。每个MCP Server都是一个独立的进程,有自己的内存空间和事件循环。

Claude Code维护了一个MCP Server进程池:

  • 启动时:根据配置初始化所有Server
  • 运行时:按需发送请求到对应的Server
  • 关闭时:优雅关闭所有Server(SIGTERM → SIGKILL)

9.10.2 超时与故障处理

MCP工具调用有独立的超时机制:

javascript
async function callMcpTool(server, toolName, args) {
  try {
    const result = await Promise.race([
      server.callTool(toolName, args),
      timeout(MCP_TIMEOUT)
    ]);
    return result;
  } catch (error) {
    if (error instanceof TimeoutError) {
      // MCP Server超时
      return { error: "MCP server timeout", server, tool: toolName };
    }
    // 其他错误
    throw error;
  }
}

9.10.3 缓存

MCP的资源查询结果可以被缓存。Claude Code可能会缓存ListMcpResources的结果,避免每次对话都重新查询。

9.11 本章小结

MCP协议是Claude Code最具有远见的架构决策之一:

  • 核心思想:标准化AI与外部系统的交互接口
  • 五种配置来源:CLI、claude.ai Connectors、插件、企业策略、Channel
  • 企业安全:bq6()过滤、XAA认证、OAuth凭据管理
  • 懒去重:Plugin Server与claude.ai Connector的智能去重
  • 动态发现:ListMcpResources + ReadMcpResource的"先发现后消费"模式
  • 统一调度:MCP工具与内置工具在模型视角下的无差别对待
  • 生态飞轮:从"工具"到"协议"的范式升级

MCP的核心价值不在于技术实现(虽然实现本身也很优雅),而在于它定义了一个开放标准。这个标准让Claude Code的生态不再是零和博弈——第三方开发者可以通过MCP扩展Claude Code的能力,而Claude Code因此变得更加强大,吸引更多用户,反过来又吸引更多第三方开发者。

在下一章中,我们将看到Claude Code如何利用这个可扩展平台,实现多代理协作——让多个AI代理协同完成复杂任务。


第10章:多代理协作 — Team/Swarm架构深度剖析

基于 MIT 许可发布