Skip to content

第19章 Claude Code 的 MCP 客户端:12 万行的实战

"The proof of a protocol is not in its specification, but in the 120,000 lines of code that someone actually ships with it."

本章要点

  • 理解 Claude Code 作为 MCP 客户端的整体架构及其与 Claude Desktop 的本质区别
  • 掌握 Server 发现机制:从 claude_desktop_config.json 到动态注册
  • 深入 OAuth 授权流程在 CLI 环境下的特殊处理
  • 理解工具延迟加载(Deferred Loading)如何解决大规模 MCP 集成的性能问题
  • 认识 MCP 工具如何与 Claude Code 内置工具在统一接口下共存

19.1 为什么要研究 Claude Code

在前面的章节中,我们从协议规范和 SDK 源码的角度理解了 MCP 的方方面面。但规范是抽象的,SDK 提供的是积木,真正的挑战在于用这些积木搭建一个面向百万用户的产品

Claude Code 是 Anthropic 官方的命令行 AI 编程工具。截至 2026 年初,其代码库中与 MCP 相关的代码量超过 12 万行——这不仅是目前最大规模的 MCP 客户端实现,也是 MCP 协议本身的"试验场":许多协议特性是在 Claude Code 的实践中被发现需要、被提出、最终被纳入规范的。

研究 Claude Code 的 MCP 实现,不是为了模仿它的每一行代码,而是为了理解一个成熟的 MCP 客户端需要解决哪些协议规范没有明说的工程问题

19.2 架构全景

Claude Code 的 MCP 客户端架构可以分为五个层次:

与 Claude Desktop 不同,Claude Code 是一个 CLI-first 的应用。这意味着它面临一些独特的挑战:

  • 没有持久的 GUI 窗口来展示 Elicitation 表单
  • OAuth 回调需要临时启动本地 HTTP Server 来接收
  • 终端环境对并发连接数和资源占用更敏感
  • 用户期望启动速度极快,不能等待所有 Server 初始化

19.3 Server 发现与配置

19.3.1 配置文件层级

Claude Code 使用与 Claude Desktop 兼容的配置格式,但引入了更灵活的层级系统:

~/.claude/claude_desktop_config.json     # 全局配置
./.claude/claude_desktop_config.json     # 项目级配置
./.mcp.json                              # MCP 专用配置(简化格式)

一个典型的配置文件:

json
{
  "mcpServers": {
    "github": {
      "command": "npx",
      "args": ["-y", "@modelcontextprotocol/server-github"],
      "env": {
        "GITHUB_TOKEN": "ghp_..."
      }
    },
    "filesystem": {
      "command": "npx",
      "args": ["-y", "@modelcontextprotocol/server-filesystem", "/home/user/projects"]
    },
    "remote-db": {
      "url": "https://db-mcp.example.com/mcp",
      "headers": {
        "Authorization": "Bearer ${MCP_DB_TOKEN}"
      }
    }
  }
}

配置合并策略是深度合并,项目级覆盖全局。同名 Server 在项目级配置中出现时完全替换全局配置中的同名条目。

19.3.2 环境变量替换

注意上面 ${MCP_DB_TOKEN} 的用法——Claude Code 支持在配置文件中引用环境变量。这是一个实用的安全措施:密钥不应硬编码在配置文件中,尤其是项目级配置可能被提交到 Git 仓库。

19.3.3 Server 启动与健康检查

Server Manager 在启动时并不立即初始化所有 Server。它采用按需启动策略:

  1. 解析配置文件,构建 Server 注册表
  2. 对 stdio 类型的 Server,在首次被引用时才 spawn 子进程
  3. 对 HTTP 类型的 Server,在首次请求时才建立连接
  4. 每个 Server 启动后执行 MCP 初始化握手(initializeinitialized
  5. 缓存 Server 的能力声明,用于后续的功能路由

如果一个 Server 在启动或初始化过程中失败,Server Manager 会记录错误但不影响其他 Server——这是"优雅降级"的核心体现。

19.4 OAuth 在 CLI 中的实现

19.4.1 无浏览器环境的挑战

对于远程 MCP Server,认证通常通过 OAuth 完成。在有 GUI 的桌面应用中,这很自然——弹出浏览器,完成授权,回调到应用。但在 CLI 环境中,这需要一些巧妙的处理。

Claude Code 的 OAuth 流程:

关键实现细节:

  • 临时 HTTP Server 只监听 127.0.0.1 的随机端口,最大程度降低安全风险
  • 使用 PKCE(Proof Key for Code Exchange)防止 authorization code 被截获
  • Token 存储在操作系统的密钥链中(macOS Keychain / Linux Secret Service)
  • Refresh token 在过期前自动刷新,用户无感

19.4.2 Token 生命周期管理

OAuth Handler 维护一个 token 缓存,键是 Server URL + 用户标识。每次向远程 Server 发送请求前,都会检查 token 是否即将过期(提前 5 分钟刷新),确保请求不会因为 token 过期而意外失败。

19.5 工具延迟加载(Deferred Loading)

19.5.1 问题的规模

一个典型的 Claude Code 用户可能配置了 5-10 个 MCP Server,每个 Server 暴露 3-20 个工具。这意味着在最坏情况下,系统需要管理上百个工具。如果在每次对话开始时都把所有工具的完整定义(包括 JSON Schema)发送给 LLM,会造成:

  1. 上下文浪费:工具描述可能占用数千 token,挤占真正有用的对话内容
  2. 决策干扰:LLM 面对过多工具时,选择正确工具的准确率会下降
  3. 启动延迟:等待所有 Server 返回工具列表会拖慢首次交互

19.5.2 延迟加载策略

Claude Code 实现了一个精巧的分层加载机制:

第一层:工具名称 + 简要描述。对话开始时,只向 LLM 提供所有可用工具的名称和一行描述。这类似于一个"目录",让 LLM 知道有哪些能力可用,但不占用太多 context。

第二层:完整工具定义。当 LLM 决定使用某个工具时,Claude Code 才获取该工具的完整 JSON Schema,包括详细的参数描述、示例、约束条件等。

这种模式在用户体验中表现为:用户在系统提示中看到类似"以下延迟工具可通过 ToolSearch 获取:Read, Edit, Grep..."的提示,当 Agent 判断需要使用某个工具时,它先调用 ToolSearch 获取完整定义,然后才发起实际的工具调用。

typescript
// 概念性代码,展示延迟加载的核心思路
class DeferredToolLoader {
  private summaries: Map<string, ToolSummary> = new Map();
  private fullDefinitions: Map<string, Tool> = new Map();

  // 对话开始时只加载摘要
  async loadSummaries(): Promise<ToolSummary[]> {
    const results: ToolSummary[] = [];
    for (const server of this.servers) {
      const tools = await server.listTools();
      for (const tool of tools) {
        const summary = { name: tool.name, description: tool.description?.slice(0, 80) };
        this.summaries.set(tool.name, summary);
        results.push(summary);
      }
    }
    return results;
  }

  // LLM 需要时才获取完整定义
  async getFullDefinition(toolName: string): Promise<Tool | null> {
    if (this.fullDefinitions.has(toolName)) {
      return this.fullDefinitions.get(toolName)!;
    }
    // 找到对应的 Server 并获取完整工具信息
    const server = this.findServerForTool(toolName);
    if (!server) return null;
    const tools = await server.listTools();
    const tool = tools.find(t => t.name === toolName);
    if (tool) {
      this.fullDefinitions.set(toolName, tool);
    }
    return tool ?? null;
  }
}

19.5.3 与内置工具的统一

Claude Code 有大量内置工具——Read(读文件)、Edit(编辑文件)、Bash(执行命令)、Grep(搜索)等。MCP 工具需要与这些内置工具在同一个接口下暴露给 LLM。

Tool Manager 维护一个统一的工具注册表:

当 LLM 发起工具调用时,Tool Manager 根据工具名称路由到正确的执行器——内置工具直接在进程内执行,MCP 工具则通过对应的 Server 连接转发 tools/call 请求。对 LLM 来说,两者完全透明。

19.5.4 命名空间与冲突处理

MCP 工具的命名采用 serverName:toolName 的命名空间格式。如果两个 Server 都暴露了名为 search 的工具,它们会被注册为 github:searchjira:search,避免冲突。

但如果 MCP 工具名与内置工具名冲突,内置工具优先。这确保了核心功能不会被第三方 Server 意外覆盖——又一个"安全优先"的设计选择。

19.6 Elicitation 与 Sampling 的 CLI 适配

19.6.1 终端中的表单渲染

当 MCP Server 发起 Form Mode Elicitation 时,Claude Code 需要在终端中渲染表单。这比 GUI 应用困难得多——终端没有原生的表单控件。

Claude Code 的做法是将 Elicitation 请求转化为交互式的终端提示:

[MCP Server: github] 请求你提供信息:
  请提供你的 GitHub 用户名

  用户名 (必填): _
  邮箱 (email 格式): _

  [Enter] 提交  [Esc] 取消  [Tab] 下一字段

对于枚举类型,使用箭头键选择的列表:

  选择部署环境:
  > staging
    production
    development

19.6.2 Sampling 的隐含实现

Claude Code 对 Sampling 请求的处理尤为独特——它不需要"借用"外部 LLM,因为它本身就运行着 Claude。当 MCP Server 发送 sampling/createMessage 请求时,Claude Code 直接将请求注入到当前的 LLM 对话流中,利用自己的 Claude 模型来响应。

这带来了一个有趣的递归:Server 通过 Client 借用 LLM 的能力,而这个 LLM 正是驱动 Client 的同一个模型。实际上,这使得 MCP Server 可以利用 Claude 的推理能力来增强自己的工具执行,同时保持了协议层面的解耦。

19.7 连接管理与容错

19.7.1 Server 生命周期状态机

每个 Server 连接都有明确的状态:

[未启动] → [启动中] → [初始化中] → [就绪] → [运行中]
                ↓           ↓          ↓          ↓
             [启动失败]  [初始化失败] [断开]    [错误]

                                     [重连中] → [就绪]

Server Manager 为每个状态定义了明确的行为:

  • 启动失败:记录错误,标记为不可用,不重试(避免反复 spawn 失败进程)
  • 初始化失败:可能是版本不兼容,记录 Server 报告的版本信息,帮助用户诊断
  • 断开:对 stdio Server,检测子进程是否退出;对 HTTP Server,检测连接是否中断。自动尝试重连
  • 错误:工具调用返回错误不影响连接状态,但连续多次错误可能触发健康检查

19.7.2 超时策略

不同操作的超时时间经过精心调优:

操作超时时间理由
Server 启动30 秒npm 安装可能较慢
初始化握手10 秒协议交互应快速完成
工具列表5 秒元数据查询不应耗时
工具调用可配置,默认 120 秒复杂操作可能耗时较长
Elicitation 响应300 秒等待用户输入

19.7.3 资源管理

Claude Code 作为 CLI 工具,用户期望它在退出时干净地释放所有资源。Server Manager 在关闭时:

  1. 向所有 Server 发送关闭通知
  2. 等待正在进行的工具调用完成(最多 5 秒)
  3. 关闭所有传输连接
  4. 终止所有 stdio 子进程
  5. 清理临时文件和端口占用

19.8 12 万行代码的启示

Claude Code 的 MCP 实现规模之大,背后是真实的工程复杂性:

配置管理约占 15%——多层级配置合并、环境变量替换、配置验证、迁移升级。

传输与连接约占 25%——stdio 子进程管理、HTTP 连接池、重连逻辑、超时处理、OAuth 全流程。

工具管理约占 20%——延迟加载、命名空间、与内置工具融合、权限控制、工具描述缓存。

Elicitation/Sampling 适配约占 15%——终端 UI 渲染、表单验证、OAuth URL 处理、Sampling 请求路由。

测试与错误处理约占 25%——每个边界条件、每个竞态条件、每个错误路径都需要测试。12 万行中有相当一部分是测试代码。

这个比例揭示了一个重要事实:MCP 客户端的核心复杂性不在协议本身,而在于将协议适配到具体的产品形态中。CLI 有 CLI 的挑战,桌面应用有桌面应用的挑战,Web 应用有 Web 应用的挑战。MCP 协议的抽象层级恰到好处——它定义了"做什么",但把"怎么做"的自由留给了实现者。

19.9 对其他客户端的借鉴

从 Claude Code 的实践中,我们可以提炼出对所有 MCP 客户端实现者的建议:

  1. 按需连接,延迟加载。不要在启动时初始化所有 Server,用户可能只用其中一两个。
  2. 配置分层,安全优先。支持全局和项目级配置,密钥通过环境变量或系统密钥链管理。
  3. 统一工具接口。MCP 工具和内置工具对 LLM 应该透明无差异,命名空间解决冲突。
  4. 优雅降级。单个 Server 的故障不应影响整个系统,错误信息应帮助用户定位问题。
  5. 资源意识。CLI 工具对启动速度和内存占用敏感,Web 应用对并发连接数敏感,移动端对电量消耗敏感——了解你的运行环境。

19.10 本章小结

Claude Code 是 MCP 协议在真实产品中最大规模的落地案例。通过研究它的架构,我们看到了:

  • Server 发现通过分层配置文件实现,支持全局、项目级、环境变量三个维度
  • OAuth 认证在 CLI 环境下通过临时本地 HTTP Server + PKCE 实现,兼顾安全与易用
  • 工具延迟加载通过"摘要 + 按需获取"的两层策略,解决了大规模工具集成的 context 压力
  • 内置工具与 MCP 工具通过统一的 Tool Manager 融合,对 LLM 透明
  • 12 万行代码中真正的复杂性在于将协议适配到具体产品形态,而非协议本身

这些经验告诉我们:设计一个好的 MCP 客户端,既要深入理解协议规范,更要理解你的用户在什么场景下、用什么方式与 AI 工具交互。协议提供了骨架,产品思维才能填充血肉。

在下一章,我们将角色互换——从消费 MCP 的 Client 转向生产 MCP 的 Server,从零开始构建一个生产级 MCP Server。

基于 VitePress 构建