在 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 中可以混合 普通 Request 和 Notification
响应格式
- 服务端应返回一个 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. result 和 error 同时存在;规范要求 两者只能存在一个。
4. method 不是字符串;method 必须是字符串类型。
5. params 必须是 Object 或 Array。
6. 命名参数大小写不匹配
JSON 字段名 大小写敏感。
7. Batch 全是 Notification 却返回 []
原因:如果 Batch 中 没有需要返回的 Response,规范要求 不返回 JSON-RPC 响应。
- HTTP 返回
204 No Content - 或返回空 body
8. 业务错误使用了协议错误码
-32603 是 JSON-RPC 内部错误,不应该用于业务逻辑。
-32768 ~ -32000用于 协议层错误- 业务错误使用 自定义错误码(例如 1000+)
1. 输入校验:不要相信任何客户端
规范只定义了协议格式,但没说怎么校验业务参数。实际项目中,你需要在服务端做多层校验:
协议层校验(必须做):
jsonrpc字段是否存在且等于"2.0"method字段是否存在且为 String 类型params字段(如果存在)是否为 Object 或 Arrayid字段(如果存在)是否为 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字段,敏感信息脱敏) - 响应结果(
result或error) - 执行耗时
- 客户端 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_v1、getUserInfo_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.需要预先定义 .proto 文件:每个工具都要写 Protocol Buffers 定义,增加了开发成本
- 2.强依赖代码生成:客户端和服务端都需要生成代码,动态调用不方便
- 3.二进制协议不易调试:无法直接用 curl 或浏览器测试,必须用专门工具
- 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)

Comments NOTHING