扫码
40.67M · 2026-04-17
:::tip 本章要点
在前面的章节中,我们从协议规范和 SDK 源码的角度理解了 MCP 的方方面面。但规范是抽象的,SDK 提供的是积木,真正的挑战在于用这些积木搭建一个面向百万用户的产品。
Claude Code 是 Anthropic 官方的命令行 AI 编程工具。截至 2026 年初,其代码库中与 MCP 相关的代码量超过 12 万行——这不仅是目前最大规模的 MCP 客户端实现,也是 MCP 协议本身的"试验场":许多协议特性是在 Claude Code 的实践中被发现需要、被提出、最终被纳入规范的。
研究 Claude Code 的 MCP 实现,不是为了模仿它的每一行代码,而是为了理解一个成熟的 MCP 客户端需要解决哪些协议规范没有明说的工程问题。
Claude Code 的 MCP 客户端架构可以分为五个层次:
graph TB
subgraph "用户交互层"
CLI["CLI 界面"]
REPL["交互式 REPL"]
end
subgraph "Agent 层"
AG["Agent Core"]
TM["Tool Manager<br/>统一内置工具 + MCP 工具"]
end
subgraph "MCP 客户端层"
SD["Server Discovery<br/>配置发现"]
SM["Server Manager<br/>生命周期管理"]
OA["OAuth Handler<br/>认证管理"]
DL["Deferred Loader<br/>延迟加载"]
end
subgraph "传输层"
ST["Stdio Transport"]
HT["Streamable HTTP Transport"]
end
subgraph "MCP Servers"
S1["本地 Server<br/>(stdio)"]
S2["远程 Server<br/>(HTTP)"]
S3["内置 Server<br/>(filesystem, etc.)"]
end
CLI --> AG
REPL --> AG
AG --> TM
TM --> SM
SM --> SD
SM --> OA
SM --> DL
SM --> ST
SM --> HT
ST --> S1
HT --> S2
ST --> S3
style AG fill:#3b82f6,color:#fff,stroke:none
style TM fill:#8b5cf6,color:#fff,stroke:none
style SM fill:#ec4899,color:#fff,stroke:none
style SD fill:#f59e0b,color:#fff,stroke:none
style OA fill:#10b981,color:#fff,stroke:none
style DL fill:#6366f1,color:#fff,stroke:none
与 Claude Desktop 不同,Claude Code 是一个 CLI-first 的应用。这意味着它面临一些独特的挑战:
Claude Code 使用与 Claude Desktop 兼容的配置格式,但引入了更灵活的层级系统:
~/.claude/claude_desktop_config.json # 全局配置
./.claude/claude_desktop_config.json # 项目级配置
./.mcp.json # MCP 专用配置(简化格式)
一个典型的配置文件:
{
"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 在项目级配置中出现时完全替换全局配置中的同名条目。
注意上面 ${MCP_DB_TOKEN} 的用法——Claude Code 支持在配置文件中引用环境变量。这是一个实用的安全措施:密钥不应硬编码在配置文件中,尤其是项目级配置可能被提交到 Git 仓库。
Server Manager 在启动时并不立即初始化所有 Server。它采用按需启动策略:
initialize → initialized)如果一个 Server 在启动或初始化过程中失败,Server Manager 会记录错误但不影响其他 Server——这是"优雅降级"的核心体现。
对于远程 MCP Server,认证通常通过 OAuth 完成。在有 GUI 的桌面应用中,这很自然——弹出浏览器,完成授权,回调到应用。但在 CLI 环境中,这需要一些巧妙的处理。
Claude Code 的 OAuth 流程:
sequenceDiagram
participant U as 用户终端
participant CC as Claude Code
participant LS as 本地 HTTP Server
participant RS as 远程 MCP Server
participant AS as OAuth 授权服务器
CC->>RS: 尝试连接(无凭证)
RS->>CC: 401 + OAuth metadata URL
CC->>AS: 获取 OAuth metadata
AS->>CC: authorization_endpoint, token_endpoint
CC->>CC: 生成 PKCE code_verifier/challenge
CC->>LS: 启动临时 HTTP Server (localhost:随机端口)
CC->>U: "请在浏览器中打开此 URL 完成授权"
Note over U: 用户打开浏览器
U->>AS: 访问授权页面
U->>AS: 授权同意
AS->>LS: 重定向回调,携带 authorization_code
LS->>CC: 接收 code
CC->>AS: 用 code + code_verifier 换取 token
AS->>CC: access_token + refresh_token
CC->>RS: 使用 access_token 连接
RS->>CC: 初始化成功
Note over CC: 关闭临时 HTTP Server
Note over CC: 安全存储 token
关键实现细节:
127.0.0.1 的随机端口,最大程度降低安全风险OAuth Handler 维护一个 token 缓存,键是 Server URL + 用户标识。每次向远程 Server 发送请求前,都会检查 token 是否即将过期(提前 5 分钟刷新),确保请求不会因为 token 过期而意外失败。
一个典型的 Claude Code 用户可能配置了 5-10 个 MCP Server,每个 Server 暴露 3-20 个工具。这意味着在最坏情况下,系统需要管理上百个工具。如果在每次对话开始时都把所有工具的完整定义(包括 JSON Schema)发送给 LLM,会造成:
Claude Code 实现了一个精巧的分层加载机制:
第一层:工具名称 + 简要描述。对话开始时,只向 LLM 提供所有可用工具的名称和一行描述。这类似于一个"目录",让 LLM 知道有哪些能力可用,但不占用太多 context。
第二层:完整工具定义。当 LLM 决定使用某个工具时,Claude Code 才获取该工具的完整 JSON Schema,包括详细的参数描述、示例、约束条件等。
这种模式在用户体验中表现为:用户在系统提示中看到类似"以下延迟工具可通过 ToolSearch 获取:Read, Edit, Grep..."的提示,当 Agent 判断需要使用某个工具时,它先调用 ToolSearch 获取完整定义,然后才发起实际的工具调用。
// 概念性代码,展示延迟加载的核心思路
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;
}
}
Claude Code 有大量内置工具——Read(读文件)、Edit(编辑文件)、Bash(执行命令)、Grep(搜索)等。MCP 工具需要与这些内置工具在同一个接口下暴露给 LLM。
Tool Manager 维护一个统一的工具注册表:
graph LR
subgraph "Tool Manager 统一注册表"
direction TB
B1["Read (内置)"]
B2["Edit (内置)"]
B3["Bash (内置)"]
B4["Grep (内置)"]
M1["github:create_issue (MCP)"]
M2["github:list_repos (MCP)"]
M3["db:query (MCP)"]
M4["slack:send_message (MCP)"]
end
LLM["Claude LLM"] --> |"统一的工具列表"| B1
LLM --> M1
style B1 fill:#3b82f6,color:#fff,stroke:none
style B2 fill:#3b82f6,color:#fff,stroke:none
style B3 fill:#3b82f6,color:#fff,stroke:none
style B4 fill:#3b82f6,color:#fff,stroke:none
style M1 fill:#10b981,color:#fff,stroke:none
style M2 fill:#10b981,color:#fff,stroke:none
style M3 fill:#10b981,color:#fff,stroke:none
style M4 fill:#10b981,color:#fff,stroke:none
当 LLM 发起工具调用时,Tool Manager 根据工具名称路由到正确的执行器——内置工具直接在进程内执行,MCP 工具则通过对应的 Server 连接转发 tools/call 请求。对 LLM 来说,两者完全透明。
MCP 工具的命名采用 serverName:toolName 的命名空间格式。如果两个 Server 都暴露了名为 search 的工具,它们会被注册为 github:search 和 jira:search,避免冲突。
但如果 MCP 工具名与内置工具名冲突,内置工具优先。这确保了核心功能不会被第三方 Server 意外覆盖——又一个"安全优先"的设计选择。
当 MCP Server 发起 Form Mode Elicitation 时,Claude Code 需要在终端中渲染表单。这比 GUI 应用困难得多——终端没有原生的表单控件。
Claude Code 的做法是将 Elicitation 请求转化为交互式的终端提示:
[MCP Server: github] 请求你提供信息:
请提供你的 GitHub 用户名
用户名 (必填): _
邮箱 (email 格式): _
[Enter] 提交 [Esc] 取消 [Tab] 下一字段
对于枚举类型,使用箭头键选择的列表:
选择部署环境:
> staging
production
development
Claude Code 对 Sampling 请求的处理尤为独特——它不需要"借用"外部 LLM,因为它本身就运行着 Claude。当 MCP Server 发送 sampling/createMessage 请求时,Claude Code 直接将请求注入到当前的 LLM 对话流中,利用自己的 Claude 模型来响应。
这带来了一个有趣的递归:Server 通过 Client 借用 LLM 的能力,而这个 LLM 正是驱动 Client 的同一个模型。实际上,这使得 MCP Server 可以利用 Claude 的推理能力来增强自己的工具执行,同时保持了协议层面的解耦。
每个 Server 连接都有明确的状态:
[未启动] → [启动中] → [初始化中] → [就绪] → [运行中]
↓ ↓ ↓ ↓
[启动失败] [初始化失败] [断开] [错误]
↓
[重连中] → [就绪]
Server Manager 为每个状态定义了明确的行为:
不同操作的超时时间经过精心调优:
| 操作 | 超时时间 | 理由 |
|---|---|---|
| Server 启动 | 30 秒 | npm 安装可能较慢 |
| 初始化握手 | 10 秒 | 协议交互应快速完成 |
| 工具列表 | 5 秒 | 元数据查询不应耗时 |
| 工具调用 | 可配置,默认 120 秒 | 复杂操作可能耗时较长 |
| Elicitation 响应 | 300 秒 | 等待用户输入 |
Claude Code 作为 CLI 工具,用户期望它在退出时干净地释放所有资源。Server Manager 在关闭时:
Claude Code 的 MCP 实现规模之大,背后是真实的工程复杂性:
配置管理约占 15%——多层级配置合并、环境变量替换、配置验证、迁移升级。
传输与连接约占 25%——stdio 子进程管理、HTTP 连接池、重连逻辑、超时处理、OAuth 全流程。
工具管理约占 20%——延迟加载、命名空间、与内置工具融合、权限控制、工具描述缓存。
Elicitation/Sampling 适配约占 15%——终端 UI 渲染、表单验证、OAuth URL 处理、Sampling 请求路由。
测试与错误处理约占 25%——每个边界条件、每个竞态条件、每个错误路径都需要测试。12 万行中有相当一部分是测试代码。
这个比例揭示了一个重要事实:MCP 客户端的核心复杂性不在协议本身,而在于将协议适配到具体的产品形态中。CLI 有 CLI 的挑战,桌面应用有桌面应用的挑战,Web 应用有 Web 应用的挑战。MCP 协议的抽象层级恰到好处——它定义了"做什么",但把"怎么做"的自由留给了实现者。
从 Claude Code 的实践中,我们可以提炼出对所有 MCP 客户端实现者的建议:
Claude Code 是 MCP 协议在真实产品中最大规模的落地案例。通过研究它的架构,我们看到了:
这些经验告诉我们:设计一个好的 MCP 客户端,既要深入理解协议规范,更要理解你的用户在什么场景下、用什么方式与 AI 工具交互。协议提供了骨架,产品思维才能填充血肉。
在下一章,我们将角色互换——从消费 MCP 的 Client 转向生产 MCP 的 Server,从零开始构建一个生产级 MCP Server。