MCP 协议设计与实现
第21章 设计模式与架构决策
第21章 设计模式与架构决策
“Patterns are not invented, they are discovered — in the recurring decisions that successful systems make when facing the same fundamental tensions.”
“好的协议不是发明出来的,是被迫发现的——当一个系统面对多个根本性张力时,它会收敛到一组几乎唯一的设计选择。”
本章要点
- 从全书内容中提炼六大核心设计模式,每个模式对应一对根本张力
- 理解每个模式在 MCP 不同章节中的具体体现,建立横向视图
- 掌握模式间的协同关系与互相强化,它们不是孤立的条目而是一套咬合系统
- 认识 MCP 设计决策对未来协议演进的约束与启示
- 建立可迁移到其他系统设计的架构决策框架——协议设计的”六脉神剑”
21.1 模式的发现
21.1.1 为什么”从现象总结”比”空谈原理”更有力
本章是全书的收官之章。我不打算从理论出发罗列”好协议的 10 条标准”——那种文章网上太多。我要做的是反向操作:带你回看前 20 章的具体决策,从中反向归纳出 MCP 作者群最底层的思考模型。
为什么这样做?
- 空谈原理时人人点头,落地时人人走样。抽象的原则很难内化
- 反之,从具体 API 反推”为什么这里要三个参数而不是两个”,就能在大脑里建立可复用的模式匹配器
- 下次你自己设计协议时,遇到类似张力,这些模式会自动触发,替你做出更好的决策
21.1.2 全书脉络回顾
回顾这 21 章的旅程——从 MCP 的诞生动机(第 1 章)到架构全景(第 2 章),到 JSON-RPC 基础(第 3 章),到生命周期管理(第 4 章),到三大原语的深入分析(第 5-7 章),到两大 SDK 的实现解读(第 8-11 章),到传输层的多重选择(第 12-14 章),到安全与授权(第 15 章),到发现与注册(第 16 章),到反向通道与配置管理(第 17-18 章),到真实产品中的落地(第 19 章),再到亲手构建 Server(第 20 章)——我们积累了足够的材料来做一件更深层的事:
识别贯穿整个协议的设计模式。
这些模式不是凭空创造的规则,而是从源码、规范和实践中反复出现的决策逻辑中提炼出来的。它们回答的不是”MCP 做了什么”,而是”MCP 为什么这样做,而不是别样做”。
mindmap
root((MCP 六大设计模式))
协议层
能力协商<br/>解决<兼容性>张力
版本演进策略<br/>解决<稳定与演进>张力
传输抽象<br/>解决<解耦与实用>张力
语义层
三原语模型<br/>解决<灵活与标准>张力
控制平面分离
安全层
安全即默认<br/>解决<开放与安全>张力
工程层
渐进式特性暴露<br/>解决<强大与易用>张力
21.1.3 阅读本章的方式
每个模式按以下结构解析:
- 根本张力——该模式试图调和哪两个对立目标
- 模式描述——具体是怎么做的
- 在 MCP 中的体现——全书哪些章节、哪些 API 是它的实例
- 可迁移的洞察——离开 MCP 后,这个模式还能用到哪里
这个四段式让你可以跳读:只看”张力”部分就能快速把握;想深入就读”体现”章节;想迁移到自己项目就看”洞察”。
21.2 模式一:能力协商(Capability Negotiation)
21.2.1 解决的张力
协议需要稳定(Client 和 Server 可能由不同团队在不同时间开发),但功能需要演进(新特性不断被添加)。如何让新旧版本、功能残缺版和功能齐全版在同一张网里共存?
如果用简单的”版本号”来解决——1.0、1.1、2.0——立刻会撞上三堵墙:
- 维度坍塌:Server A 实现了 Tools 和 Resources 但不支持 Completion,Server B 实现了 Tools 和 Completion 但不支持 Resources。他们都是 “1.0 兼容”还是”1.1 兼容”?版本号没法描述这种部分实现。
- 升级锁步:任何新特性需要升版本号,所有 Client/Server 都要跟着升——在一个百万级实现的生态里这是灾难。
- 回归恐怖:一个 Client 想用版本 2.0 的新特性,但只能和支持 2.0 的 Server 通信,用户就骂”为什么我的老 Server 不兼容”。
21.2.2 模式描述
MCP 的初始化握手不仅是”我是谁”的自我介绍,更是”我能做什么”的能力宣告。Client 和 Server 各自声明自己支持的特性集合,双方在交集内工作:
graph LR
subgraph "Client 能力"
CR["roots"]
CE["elicitation<br/>(form + url)"]
CS["sampling"]
end
subgraph "Server 能力"
ST["tools"]
SR["resources<br/>(subscribe)"]
SP["prompts"]
SL["logging"]
SC["completions"]
end
subgraph "协商结果"
N1["Client 提供 roots 信息 → Server 可查询"]
N2["Client 支持 elicitation → Server 可提问"]
N3["Server 提供 tools → Client 可调用"]
N4["Server 支持 logging → Client 可设置级别"]
end
CR --> N1
CE --> N2
ST --> N3
SL --> N4
style CR fill:#3b82f6,color:#fff,stroke:none
style CE fill:#3b82f6,color:#fff,stroke:none
style CS fill:#3b82f6,color:#fff,stroke:none
style ST fill:#10b981,color:#fff,stroke:none
style SR fill:#10b981,color:#fff,stroke:none
style SP fill:#10b981,color:#fff,stroke:none
style SL fill:#10b981,color:#fff,stroke:none
style SC fill:#10b981,color:#fff,stroke:none
21.2.3 在 MCP 中的体现
这个模式贯穿全书多个章节:
- 第 4 章(生命周期):
initialize请求和响应中的capabilities字段是整个协商的起点 - 第 5 章(Tools):Server 声明
tools能力后,Client 才能调用tools/list和tools/call - 第 6 章(Resources):
resources.subscribe是可选的子能力,Client 只在 Server 声明时才订阅 - 第 17 章(Sampling):Server 必须检查 Client 是否声明了
sampling能力 - 第 18 章(Elicitation):精细到区分
form和url两种子模式的声明
SDK 中 enforceStrictCapabilities 选项控制是否严格执行能力约束——开发阶段可以关闭以便调试,生产环境必须开启。
21.2.4 能力协商的三层嵌套
MCP 的能力协商不止一层,而是能力 → 子能力 → 参数的三层嵌套:
{
"capabilities": {
"elicitation": { // 第一层:顶层能力
"form": {}, // 第二层:子模式
"url": { // 第二层:另一个子模式
"timeout": 600 // 第三层:子模式的参数
}
}
}
}
这种嵌套让协议能力粒度极细——同一个顶层能力下,可以逐步添加子特性而不破坏协商。
21.2.5 可迁移的洞察
能力协商模式适用于任何需要渐进式兼容的系统。它的核心智慧是:不要假设对方的能力,问了再用。这比版本号更灵活——版本号是线性的,能力集合是多维的。
对比可见的同类实现:
| 系统 | 能力协商机制 |
|---|---|
| HTTP/2 | SETTINGS 帧:窗口大小、头表大小、最大并发流 |
| TLS 1.3 | ClientHello 的 supported_versions / signature_algorithms 扩展 |
| WebSocket | Sec-WebSocket-Protocol、Sec-WebSocket-Extensions |
| LSP(语言服务器) | initialize 响应中的 serverCapabilities |
| OAuth 2.0 Metadata Discovery | .well-known/oauth-authorization-server 中的 grant_types_supported |
| gRPC | Deadline、Metadata、Compression 多层协商 |
| MCP | initialize 的 capabilities 字段(三层嵌套) |
能看到,几乎所有成功的协议都选择了能力协商而非硬版本号。这不是偶然。
21.3 模式二:传输抽象(Transport Abstraction)
21.3.1 解决的张力
协议逻辑应该与通信方式解耦(同样的 Tool 定义,不应因为从 stdio 切换到 HTTP 而改变),但不同部署场景需要不同的传输特性(本地进程用 stdio 更简单,远程服务需要 HTTP 的鉴权和重连)。
21.3.2 模式描述
MCP 定义了一个极简的 Transport 接口——只有发送消息、接收消息、关闭连接三个操作——然后在此之上构建了所有协议逻辑。具体的传输实现是可插拔的:
// TypeScript SDK 的 Transport 接口(简化)
interface Transport {
start(): Promise<void>;
send(message: JSONRPCMessage): Promise<void>;
close(): Promise<void>;
onmessage?: (message: JSONRPCMessage) => void;
onclose?: () => void;
onerror?: (error: Error) => void;
}
任何实现了这个接口的对象都可以作为传输层——这意味着你可以在 Electron IPC、gRPC、甚至蓝牙上运行 MCP 协议,而无需修改任何上层代码。Protocol 类只调用 transport.send() 和监听 transport.onmessage,完全不知道消息是通过 stdin 还是 HTTP 传输的。
21.3.3 在 MCP 中的体现
- 第 12 章(stdio):最简单的传输,一行命令启动 Server,通过标准输入输出通信
- 第 13 章(Streamable HTTP):支持无状态部署、会话管理、SSE 推送的现代 HTTP 传输
- 第 14 章(SSE + WebSocket):补充传输方式,各有适用场景
- 第 20 章(构建 Server):同一套 Server 代码通过切换传输层支持本地和远程两种部署模式
21.3.4 传输特性差异矩阵
各传输层的实际差异:
| 维度 | stdio | Streamable HTTP | SSE | WebSocket |
|---|---|---|---|---|
| 部署形态 | 本地子进程 | 远程 HTTP 服务 | 远程 HTTP 服务 | 远程 HTTP 服务 |
| 启动成本 | 进程 fork(~10ms) | TCP+TLS 握手(~100ms) | 同 HTTP | 同 HTTP |
| 会话维持 | 进程生命周期 | 显式 session | 单连接 | 单连接 |
| Server → Client 推送 | 原生(stdout) | SSE 子通道 | 原生 | 原生 |
| 扩展性 | 单实例 | 可水平扩展(配合会话存储) | 较差(每连接绑实例) | 较差 |
| 鉴权 | 进程继承环境 | OAuth 2.1 | 同 HTTP | 同 HTTP |
| 调试工具 | 简单(终端) | 成熟(curl/Postman) | 浏览器 DevTools | 专用工具 |
| 典型延迟 | <1ms | 10-100ms | 10-100ms | 10-100ms |
同一套 Server 代码可以对接这四种传输,只要传输实现了 Transport 接口。这是分层纪律的胜利。
21.3.5 可迁移的洞察
传输抽象的核心启示是分层的纪律:协议层只依赖抽象的消息收发接口,永远不引用具体的传输细节。这需要在设计之初就明确分层边界,之后严格执行。
一旦协议层”泄露”了对特定传输的假设(比如依赖 HTTP 的 Header 机制),抽象就会被破坏。LSP(Language Server Protocol)采用了同样的设计,这不是巧合——MCP 的设计者明确将 LSP 视为先驱。
反面教训:SMB/CIFS 协议早期紧耦合于 NetBIOS over TCP,导致后来想在现代 TCP/IP 网络上直接跑时,要额外搞一层 NBT(NetBIOS over TCP/IP)适配层——至今仍在背这个包袱。MCP 从第一天就定义清楚 Transport 接口,避免了这种”历史债务”。
21.4 模式三:三原语模型(Three-Primitive Model)
21.4.1 解决的张力
AI Agent 需要灵活地与外部世界交互(读数据、执行操作、获取指导),但交互模式必须标准化(否则每个 Server 都自创接口,生态无法形成)。
更深层的张力是:同样是”获取数据”,由谁决定何时获取,安全模型完全不同。
21.4.2 模式描述
MCP 将 Server 能提供的一切抽象为三种原语,区分标准不是数据类型,而是谁控制何时使用:
| 原语 | 隐喻 | 控制方 | 典型交互 | 安全模型 |
|---|---|---|---|---|
| Tools | 手(执行能力) | LLM 决定调用 | tools/call | 需要权限检查 + 用户确认 |
| Resources | 眼(感知能力) | 应用程序控制 | resources/read | 应用已经决定加载,不需额外权限 |
| Prompts | 脑(知识模板) | 用户选择 | prompts/get | 用户主动触发,最高信任 |
控制方的不同直接决定了安全模型:
- Tools 需要权限检查——LLM 的决策是概率性的,可能误调用
- Resources 不需要额外权限——应用程序自己选择加载什么
- Prompts 需要用户确认——用户主动触发的操作
21.4.3 在 MCP 中的体现
- 第 5 章(Tools):工具是 MCP 最活跃的原语,从简单的 API 调用到复杂的多步操作
- 第 6 章(Resources):资源提供了声明式的数据访问,支持订阅和变更通知
- 第 7 章(Prompts):提示模板定义了可复用的对话结构,引导 LLM 的行为
- 第 20 章(构建 Server):实战中三者的协同——Prompt 引导 LLM,LLM 决定调用 Tool,Tool 执行中读取 Resource
21.4.4 三原语的”否”问题
经常有人问:“为什么不能只要 Tool,不要 Resource 和 Prompt?”
从纯功能上说确实可以——Resource 可以用”只读 Tool”实现,Prompt 可以用”带参数的 Tool”实现。但这样做会破坏三件事:
- 安全模型统一化:如果一切都是 Tool,Client 必须对每次调用都弹用户确认,体验崩坏
- LLM 的选择效率:LLM 在数十个 Tool 里选哪个调用已经很困难,再混入数百个”只读 Tool”会把选择空间炸穿
- 语义透明度:Client UI 无法再给用户展示”这个 Server 提供 3 个只读资源 + 5 个可调用工具”这样的清晰视图
所以三原语的存在不是”冗余”,而是把控制权维度显式化。不区分它们,代价不仅在协议层,更在用户体验和 LLM 使用效率层。
21.4.5 可迁移的洞察
三原语模型的深层智慧是按控制权分类,而非按数据类型分类。在设计任何 Agent 交互框架时,区分”谁决定何时使用”比区分”这是什么类型的数据”更有架构价值。数据结构可以相同(三个原语都返回 content 数组),但控制语义的差异决定了完全不同的安全和交互模型。
这个思路可以推广到所有需要”受控暴露能力”的系统:
- 浏览器:Permission(用户授权)、Capability(安全上下文)、API(开发者控制)三层
- 移动 OS:前台 Activity(用户控制)、Service(应用控制)、ContentProvider(跨应用约定)三类组件
- 数据库:DDL(DBA 控制)、DML(应用控制)、DQL(查询方发起)三层
“按控制权而非数据类型分层”是一种成熟的架构品味。
21.5 模式四:渐进式特性暴露(Progressive Feature Disclosure)
21.5.1 解决的张力
协议功能丰富(Elicitation、Sampling、Progress、Completion 等),但入门门槛应该低(一个最简单的 Server 应该几十行代码就能写出来)。
21.5.2 模式描述
MCP 的每个特性都是可选的。一个 Server 可以只暴露一个 Tool,不支持 Resources、不支持 Prompts、不支持 Completion、不支持 Logging——它仍然是一个完全合法的 MCP Server。随着需求增长,开发者可以逐步添加更多特性:
graph TB
L1["Level 1: 最小可用<br/>一个 Tool + stdio 传输"] --> L2["Level 2: 丰富交互<br/>+ Resources + Prompts"]
L2 --> L3["Level 3: 智能补全<br/>+ Completion + Tool Annotations"]
L3 --> L4["Level 4: 操作反馈<br/>+ Progress + Logging"]
L4 --> L5["Level 5: 反向通道<br/>+ Elicitation + Sampling"]
L5 --> L6["Level 6: 远程部署<br/>+ HTTP 传输 + OAuth"]
style L1 fill:#10b981,color:#fff,stroke:none
style L2 fill:#3b82f6,color:#fff,stroke:none
style L3 fill:#6366f1,color:#fff,stroke:none
style L4 fill:#8b5cf6,color:#fff,stroke:none
style L5 fill:#ec4899,color:#fff,stroke:none
style L6 fill:#f59e0b,color:#fff,stroke:none
每一级都是可生产的——你不必一路爬到顶再发布。实际上绝大多数生产 MCP Server 停留在 Level 2-3 就已经很有价值。
21.5.3 在 MCP 中的体现
- 第 8 章(TypeScript Server SDK):
McpServer高级 API 让创建基本 Server 只需十几行代码 - 第 10 章(Python Server SDK):
@server.tool()装饰器让工具定义简洁到极致 - 第 18 章(Elicitation/Roots):这些高级特性只在 Server 需要时才涉及
- 第 20 章(构建 Server):从最简开始,逐步添加 Completion、Progress 等特性
21.5.4 渐进式暴露 vs 钩子膨胀
有一个反模式叫**“钩子膨胀”**:框架为了”灵活”,暴露了几十个 Hook(before/after/aroundXYZ),开发者被迫理解全部钩子才能写一个最简单的 Server。早期的 Enterprise Java、某些重型 IoC 容器都患有这个病。
渐进式特性暴露的正确姿势:
- 有一个”最小心智模型”:10 分钟就能读完文档入门
- 高级特性在自己的文档章节:不看不影响用
- API 的默认行为即合理行为:不用配置就能跑
- 每个特性独立可开关:不产生横向耦合
MCP 做到了这四点。对比一下:SOAP 协议把所有特性堆在一个 WSDL 里,写一个 Hello World 都要 200 行 XML——这就是渐进式暴露的反例。
21.5.5 可迁移的洞察
渐进式暴露的关键是正交性——每个特性应该独立于其他特性工作。如果添加 Completion 支持要求同时实现 Resources,那就不是渐进式的。MCP 做到了几乎完全的正交性:任何特性组合都是合法的。
这对协议和框架设计的启示是:在添加新特性时,始终问**“这个特性是否可以独立于其他所有特性使用?”** 如果答案是否,就要重新设计依赖关系。
21.6 模式五:安全即默认(Security by Default)
21.6.1 解决的张力
开放性是生态繁荣的前提(任何人都能写 Server),但安全是用户信任的基础(恶意或有缺陷的 Server 不应该损害用户)。
21.6.2 模式描述
MCP 在协议层面内建了多层安全机制,而不是把安全作为”可选的最佳实践”留给实现者:
- 人在回路(Human-in-the-loop):工具调用默认需要用户确认,Elicitation URL 需要用户同意
- 最小权限原则:能力协商确保只有声明支持的特性才会被使用
- 安全分级:Form Mode 禁止收集密码和 API 密钥,URL Mode 确保敏感数据不经过 Client
- 传输安全:远程部署强制 OAuth 2.1 + PKCE,Host Header 验证防止 DNS 重绑定
- 信息隔离:Server 默认看不到对话历史,不能访问其他 Server 的数据
- 环境隔离:stdio 传输默认只继承白名单环境变量,防止凭证泄露
21.6.3 在 MCP 中的体现
- 第 5 章(Tools):Tool Annotations(
readOnlyHint、destructiveHint)帮助 Client 做权限决策 - 第 15 章(OAuth):MCP 授权规范基于 OAuth 2.1,强制 PKCE,禁止隐式授权流
- 第 18 章(Elicitation):Form Mode 禁止收集敏感信息,URL Mode 必须在安全浏览器中打开
- 第 19 章(Claude Code):MCP 工具与内置工具冲突时内置优先,防止第三方覆盖核心功能
21.6.4 “默认安全”的四个等级
根据”让用户做正确的事有多容易”的不同,安全机制可以分四级:
| 等级 | 名字 | 特征 | MCP 中的例子 |
|---|---|---|---|
| L1 | 不安全是默认 | 需要配置才能安全 | (MCP 没有这种设计) |
| L2 | 安全是可选 | 默认不安全,文档提醒你开 | (MCP 没有这种设计) |
| L3 | 安全是默认 | 默认安全,可以手动关闭 | stdio 环境变量白名单 |
| L4 | 安全不可关 | 协议强制,无法绕过 | URL Mode 禁止预认证 URL |
MCP 把安全机制大多数放在 L3-L4——默认就是安全的,且绕过它要付出代价。
21.6.5 可迁移的洞察
安全即默认意味着不安全的行为应该比安全的行为更难实现。在 MCP 中,发送敏感数据通过 Form Mode 是被规范禁止的,你必须使用更复杂但更安全的 URL Mode——增加的摩擦是有意为之的。
这个理念可以这样概括:让做正确的事比做错误的事更容易(Make the right thing easy, and the wrong thing hard)。
现实中同类思想:
- Rust 的借用检查器——默认安全,需要
unsafe才能突破 - HTTPS Everywhere——浏览器默认 HTTPS,HTTP 要点”继续”
- CSP(Content Security Policy)——严格模式下内联脚本默认禁止
- iOS 沙箱——App 默认只能访问自己的数据,跨 App 访问要走 Intent
这些都是”安全即默认”的模式实例。
21.7 模式六:协议版本策略(Protocol Versioning Strategy)
21.7.1 解决的张力
协议需要演进(新功能、修复设计缺陷),但已部署的实现不能被破坏(百万级 Client 和 Server 不可能同时升级)。
21.7.2 模式描述
MCP 使用日期格式的版本标识(如 2025-06-18、2025-11-25),而非语义化版本号。初始化时 Client 提出支持的版本,Server 响应实际使用的版本。如果不兼容,连接直接失败——这是有意的设计:宁可连接失败,也不要在不兼容的状态下运行。
21.7.3 日期版本 vs SemVer 的对比
| SemVer(如 1.2.3) | 日期版本(如 2025-11-25) | |
|---|---|---|
| 可比性 | 需要约定比较规则 | 天然按时间序 |
| ”补丁”概念 | 有 | 无(每版本都是完整规范) |
| 什么叫 Breaking | 主版本+1 | 直接发新日期 |
| 争论焦点 | ”这算 major 还是 minor” | 无——所有变更都是新版本 |
| 实现同步 | 需要 major 严格锁步 | 能力协商解决兼容 |
| 对开发者直观度 | 需要理解 major/minor | 直接看”2025 年 6 月版” |
MCP 协议没有”补丁”的概念——每个版本都是完整规范。这决定了日期版本比 SemVer 更合适。
21.7.4 与能力协商的配合
版本号 + 能力协商构成了双重兼容性策略:
- 版本号处理宏观的协议演进(消息格式变化、新方法添加)
- 能力协商处理微观的特性差异(同一版本内,不同实现支持不同特性)
这就像 HTTP 版本(1.1、2、3)与 HTTP Headers 的关系:版本决定了基本的通信规则,Headers 协商具体的行为。两个层次各司其职,互不干扰。
21.8 模式间的协同
这六个模式不是孤立的。它们形成了一个互相强化的系统:
graph TB
CN["能力协商"] --> |"决定可用特性"| PFD["渐进式暴露"]
CN --> |"确保安全特性生效"| SBD["安全即默认"]
TA["传输抽象"] --> |"任何传输上工作"| TPM["三原语模型"]
TA --> |"HTTP 传输启用"| SBD
TPM --> |"按控制权分层"| PFD
PFD --> |"每层独立演进"| PVS["版本策略"]
SBD --> |"安全约束纳入规范"| PVS
PVS --> |"新版本添加新能力"| CN
style CN fill:#3b82f6,color:#fff,stroke:none
style TA fill:#8b5cf6,color:#fff,stroke:none
style TPM fill:#ec4899,color:#fff,stroke:none
style PFD fill:#10b981,color:#fff,stroke:none
style SBD fill:#f59e0b,color:#fff,stroke:none
style PVS fill:#6366f1,color:#fff,stroke:none
考虑一个具体的例子:当 MCP 在 2025-11-25 版本中引入 URL Mode Elicitation 时——
- 版本策略让旧 Client 可以安全地拒绝新版本的连接
- 能力协商让新 Client 可以声明是否支持
url模式 - 安全即默认要求敏感数据必须通过 URL Mode,不能走 Form Mode
- 渐进式暴露确保不需要 URL Mode 的 Server 完全不受影响
- 传输抽象保证这个特性在 stdio 和 HTTP 上都能工作
- 三原语模型中,URL Elicitation 自然地嵌入到 Tool 执行流程中
六个模式像齿轮一样咬合,每个都让其他模式更有效。这就是成熟协议的标志——不是”有多少特性”,而是”特性之间有多融洽”。
21.9 MCP 的设计权衡
没有完美的设计。每个模式背后都有权衡,诚实地记录这些权衡是本章的责任。
21.9.1 能力协商的代价
初始化握手增加了首次连接的延迟。每个 Client 和 Server 都需要维护能力映射表。但这个代价换来了无需版本锁步升级的自由度——对于一个开放生态来说,这是值得的。
可量化的代价:initialize 请求/响应的 RTT(stdio 上约 1ms,HTTP 上约 50-100ms),以及约 1KB 的能力声明数据。对于长连接场景可以忽略,对于冷启动场景可用预热或 keep-alive 缓解。
21.9.2 三原语的边界模糊
在实践中,Tool 和 Resource 的边界有时不清晰。“获取文件内容”是 Resource 还是 Tool?
MCP 规范的回答是看控制方——如果是应用程序自动加载上下文,用 Resource;如果是 LLM 决定需要获取,用 Tool。这个区分在概念上清晰,但在实现中需要开发者做出判断。
实践建议:
| 场景 | 选 Resource | 选 Tool |
|---|---|---|
| 数据集合固定、提前知道 URI | ✓ | |
| 数据依赖 LLM 推理后才能确定 URI 或参数 | ✓ | |
| Client 要展示”所有可用的 X” | ✓ | |
| 操作有副作用(哪怕是读缓存也 mutate) | ✓ |
21.9.3 安全的摩擦
安全机制增加了开发复杂度。一个简单的 Server 如果需要收集 API 密钥,必须实现 URL Mode Elicitation 的完整流程——包括 HTTPS 页面、用户身份验证、回调处理。对于小型项目来说,这可能显得过重。但协议设计者做出了明确的取舍:宁可让少数开发者多写代码,也不让多数用户面临安全风险。
这条”摩擦线”画在哪里,体现了协议作者的价值判断——MCP 明显站在用户一侧。
21.9.4 JSON-RPC 的局限
MCP 选择 JSON-RPC 2.0 作为基础,获得了简单性和广泛的语言支持,但也继承了它的限制:
- 没有原生的流式传输——需要 SSE 补充
- 没有二进制消息——大文件传输效率较低(base64 膨胀 33%)
- 没有内建的请求多路复用——高并发下需要应用层管理
这些限制通过传输层的扩展来弥补,但增加了整体复杂度。如果重新设计,是否会选 Protocol Buffers + gRPC?社区讨论中有此声音,但主流意见是:JSON 的人类可读性 + 开箱即用的调试能力在 AI Agent 场景(本来就以文本为主)中胜过了性能考量。
21.10 全书架构决策总结
把前 20 章分散的设计决策汇总成一张表,作为本书的”架构决策卡片”:
| 决策 | 选择 | 放弃的方案 | 理由 | 相关章节 |
|---|---|---|---|---|
| 消息格式 | JSON-RPC 2.0 | REST, gRPC, 自定义 | 简单、双向、人类可读 | 第 3 章 |
| 版本号格式 | 日期 (2025-11-25) | semver | 避免兼容性歧义 | 第 4 章 |
| 交互原语 | Tool/Resource/Prompt | 统一 Action 模型 | 按控制权分类更精确 | 第 5-7 章 |
| 本地传输 | stdio | Unix Socket, Named Pipe | 跨平台、零配置 | 第 12 章 |
| 远程传输 | Streamable HTTP | REST+WebSocket | 单连接、渐进增强 | 第 13 章 |
| 认证 | OAuth 2.1 + PKCE | API Key, mTLS | 无需预存关系、支持作用域 | 第 15 章 |
| Schema 验证 | JSON Schema 2020-12 | TypeScript 类型、Protobuf | 语言无关、工具丰富 | 第 5 章 |
| Server 发现 | 配置文件 + OAuth 发现 | DNS-SD, mDNS | 简单、显式、安全 | 第 16 章 |
| 敏感信息收集 | URL Mode Elicitation | Form Mode 或 Prompt 注入 | 数据不经 Client/LLM | 第 18 章 |
| 反向请求关联 | 绑定到当前正向请求 | 独立异步通道 | 传输层兼容性 | 第 18 章 |
| Token 存储 | Server 端会话绑定 | Client 传递 | 防 Token 泄露到 LLM | 第 15 章 |
| Sampling 鉴权 | 用户确认每次 | Token/API Key | 防滥用 LLM 费用 | 第 17 章 |
21.11 面向未来的思考
MCP 的设计模式揭示了几个面向未来的方向:
21.11.1 Agent-to-Agent 通信
当前 MCP 是 Client-Server 模型,但 AI Agent 生态正在走向 Agent 间直接协作。MCP 的传输抽象和能力协商机制可以自然地扩展到 Agent-to-Agent 场景——只要把”Agent”也视为一个 Peer,双方在 initialize 时协商对等能力即可。
已经出现的实验包括 Google A2A 协议、Anthropic 的 Agent SDK,它们或多或少借鉴了 MCP 的设计模式。
21.11.2 多模态原语
当前的三原语模型主要处理文本和简单的二进制数据。随着多模态 AI 的发展,视频流、实时音频、3D 场景等数据类型可能需要新的原语或现有原语的扩展。
一个可能的演进方向是在三原语上增加”Stream”原语——专门处理连续的时序数据,但这会触碰到 JSON-RPC 本身的边界,可能需要传输层的深度修改。
21.11.3 去中心化发现
当前的 Server 发现依赖配置文件或中央注册表。未来可能出现基于 DNS-SD、mDNS 或区块链的去中心化发现机制——类似于 IPFS 对静态内容做的事,应用到 Agent Service Discovery 上。
21.11.4 长生命周期任务
MCP 的 Progress 和 Cancellation 机制为长操作提供了基本支持,但随着 Agent 执行越来越复杂的任务(几小时甚至几天),可能需要更完善的任务管理原语——MCP 规范中已出现的实验性 Tasks 特性就是这个方向的探索。
Tasks 特性的核心思路:把长操作从 “request-response” 模式升级为 “fire-poll-retrieve” 模式,类似于 AWS Step Functions 的异步执行语义。
21.12 全书总结
MCP 不只是一个协议规范——它是一套关于如何让 AI 系统安全、可靠、可扩展地与外部世界交互的设计思想。
从第 1 章到第 21 章,我们完成了一次完整的旅程:
- 为什么:AI Agent 需要标准化的外部交互协议(第 1 章)
- 是什么:基于 JSON-RPC 的 Client-Server 架构,三大原语,能力协商(第 2-7 章)
- 怎么实现:TypeScript 和 Python SDK 的深度解读(第 8-11 章)
- 怎么通信:从 stdio 到 Streamable HTTP,传输层的选择与权衡(第 12-14 章)
- 怎么安全:OAuth 授权、Elicitation 安全隔离、工具注解(第 15、18 章)
- 怎么发现:配置文件、注册表、动态发现(第 16 章)
- 怎么协作:Sampling、Elicitation、Progress 等反向通道(第 17-18 章)
- 怎么落地:Claude Code 的 12 万行实战经验(第 19 章)
- 怎么构建:从零到生产级 Server 的完整开发流程(第 20 章)
- 为什么这样:六大设计模式与架构决策(本章)
MCP 协议仍在快速演进。但本书覆盖的核心概念和设计模式是稳定的——它们不会随版本更新而失效,因为它们反映的是更深层的系统设计智慧。理解了这些模式,你不仅能用好 MCP,更能在面对下一个协议设计问题时,做出更好的架构决策。
模型是引擎,协议是道路。好的道路不限制车速,但确保每辆车都到达正确的目的地。
全书源码参考
- MCP 规范:
https://github.com/modelcontextprotocol/specification - TypeScript SDK:
https://github.com/modelcontextprotocol/typescript-sdk - Python SDK:
https://github.com/modelcontextprotocol/python-sdk
延伸阅读:协议演化的长河
协议是计算机科学里最古老的抽象之一——1969 年 ARPANET 的 NCP、1974 年 TCP/IP、1983 年 DNS、1991 年 HTTP、1997 年 TLS、2014 年 HTTP/2、2021 年 HTTP/3、2024 年 MCP——每一次协议的诞生,都是对某一类分布式协作问题的系统性回答。协议一旦被广泛采用,就会深刻塑造整个行业的形态。
观察这条演化长河,可以提炼几条恒久的设计原则。简单比复杂更持久——HTTP/0.9 几乎只有 GET 请求却活了三十年,ASN.1 设计得无所不包却几乎被弃用。可扩展比完美更重要——TCP 的保留字段后来承载了 ECN、MSS、Window Scaling;HTTP 的 Header 扩展能力催生出 WebDAV、CORS、HSTS。向后兼容是生死线——IPv6 到今天还没完全取代 IPv4,就是因为没法无痛升级;HTTP/2 能快速普及,就是因为升级路径透明。MCP 吸收了这些历史经验——简单的三原语、灵活的 capability 协商、严格的版本策略,这是它能被 Anthropic、OpenAI、Google 等厂商快速采纳的原因。
延伸阅读:协议 vs 框架——抽象层次的选择
AI 应用开发里,从低到高常见的抽象层次依次是 HTTP API(如 OpenAI Chat Completions)、协议(如 MCP)、框架(如 LangChain、LangGraph)、平台(如 Anthropic Console、OpenAI Platform)。HTTP API 最灵活但最重复;协议标准化了通信格式但不规定业务逻辑;框架提供业务抽象但绑定特定思路;平台提供端到端体验但锁定供应商。
MCP 选择”协议”这一层是刻意的抽象位置:既不像 HTTP API 那样要求每个开发者自己处理协议细节,也不像框架那样强加某种编程范式,更不像平台那样锁定到某家云服务。协议层让生态参与者(LLM 提供商、工具开发者、Agent 框架)能各自发挥又互相协作。《LangGraph 源码》第 1 章讨论过类似的”低层基础设施”定位,《OpenClaw 源码》第 9 章讨论的”可扩展的插件协议”也是同一思路。
延伸阅读:能力协商模式的普适性
MCP 的能力协商——客户端和服务端在建立连接时交换 capability 列表、只使用双方都支持的能力——在协议世界广泛出现:TLS 的 cipher suite 协商、HTTP 的 Content-Negotiation、GraphQL 的 schema introspection、TCP 的 options 字段都是同一思路。核心价值是”让新旧实现共存”——新特性被 capability 标记,老客户端不理解就跳过。
能力协商的反面是”版本硬切换”——所有客户端和服务端必须同时升级到新版本,否则无法通信。IPv4 → IPv6 就是硬切换的典型教训,因为无法渐进升级,花了 30 年才逐步替换。任何需要长期演化的系统,都应该优先采用能力协商而非版本硬切换。