MCP 协议深入:JSON-RPC 2.0 与消息语义

深入 MCP 协议底层:JSON-RPC 2.0 在 MCP 中的落地、请求/响应/通知三类消息、id 语义与错误码规范、消息帧与批量、MCP 方法命名空间(initialize/tools/call/resources/read)、协议能力协商(protocolVersion/capabilities)、服务器与客户端握手流程、扩展协议边界与版本演进。

1. JSON-RPC 2.0 在 MCP 中的角色

MCP(Model Context Protocol)的传输层传送的是 JSON-RPC 2.0 消息。理解 JSON-RPC 是读懂 MCP 一切交互的地基:为什么有的消息有 id、有的没有?为什么工具调用成功却返回 isError: true?协议怎么知道客户端支持哪些能力?这些答案都在 JSON-RPC 的消息语义与 MCP 的约定里。

1.1 JSON-RPC 的三类消息

JSON-RPC 2.0 定义三类消息,MCP 全部使用:

类型有无 id语义MCP 示例
请求(Request)有期待响应tools/call
响应(Response)有(对应请求)返回结果/错误工具结果
通知(Notification)无单向,无响应notifications/message
# 消息形状
# 请求: {"jsonrpc":"2.0","id":1,"method":"tools/call","params":{...}}
# 响应: {"jsonrpc":"2.0","id":1,"result":{...}}
# 错误: {"jsonrpc":"2.0","id":1,"error":{"code":-32601,"message":"..."}}
# 通知: {"jsonrpc":"2.0","method":"notifications/message","params":{...}}

1.2 id 的语义

id 把请求与响应配对。核心规则:

# 1) id 由发送方分配,响应必须带同一 id
# 2) id 类型: 数字或字符串(协议不强制类型,但客户端常递增数字)
# 3) 请求-响应一一对应: 一方发请求后,在收到同 id 响应前,可并发多个请求
# 4) 通知无 id: 发送后不期待任何回执(失败也静默)
# 陷阱: 客户端并发请求时若服务端乱序响应,靠 id 找回对应请求

2. 错误码规范

2.1 JSON-RPC 标准错误码

MCP 继承 JSON-RPC 的错误码约定,负数为协议级,正数为应用级:

错误码含义场景
-32700解析错误无效 JSON
-32600无效请求结构不符合 JSON-RPC
-32601方法不存在调用了未注册方法
-32602参数无效工具参数校验失败
-32603内部错误服务端异常
-32000~-32099服务端错误MCP 保留

2.2 MCP 的工具错误约定

工具调用在协议层"成功"(HTTP/传输成功),但业务上失败——用 isError 表达:

# 工具返回(JSON-RPC 成功 + isError 标记)
# {"jsonrpc":"2.0","id":2,"result":{
#   "content":[{"type":"text","text":"查询失败: 无权限"}],
#   "isError":true        # ← 业务失败,但协议层成功
# }}
# 语义: isError=true → AI 助手知道工具调用失败,可重试/放弃
# 对比: 协议层错误(error 字段)→ 传输/协议故障,客户端处理

2.3 错误信息的工程要求

# 1) 工具失败用 isError(业务语义),协议故障用 error(传输语义)
# 2) 错误信息面向"模型消费"(清晰、可行动),不堆内部堆栈
# 3) 错误码稳定可枚举,模型可据此分支
# 4) 别把敏感堆栈塞进错误文本(可能被展示/记录)

3. MCP 方法命名空间

3.1 方法名的分层

MCP 方法用"资源/动作"命名,按能力域分组:

# 生命周期
# initialize / initialized / notifications/initialized
# 工具
# tools/list / tools/call / notifications/tools/list_changed
# 资源
# resources/list / resources/read / notifications/resources/list_changed
# 提示词
# prompts/list / prompts/get
# 采样(服务器请求模型)
# sampling/createMessage
# 根(服务器读取客户端文件系统)
# roots/list
# 通用
# ping / notifications/message / logging/setLevel

3.2 方法名的语义

# 1) list/call 模式: tools/list 枚举、tools/call 执行(读/写分离)
# 2) 复数资源 + 动作: resources/read 读一个资源(传入 uri)
# 3) 通知命名: notifications/X 是服务器→客户端/客户端→服务器的异步信号
# 4) 扩展方法: 官方命名空间 + 厂商自定义(带域前缀,如 google-maps/tools/xxx)

3.3 方法的版本与兼容

# 方法集随 protocolVersion 演进
# 老版本客户端调新方法 → -32601(方法不存在)→ 客户端降级
# 兼容策略: 服务端按客户端声明的能力只暴露可用方法
# 扩展: 新能力用新方法,不破坏旧方法(向后兼容)

4. 握手与能力协商

4.1 initialize 握手流程

# 客户端 → 服务器: initialize(协议版本 + 客户端能力)
# 服务器 → 客户端: 响应(协议版本 + 服务器能力 + 服务器信息)
# 客户端 → 服务器: notifications/initialized(确认完成)
# 之后才可调用 tools/resources/prompts
# 关键: 未完成握手前,不能调用业务方法

4.2 capabilities 协商

客户端与服务器各自声明能力,对方据此适配:

// 客户端 initialize 声明
{
  "protocolVersion": "2024-11-05",
  "capabilities": { "roots": { "listChanged": true }, "sampling": {} },
  "clientInfo": { "name": "my-app", "version": "1.0.0" }
}
# capabilities 语义
# 1) 客户端能力: 我能提供什么(sampling、roots)
# 2) 服务器能力: 我支持什么(tools/resources/prompts)
# 3) 协商结果: 双方只在"都支持"的能力上协作
# 4) 服务器按客户端能力决定: 是否用 sampling、是否发特定通知

4.3 protocolVersion 的演化

# 版本: "2024-11-05" 早期 → "2025-06-18" 等后续
# 服务端处理多版本客户端:
# 1) 声明自己最高支持版本
# 2) 客户端选一个它支持的版本(可能低于服务端最高)
# 3) 服务端按选定版本行为(兼容旧语义)
# 原则: 服务端尽量向后兼容,别强制客户端升级

5. 请求-响应与并发

5.1 并发请求与乱序响应

# MCP 允许单连接上并发多个请求(id 区分)
# 客户端: 发多个 tools/call → 服务端可并行处理 → 按 id 收集
# 服务端实现要点
# 1) 每个请求独立 async 处理
# 2) 响应按完成顺序回(不必与请求顺序一致,靠 id 配对)
# 3) 注意并发上限(工具是外部资源,别无限并发)

5.2 超时与无响应

# 请求发出后无响应的情况
# 1) 传输断开 → 上层重连/重发
# 2) 服务端卡死 → 客户端超时(协议层无超时,由客户端/传输层实现)
# 3) 通知类消息永不期待响应(别等)
# 工程: 客户端对每个请求设超时,超时后按"可能已执行"处理(幂等设计)

5.3 幂等与重发

# 超时重发的陷阱
# tools/call 超时 → 重发 → 工具可能被执行两次
# 缓解
# 1) 工具参数带请求 id/幂等键
# 2) 服务端对幂等键去重
# 3) 只读工具天然安全;写工具必须幂等设计(见工具可靠性)

6. 通知与异步信号

6.1 三类通知

# 1) 服务器 → 客户端
#   notifications/tools/list_changed: 工具列表变了,客户端重新拉取
#   notifications/resources/list_changed
#   notifications/message: 服务器主动发消息(日志/进度)
# 2) 客户端 → 服务器
#   notifications/initialized: 握手完成
#   notifications/cancelled: 取消进行中的请求
# 3) 双向
#   notifications/progress: 进度上报(长任务)
# 通知无响应: 接收方处理后不回复(可靠性靠重新拉取/重试)

6.2 list_changed 的价值

# 服务器工具/资源"运行时变化"时发 list_changed
# 客户端收到 → 重新 tools/list → 更新可用工具集
# 用途
# - 服务器动态挂载新工具(插件/多后端)
# - 资源增删(外部文件/数据变化)
# 价值: 客户端工具列表始终新鲜,无需轮询

6.3 取消与进度

# notifications/cancelled: 客户端取消长任务(请求 id 携带)
# 服务端响应: 停止处理,尽量清理副作用
# notifications/progress: 长任务进度(0-100),AI 助手可展示
# 组合: 长工具调用 = 进度通知 + 可取消 + 幂等

7. 协议扩展与版本演进

7.1 自定义扩展的边界

# 扩展原则
# 1) 新方法用域名前缀,别污染官方命名空间
# 2) 扩展不改变既有方法语义(向后兼容)
# 3) 扩展方法在 capabilities 里声明(对方知道可用)
# 4) 未声明的能力=不可用,别让客户端试错
# 反例: 篡改 tools/call 的既有行为 → 破坏兼容

7.2 版本演进策略

# 服务端协议演进清单
# 1) 加能力: 新方法 + capabilities 声明(不破坏旧客户端)
# 2) 改语义: 走新 protocolVersion,旧版本保持旧行为
# 3) 弃用: 标记 deprecated,给迁移窗口,再移除
# 4) 测试: 用旧版本客户端回归(模拟老握手)

8. 协议实现的测试要点

# 1) 握手: initialize/initialized 全流程 + 能力协商断言
# 2) 三类消息: 请求/响应/通知的 id 与语义
# 3) 错误码: 参数无效、方法不存在、内部错误
# 4) isError vs error: 业务失败与协议故障分开断言
# 5) 并发: 多请求乱序响应的 id 配对
# 6) 通知: list_changed、progress、cancelled
# 7) 版本: 用旧 protocolVersion 客户端验证兼容
# 工具: InMemoryTransport 直接断言消息帧(不依赖真实进程)

9. 常见陷阱

  • 通知当请求用:给通知配了 id 等响应,或期待通知有回执——通知就是单向。
  • 错误码乱用:工具业务失败用了协议 error,客户端误判为传输故障。
  • 握手前调业务方法:未 initialize 就 tools/list,服务端拒绝。
  • 能力不协商:假设对方支持某能力,实际未声明,交互失败。
  • 忽略 isError:客户端只看 JSON-RPC 成功,忽略工具内失败标记。
  • 方法名冲突:自定义方法污染官方命名空间。

10. 总结

MCP 协议的核心是 JSON-RPC 2.0 的消息语义:请求-响应用 id 配对、通知单向无回执、错误码区分协议与业务、capabilities 协商能力边界。工程上最重要的是三件事:工具失败用 isError 而非协议错误、握手完成前不调业务方法、扩展走自定义命名空间并向后兼容。把协议层的语义吃透,MCP 服务器与客户端的协作就不会在"看起来成功、实际失败"的边界上栽跟头。

继续阅读

探索更多技术文章

浏览归档,发现更多关于系统设计、工具链和工程实践的内容。

全部文章 返回首页

「AI工程」更多文章

  1. MCP 发布与生态:让工具被更多人发现和使用
  2. MCP 浏览器与网页工具:让 Agent 操作真实网页
  3. MCP 记忆与持久化工具:让 Agent 拥有长期记忆