第 49 章:开放协议的价值

个人公众号

源码验证日期:2026-05-15,基于 commit 0d81bb6

你在第 48 章看到 agent 架构如何在隔离与共享之间找到平衡。但 agent 只是一个内部组件。当你想让外部世界与 Claude Code 交互——让 AI 调用你们公司的内部 API、读取自定义数据库、操作专用工具——你需要一个更大的决定:用什么方式连接外部工具?

Claude Code 的回答是 MCP(Model Context Protocol)。但 MCP 不是一个简单的 API。它是一个协议。API 和协议的区别不只是用词——API 是”我提供这些接口,你来调用”,协议是”我们约定这样通信,谁都可以实现”。API 有一个拥有者,协议有一个社区。

这个选择的影响远超技术层面。它决定了生态系统的形状。


本章路线图

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
graph LR
CH41["第41章<br/>为什么是TypeScript"] --> CH42["第42章<br/>为什么是React/Ink"]
CH42["第42章<br/>为什么是React/Ink"] --> CH43["第43章<br/>为什么用Zod"]
CH43["第43章<br/>为什么用Zod"] --> CH44["第44章<br/>工具系统的演进"]
CH44["第44章<br/>工具系统的演进"] --> CH45["第45章<br/>安全与便利"]
CH45["第45章<br/>安全与便利"] --> CH46["第46章<br/>有限窗口"]
CH46["第46章<br/>有限窗口"] --> CH47["第47章<br/>大AsyncGenerator"]
CH47["第47章<br/>大AsyncGenerator"] --> CH48["第48章<br/>Agent架构"]
CH48["第48章<br/>Agent架构"] --> CH49["第49章<br/>开放协议"]
CH49["第49章<br/>开放协议"] --> CH50["第50章<br/>性能的故事"]
CH50["第50章<br/>性能的故事"] --> CH51["第51章<br/>纵深防御"]
CH51["第51章<br/>纵深防御"] --> CH52["第52章<br/>稳定、历史与未来"]

style CH41 fill:#e0e0e0,stroke:#999
style CH42 fill:#e0e0e0,stroke:#999
style CH43 fill:#e0e0e0,stroke:#999
style CH44 fill:#e0e0e0,stroke:#999
style CH45 fill:#e0e0e0,stroke:#999
style CH46 fill:#e0e0e0,stroke:#999
style CH47 fill:#e0e0e0,stroke:#999
style CH48 fill:#e0e0e0,stroke:#999
style CH49 fill:#4a90d9,stroke:#2c5f8a,color:#fff,stroke-width:3px
style CH50 fill:#e0e0e0,stroke:#999
style CH51 fill:#e0e0e0,stroke:#999
style CH52 fill:#e0e0e0,stroke:#999

现状:MCP 在 Claude Code 里的实现

多传输层,统一模型

MCP 支持六种传输方式:stdiossesse-idehttpwssdk。在 types.ts 里,每种传输方式有独立的配置 schema:

1
2
3
export const TransportSchema = lazySchema(() =>
z.enum(['stdio', 'sse', 'sse-ide', 'http', 'ws', 'sdk']),
)

但不管传输层是什么,上层看到的是统一的接口:一个 MCP 服务器暴露一组工具,每个工具有名字、描述和 JSON Schema 定义的输入。调用工具就是发一个请求、拿一个结果。

这种”多传输、统一模型”的设计是协议思维的产物。它说的是:不管你是本地进程(stdio)、远程服务(http/sse)、WebSocket 实时连接(ws)、还是 SDK 内嵌(sdk),你都是一个 MCP 服务器。差别只在管道,不在内容。

六层配置作用域

config.ts 里的 MCP 配置不是单一来源,而是六个作用域的合并:

1
plugin -> claudeai -> user -> project -> local -> enterprise

优先级从低到高。用户在 .mcp.json 里配的 project 级服务器优先于 claude.ai 网页端开的连接器;企业管理员配的 enterprise 级配置优先于一切。这不是随意的排序——它反映了控制权的主张:企业管理 > 本地项目 > 全局用户 > 云端服务 > 插件。

合并之后还有去重。dedupPluginMcpServersdedupClaudeAiMcpServers 通过内容签名(getMcpServerSignature)识别重复的服务器——即使名字不同,只要指向同一个命令或同一个 URL,就认为是同一个。这避免了同一工具被注册两次、浪费上下文空间。

安全策略的纵深

isMcpServerAllowedByPolicy 实现了三层安全检查:

  • 拒绝名单(denylist):绝对优先。如果服务器在拒绝名单里,不管其他规则怎么配,都不允许。
  • 允许名单(allowlist):如果存在允许名单,只有名单里的服务器才允许连接。
  • 默认允许:如果没有配置任何限制策略,所有服务器默认允许。

允许名单支持三种匹配方式:按名称、按命令(stdio 服务器)、按 URL 模式(远程服务器)。URL 模式还支持通配符:

1
2
3
4
5
function urlPatternToRegex(pattern: string): RegExp {
const escaped = pattern.replace(/[.+?^${}()|[\]\\]/g, '\\$&')
const regexStr = escaped.replace(/\*/g, '.*')
return new RegExp(`^${regexStr}$`)
}

这给了企业管理员细粒度的控制:允许所有 https://*.internal.company.com/* 的服务器,拒绝其他一切。


三种方案的对比

方案一:硬编码工具集

最简单的方案。把所有工具(文件读写、命令执行、Web 搜索……)都编译进程序。不需要外部服务器,不需要协议,不需要配置。

这是 Claude Code 最初的做法。最初版本的工具集就是固定的几个。但硬编码有两个致命问题:第一,新工具需要修改核心代码、重新发布;第二,第三方无法扩展——你不能让 Claude Code 调用你们公司的内部 API。

方案二:插件 API

定义一个 JavaScript/TypeScript 插件接口。第三方写一个 JS 模块,导出符合接口的工具函数,Claude Code 加载它。

这在 VS Code、Webpack 等项目里很常见。优点是集成度高——插件可以访问宿主的内部 API。缺点是耦合度高——插件依赖宿主的内部实现,宿主升级可能破坏插件。

更重要的是安全问题。一个 JS 插件运行在 Claude Code 的进程里,可以访问文件系统、网络、环境变量。沙箱化 JS 在理论上可行,在实践中很困难。

方案三:开放协议(MCP)

定义一个通信协议。工具提供者和工具消费者通过这个协议交互,中间的管道可以是任何传输层。第三方不需要知道 Claude Code 的内部实现,只需要实现协议。


为什么选了开放协议

理由一:可组合性——乘法效应

协议的核心价值是可组合性。一个 MCP 服务器不需要知道谁在调用它。同一个文件系统 MCP 服务器可以被 Claude Code 调用,也可以被 Cursor 调用,也可以被任何实现了 MCP 客户端的程序调用。

这意味着生态系统的增长是乘法式的:N 个客户端 x M 个服务器 = N x M 种组合。如果只是 API,增长是加法式的:每新增一个客户端,需要为 M 个服务器写 M 个适配器。

config.ts 里的插件服务器系统已经展示了这个效应。插件(loadAllPluginsCacheOnly)提供自己的 MCP 服务器,这些服务器和用户手动配置的服务器享受同样的生命周期管理——连接、工具发现、权限检查、去重。插件不需要知道 Claude Code 怎么管理这些,只需要提供配置。

理由二:安全边界——进程级隔离

MCP 服务器运行在独立的进程里(stdio 传输)或独立的网络上(http/sse/ws 传输)。这意味着它不能直接访问 Claude Code 的内存、文件系统或 API 密钥。通信只能通过协议定义的消息进行。

这种进程级隔离比 JS 插件的沙箱化强得多。你不需要信任 MCP 服务器的代码,只需要信任它的输入输出。即使一个恶意的 MCP 服务器,它的影响范围也被协议的边界限制。

理由三:生态锁定避免

如果 Claude Code 只有一个私有的工具接口,第三方要么为 Claude Code 专门写适配器,要么不支持它。MCP 作为开放协议意味着:

  • 工具服务器只需要实现一次,所有 MCP 客户端都能用
  • Claude Code 不需要为每个工具写专门的集成
  • 用户不会被锁定在单一客户端——他们可以带着工具配置迁移

config.ts 里的 .mcp.json 文件就是这个理念的体现。它放在项目根目录,可以提交到版本控制,团队里每个人都能用同样的工具配置。不管他们用的是 Claude Code 还是别的 MCP 客户端。

理由四:企业友好——精确控制

企业需要控制。config.ts 里的企业配置作用域(enterprise)给了管理员独占控制权:

1
2
3
4
if (doesEnterpriseMcpConfigExist()) {
// 企业配置存在时,不加载其他任何作用域
return { servers: filtered, errors: [] }
}

这不是附加功能,是设计核心。如果 MCP 是私有 API,企业要么完全接受,要么完全拒绝。作为协议,企业可以精确控制允许哪些服务器、拒绝哪些传输方式、要求哪些安全策略。


与其他协议的横向对比

特征MCPLSP(Language Server Protocol)OpenAPIGraphQL
领域AI 工具交互代码编辑器功能Web API 描述数据查询
设计者AnthropicMicrosoft社区Facebook
传输多种(stdio/http/ws)多种(stdio/tcp/ws)HTTPHTTP
发现工具列表 + schema能力注册Schema 文件Schema 内省
生态模式客户端 + 服务器客户端 + 服务器生成器 + 消费者客户端 + 服务器

MCP 的设计借鉴了 LSP 的成功经验。LSP 让编辑器和语言服务解耦——一个 Python 语言服务器可以被 VS Code、Neovim、Emacs 同时使用。MCP 试图做同样的事:让 AI 客户端和工具服务解耦。

关键区别在于:LSP 的交互是确定性的——“跳转到定义”总是返回同一个结果。MCP 的交互是非确定性的——工具的输入由 AI 模型生成,可能不完全符合 schema。这就是为什么 MCP 特别强调 schema 校验和错误处理。


如果重新设计

服务器发现

当前的 MCP 配置是静态的——用户或管理员手动指定每个服务器的连接信息。一个更强大的设计是动态发现:MCP 服务器在网络上广播自己的存在,客户端自动发现并连接。

动态发现带来新的安全挑战——你需要验证服务器的身份、加密通信、防止伪装。但作为可选功能,它可以让 MCP 的生态系统更加流畅。

流式工具结果

当前的 MCP 工具调用是请求-响应模式:发一个请求,等一个完整的结果。但有些操作天生是流式的——长时间运行的命令、实时数据查询、大文件的分块处理。

如果协议支持流式响应,工具可以在结果生成时就发送部分数据,而不是等到全部完成。这对用户体验是质的提升。

跨服务器组合

当前的每个 MCP 服务器是独立的。你不能让一个服务器的工具调用另一个服务器的工具。一个更有表达力的设计是允许工具组合——一个”查询数据库”的 MCP 工具可以把结果传给一个”生成图表”的 MCP 工具,形成管道。


试试看

练习一:写一个最小的 MCP 服务器

用 Node.js 写一个最简单的 MCP stdio 服务器——只暴露一个工具,返回 “Hello, MCP!”。在 .mcp.json 中配置它,让 Claude Code 调用。

练习二:追踪配置合并

config.ts 中找到 MCP 配置合并的逻辑,追踪六个作用域的合并顺序。在多个作用域中配置同一个服务器(名字不同但 URL 相同),观察去重逻辑如何工作。

练习三:对比 LSP 的设计

阅读 LSP(Language Server Protocol)的规范,对比 MCP 的设计。两者在传输层、发现机制、能力协商上有什么异同?MCP 从 LSP 学到了什么,又做了什么不同的选择?


检查点

  • MCP 是协议不是 API:”约定高于实现”——任何人都可以实现客户端或服务器
  • 六种传输方式:stdio、sse、sse-ide、http、ws、sdk——管道不同,模型统一
  • 六层配置作用域:plugin -> claudeai -> user -> project -> local -> enterprise
  • 被否决的方案:硬编码工具集(无法扩展)、插件 API(安全风险高)
  • 乘法效应:N 个客户端 x M 个服务器 = N x M 种组合
  • 安全边界:进程级隔离比 JS 沙箱更可靠
  • 企业控制:denylist/allowlist + URL 通配符 + 企业配置优先

导航

上一章:第 48 章:Agent 架构的取舍

下一章:第 50 章:性能的故事