MCP协议规范:JSON-RPC 2.0标准说明

eve2333 发布于 19 小时前 6 次阅读


在 MCP 中,重点不在于某个业务接口是用 Spring MVC 还是 Dubbo 实现的,而在于:当一个 MCP Client 想调用某个工具能力时,客户端和服务端是否有一套统一、标准、可互操作的消息格式。

  • REST / Dubbo / gRPC:偏业务接口调用方式
  • JSON-RPC 2.0:偏协议消息封装规范
  • MCP:在 JSON-RPC 2.0 之上,进一步约定工具、资源、提示词等能力模型

它本身并不绑定具体传输层,既可以跑在 HTTP 之上,也可以跑在 WebSocket、stdio 等通道之上。只要客户端和服务端都遵守 JSON-RPC 2.0 的约定,就能够以一致的方式完成请求、响应和通知。

但它也有明确的边界:JSON-RPC 只定义消息格式与处理规则,并不负责传输层细节,也不包含鉴权、服务发现等工程能力。这些需要在网关、框架或系统规范里另外补齐。

从 Java 开发者的视角看,JSON-RPC 2.0 其实不复杂,核心无非就是几种固定的消息结构:

  • Request Object:发起一次调用,请求服务端执行某个方法
  • Notification:也是请求,但因为没有 id,所以不要求服务端回包
  • Response Object:服务端对请求的响应,里面要么是结果,要么是错误
  • Batch:把多个请求一起发出去,减少多次交互开销

除了消息结构,协议里还定义了两个交互角色:

  • Client:发送请求的一方
  • Server:接收请求并返回结果的一方

注意:同一个程序可以同时扮演 Client 和 Server。互相调用的时候可以互相交换角色

核心对象:Request、Notification、Response、Error

1. Request Object(请求对象)

一次 RPC 调用通过向服务端发送 Request Object 来表示。Request 对象包含以下字段。

jsonrpc(必需)必须精确等于 "2.0"

  • 作用:标识协议版本

method(必需)要调用的方法名

  • 约束:以 rpc. 开头的方法名是保留方法,用于 RPC 内部或扩展,不应作为业务方法名

params(可选)

  • 类型:Object 或 Array
  • 含义:方法调用参数
  • 可以省略,表示无参数调用

两种参数形式:

  • 按位置传参params 为 Array,例如 [42, 23]
  • 按名称传参params 为 Object,例如 {"minuend": 42, "subtrahend": 23}
    使用命名参数时,字段名必须与服务端期望的参数名完全匹配(包括大小写)

id(条件必需)

  • 类型:String、Number 或 Null
  • 作用:用于关联请求和响应

规则:

  • 如果 不存在 id 字段,该请求被视为 Notification
  • 服务端必须在 Response 中返回相同的 id

规范层面的建议:

  • 避免使用 Null 作为 id
  • Number 类型 不应使用小数

Request 示例:

{
  "jsonrpc": "2.0",
  "method": "getUserInfo",
  "params": {"userId": "12345"},
  "id": 1
}

2. Notification(通知)

Notification 是一种没有 id 字段的 Request。它表示客户端不期望收到任何响应。

  • 服务端 不得返回 JSON-RPC Response Object
  • 即使发生错误,也不会返回错误对象(客户端无法感知错误)

如果使用 HTTP 作为传输层:服务器仍然需要返回 HTTP 响应(例如 204 No Content),但 不会返回 JSON-RPC Response 对象

什么时候发送 Notification 请求?

当调用方不关心结果/错误,且不希望为这次调用付出一次响应的成本(等待、解析、重试、幂等等)时,用 Notification。比如:日志/埋点/旁路异步触发等,调用方只要“发出去”即可,不管任务结果或可以在后台处理。

3. Response Object(响应对象)

当服务端处理 Request 时,必须返回一个 Response Object,除非该请求是 Notification。Response 包含以下字段。

jsonrpc(必需)

  • 值:必须精确等于 "2.0"

result(成功时必需)

  • 含义:调用成功的返回值
  • 规则:成功时必须存在;失败时不得存在

error(失败时必需)

  • 含义:调用失败的错误信息
  • 规则:失败时必须存在;成功时不得存在
  • 值:必须是 Error Object

id(必需)用于关联请求和响应

4. Error Object(错误对象)

当 RPC 调用失败时,Response 必须包含 error 对象。Error Object 包含三个字段:

code(必需)

  • 必须是整数
  • 含义:错误类型

message(必需)

  • 含义:简短的人类可读错误描述

data(可选)

  • 含义:额外错误信息(调试/上下文等)

Batch(批处理)到底怎么用?

Batch 允许客户端一次发送多个 Request 对象,服务端批量处理后返回多个 Response。这样可以减少网络往返次数,提高吞吐量。但在实际实现中,Batch 也是 JSON-RPC 2.0 里最容易被实现错误的一部分

1. Batch 的基本规则

请求格式

  • 客户端可以发送一个 Array,其中包含多个 Request 对象
  • Array 中可以混合 普通 RequestNotification

响应格式

  • 服务端应返回一个 Array,包含对应的 Response 对象
  • 每个 Request 应对应一个 Response
  • Notification 不应该返回 Response
  • Response 的顺序 不要求与请求顺序一致
  • 客户端必须通过 id 字段来匹配请求与响应
  • 服务端可以并发处理 Batch 中的请求

特殊情况

  • 如果 Batch 不是有效 JSON,服务端必须返回 单个错误 Response(不是 Array)
  • 如果 Batch 是一个 空 Array,这也是无效请求,应返回单个 Invalid Request
  • 如果 Batch 中 没有任何需要返回 Response 的请求(例如全部是 Notification),服务端 不应返回 JSON-RPC 响应

常见实现错误(实践中最容易踩的坑)

1. 忘记包含 jsonrpc: "2.0"

2. Notification 返回了响应;Notification 不允许返回 JSON-RPC Response。正确做法:不返回 JSON-RPC 响应(HTTP 场景可返回 204)。

3. resulterror 同时存在;规范要求 两者只能存在一个

4. method 不是字符串;method 必须是字符串类型

5. params 必须是 Object 或 Array

6. 命名参数大小写不匹配

JSON 字段名 大小写敏感

7. Batch 全是 Notification 却返回 []

原因:如果 Batch 中 没有需要返回的 Response,规范要求 不返回 JSON-RPC 响应

  • HTTP 返回 204 No Content
  • 或返回空 body

8. 业务错误使用了协议错误码

-32603JSON-RPC 内部错误,不应该用于业务逻辑。

  • -32768 ~ -32000 用于 协议层错误
  • 业务错误使用 自定义错误码(例如 1000+)


1. 输入校验:不要相信任何客户端

规范只定义了协议格式,但没说怎么校验业务参数。实际项目中,你需要在服务端做多层校验:

协议层校验(必须做):

  • jsonrpc 字段是否存在且等于 "2.0"
  • method 字段是否存在且为 String 类型
  • params 字段(如果存在)是否为 Object 或 Array
  • id 字段(如果存在)是否为 String、Number 或 Null
  • Request 和 Response 的 result/error 互斥性

业务层校验(根据方法定义):

  • 参数类型是否正确(如 userId 应该是 String,age 应该是 Number)
  • 参数范围是否合法(如 age 应该在 0~150 之间)
  • 必填参数是否存在
  • 参数之间的逻辑关系(如 startDate 必须早于 endDate

安全层校验(防攻击):

  • 参数长度限制(防止超大 JSON 攻击)
  • 参数内容过滤(防止 SQL 注入、XSS)
  • 方法名白名单(防止调用内部方法)
  • 频率限制(防止暴力调用)

2. 超时与重试:网络不可靠

JSON-RPC 是无状态协议,不管传输层的可靠性。实际项目中,你需要自己处理超时和重试:

客户端超时策略

客户端重试策略

服务端超时处理

3. 幂等与去重:同一个请求别处理两次

客户端重试时,服务端可能收到重复请求。你需要根据 id 字段做去重:

去重策略

  • 用 Redis 或内存缓存存储最近处理过的请求 ID(如最近 5 分钟)
  • 收到请求时,先检查 id 是否已处理过
  • 如果已处理,直接返回缓存的响应(不重复执行)
  • 如果未处理,执行方法并缓存响应

注意事项

  • 只对有 id 的请求做去重,Notification 不需要去重(因为没有响应)
  • 去重窗口不要太长(如 5~10 分钟),避免内存占用过大
  • 去重 key 可以加上客户端标识(如 clientId:requestId),避免不同客户端的 id 冲突

4. 日志与链路追踪:出问题能快速定位

JSON-RPC 调用链路可能很长(客户端 → 网关 → 服务 A → 服务 B),你需要记录完整的调用链路:

日志记录内容

  • 请求 ID(id 字段)
  • 方法名(method 字段)
  • 参数(params 字段,敏感信息脱敏)
  • 响应结果(resulterror
  • 执行耗时
  • 客户端 IP、User-Agent
  • 调用链路 ID(Trace ID,用于关联多个服务的日志)

链路追踪

  • 在 HTTP Header 或 JSON-RPC 扩展字段中传递 Trace ID(如 X-Trace-Id
  • 每个服务收到请求时,从 Header 中提取 Trace ID,记录到日志
  • 调用下游服务时,把 Trace ID 传递下去
  • 用 ELK、Jaeger、Zipkin 等工具聚合日志,可视化调用链路

5. 鉴权与签名:不是谁都能调你的接口

JSON-RPC 协议本身不管鉴权,你需要在传输层或应用层加鉴权机制:

传输层鉴权(推荐):

  • 用 HTTPS + API Key(在 HTTP Header 中传递,如 Authorization: Bearer <token>
  • 用 HTTPS + JWT(在 HTTP Header 中传递,服务端验证签名和过期时间)
  • 用 mTLS(双向 TLS,客户端和服务端互相验证证书)

应用层鉴权(不推荐,但有时必须):

  • 在 JSON-RPC 请求中加鉴权字段(如 {"jsonrpc": "2.0", "method": "getUserInfo", "params": {...}, "auth": {"token": "..."}, "id": 1}
  • 服务端先验证 auth 字段,再执行方法
  • 缺点:鉴权逻辑和业务逻辑耦合,不符合分层设计

签名防篡改

  • 客户端用私钥对请求内容签名,放在 HTTP Header 或 JSON-RPC 扩展字段中
  • 服务端用公钥验证签名,确保请求未被篡改
  • 签名内容包括:请求体 + 时间戳 + nonce(防重放攻击)

6. 兼容性与灰度:新老版本共存

实际项目中,你可能需要同时支持多个版本的 API(如 v1、v2),或者灰度发布新功能:

版本管理策略

  • 在方法名中加版本号(如 getUserInfo_v1getUserInfo_v2
  • 在 HTTP 路径中加版本号(如 /api/v1/jsonrpc/api/v2/jsonrpc
  • 在 JSON-RPC 扩展字段中加版本号(如 {"jsonrpc": "2.0", "version": "v2", "method": "getUserInfo", ...}

灰度发布策略

  • 根据客户端标识(如 User-Agent、IP、用户 ID)路由到不同版本
  • 用特性开关(Feature Flag)控制新功能的开启/关闭
  • 监控新版本的错误率、响应时间,出问题快速回滚

7. 错误码映射:统一错误处理

JSON-RPC 预定义了 6 个错误码(-32700-32603),但实际项目中你需要更多错误码:

错误码分层

  • 协议层错误-32768-32000):解析错误、方法不存在、参数无效等,直接用规范预定义的错误码
  • 基础设施错误-32000-32099):数据库连接失败、Redis 超时、消息队列异常等,用规范保留的 Server error 范围
  • 业务错误(正数,如 1000~9999):用户不存在、余额不足、订单已关闭等,自定义错误码

错误码文档化

  • 维护一个错误码表格,包含:错误码、错误消息、含义、解决方案
  • 在 API 文档中公开错误码表格,方便客户端处理错误
  • 用枚举或常量定义错误码,避免硬编码

HTTP、gRPC 和 JSON-RPC 2.0 的定位差异

1. HTTP:传输协议,不是调用协议

HTTP 是一个传输层协议,它定义了:

  • 如何建立连接(TCP/TLS)
  • 如何发送请求(GET、POST、PUT、DELETE)
  • 如何返回响应(状态码、Header、Body)

但 HTTP 不定义方法名怎么表示?参数怎么传递?错误怎么表达?通知消息?这导致基于 HTTP 的 API 设计千差万别

2. gRPC:强类型 RPC 框架,但过于重量级

gRPC 是一个完整的 RPC 框架,它提供了:

  • 基于 Protocol Buffers 的强类型接口定义
  • HTTP/2 传输层
  • 流式调用支持
  • 多语言代码生成

gRPC 的优势在于性能和类型安全,但它也带来了额外的复杂度:

  1. 1.需要预先定义 .proto 文件:每个工具都要写 Protocol Buffers 定义,增加了开发成本
  2. 2.强依赖代码生成:客户端和服务端都需要生成代码,动态调用不方便
  3. 3.二进制协议不易调试:无法直接用 curl 或浏览器测试,必须用专门工具
  4. 4.HTTP/2 依赖:部分环境(如浏览器、某些代理)对 HTTP/2 支持不完善

对于 MCP 这种需要轻量、灵活、易于调试的场景,gRPC 显得过于重量级。

3. JSON-RPC 2.0:协议层标准,专注消息格式

JSON-RPC 2.0 的定位与 HTTP、gRPC 都不同,它是一个消息格式规范,只定义一次调用的请求结构;成功响应和错误响应的结构;通知消息的结构;不绑定传输层

MCP 选择 JSON-RPC 2.0 的核心原因

1. 统一的消息格式

请求,成功失败都是统一的,而且实现非常简单

都能按照完全相同的方式解析和构造消息。

2. 轻量且易于实现

JSON-RPC 2.0 的规范非常简洁(完整规范只有几页),核心概念只有 4 个:

  • Request Object
  • Response Object
  • Notification
  • Error Object

3. 人类可读,易于调试

JSON-RPC 2.0 使用纯文本 JSON 格式,可以直接测试;gRPC 的 Protocol Buffers 是二进制格式,必须用专门工具才能查看。

4. 传输层无关,灵活性高

JSON-RPC 2.0 不绑定传输层,可以适配所有本地远程实时场景。而 gRPC 强依赖 HTTP/2,无法用于 stdio 场景。

5. 支持通知消息(Notification)

MCP 中有些场景不需要响应,例如:日志上报,进度更新

JSON-RPC 2.0 原生支持 Notification(不带 id 的请求),服务端不会返回响应,减少了不必要的网络开销。

6. 批量调用支持

JSON-RPC 2.0 支持 Batch Request,可以一次发送多个请求:

JSON-RPC 2.0 的局限性

当然,JSON-RPC 2.0 也不是完美的,它有一些明确的局限性:

1. 不包含传输层细节

JSON-RPC 2.0 只定义消息格式,不管:

  • 如何建立连接(TCP?WebSocket?)
  • 如何处理超时和重试
  • 如何做负载均衡
  • 如何做服务发现

2. 不包含鉴权机制

JSON-RPC 2.0 不定义如何鉴权,需要在传输层(如 HTTP Header)或应用层(如在 params 中加 auth 字段)自行实现。

3. 不包含类型定义

JSON-RPC 2.0 不强制类型检查,参数和返回值都是动态的 JSON。这意味着:

  • 需要在运行时校验参数类型
  • 无法在编译期发现类型错误
  • 需要额外的文档或 Schema 定义(如 JSON Schema)