摘要:MCP协议基于JSON-RPC 2.0进行通信,本文详解JSON-RPC消息格式、请求响应模型、批量调用、错误码定义,以及MCP如何在此基础上扩展出初始化握手和能力协商机制。
JSON-RPC 2.0 MCP通信的底层语言
我第一次抓包看MCP的通信内容时有点懵,消息里全是jsonrpc、method、params这些字段,看起来跟HTTP API完全不一样。后来才知道MCP底层用的是JSON-RPC 2.0协议,一个比我预想中更简洁的RPC规范。搞懂JSON-RPC 2.0之后再看MCP的消息流就豁然开朗了,所有工具调用、资源读取、能力协商的本质都是JSON-RPC消息的收发。这篇文章把JSON-RPC 2.0的规范和MCP的使用方式完整讲清楚,配上真实的消息示例。
JSON-RPC 2.0是什么
JSON-RPC 2.0是一个无状态的轻量级远程过程调用协议,2010年发布,用JSON作为数据格式。它的核心思想很简单,客户端发一个JSON对象描述要调用什么方法、传什么参数,服务端返回一个JSON对象包含执行结果。
它跟HTTP API的区别在于传输无关。JSON-RPC不绑定任何传输协议,可以在同一个进程内用,可以通过socket用,可以通过HTTP用,也可以通过标准输入输出用。MCP正是利用了这个特性,在stdio和Streamable HTTP两种传输上跑同一套JSON-RPC消息。
JSON-RPC 2.0跟1.0版本的区别也很好区分。2.0的消息里一定有个"jsonrpc": "2.0"字段,1.0没有。
三种消息格式
JSON-RPC 2.0定义了三种消息类型,请求、响应和通知。我逐个讲。
请求 Request
请求是客户端发给服务端的消息,期望得到响应。它有四个字段。
{"jsonrpc":"2.0","id":1,"method":"tools/call","params":{"name":"add_note","arguments":{"title":"测试笔记","content":"这是内容"}}}jsonrpc字段固定为"2.0",标识协议版本。id字段是客户端生成的唯一标识符,可以是数字或字符串,服务端响应时必须返回相同的id,客户端据此匹配请求和响应。method是要调用的方法名。params是方法参数,可以按位置传(数组)或按名称传(对象),MCP统一用按名称传。
id的值有几个注意事项。不建议用Null,因为Null在响应里表示无法识别请求id。不建议用带小数的数字,因为浮点数精度可能导致匹配失败。
响应 Response
响应是服务端对请求的回复。成功和失败有两种不同的结构。
成功响应。
{"jsonrpc":"2.0","id":1,"result":{"content":[{"type":"text","text":"笔记创建成功,ID为 1"}]}}错误响应。
{"jsonrpc":"2.0","id":1,"error":{"code":-32602,"message":"Invalid params","data":{"detail":"title字段不能为空"}}}响应的规则很严格。result和error字段不能同时出现,成功时只有result,失败时只有error。id必须跟请求的id一致。如果请求解析失败无法识别id,响应的id为Null。
错误对象有三个字段。code是整数错误码。message是简短错误描述。data是可选的附加信息,可以是任意类型。
通知 Notification
通知是一种特殊的请求,没有id字段,服务端不需要回复。
{"jsonrpc":"2.0","method":"notifications/initialized"}通知的用途是告诉对方某件事发生了,但不关心结果。MCP里大量使用通知,比如initialized通知、取消通知、进度通知、日志通知、列表变更通知。
通知的不可靠性在于没有响应,发送方无法知道对方是否收到或处理成功。如果你需要确认,就得用请求而不是通知。
错误码体系
JSON-RPC 2.0预留了-32768到-32000的范围给预定义错误。
| 错误码 | 名称 | 含义 |
|---|---|---|
| -32700 | Parse error | 服务端解析JSON文本失败 |
| -32600 | Invalid Request | 发送的JSON不是合法的请求对象 |
| -32601 | Method not found | 方法不存在或不可用 |
| -32602 | Invalid params | 方法参数无效 |
| -32603 | Internal error | 服务端内部错误 |
| -32000到-32099 | Server error | 预留给实现自定义的服务端错误 |
MCP在这个体系上扩展了自己的语义。比如-32602在MCP里经常出现,能力协商不匹配、参数格式错误、不支持的能力请求都会返回这个码。-32000到-32099的范围可以给MCP Server存自定义的业务错误码。
MCP还定义了一些特定的错误情况。协议版本不匹配时,Server返回-32602,data字段里带上支持的版本列表和请求的版本。这比单纯返回一个错误消息有用得多。
MCP如何使用JSON-RPC
MCP在JSON-RPC 2.0之上定义了自己的方法集和消息语义。我把MCP用到的方法分几类。
生命周期方法。initialize是握手请求,Client发给Server协商版本和能力。notifications/initialized是握手完成通知。这两个方法是MCP运行的基础。
工具方法。tools/list请求获取工具列表。tools/call请求调用某个工具。notifications/tools/list_changed通知告诉Client工具列表变了,让Client重新拉取。
资源方法。resources/list列出资源。resources/read读取资源内容。resources/subscribe订阅资源变更。resources/unsubscribe取消订阅。notifications/resources/updated通知资源已更新。
Prompt方法。prompts/list列出模板。prompts/get获取生成的消息。notifications/prompts/list_changed通知模板列表变更。
实用方法。ping心跳检测。logging/setLevel设置日志级别。
Client侧方法。sampling/createMessage由Server发请求让Client用LLM生成文本。elicitation/create由Server发请求让Client向用户提问。
所有这些方法的底层都是JSON-RPC的请求、响应或通知。MCP的精巧之处在于,它没有发明新的序列化格式或传输协议,完全复用JSON-RPC 2.0的消息结构,只是定义了method的命名空间和params的schema。
真实MCP通信消息示例
下面是一个完整的MCP通信流程,从握手到工具调用到关闭。我用笔记管理Server做例子,展示每一步的真实JSON-RPC消息。
// ========== 第1步 Client发送initialize请求 =========={"jsonrpc":"2.0","id":1,"method":"initialize","params":{"protocolVersion":"2025-06-18","capabilities":{"roots":{"listChanged":true},"sampling":{}},"clientInfo":{"name":"example-client","version":"1.0.0"}}}// ========== 第2步 Server返回initialize响应 =========={"jsonrpc":"2.0","id":1,"result":{"protocolVersion":"2025-06-18","capabilities":{"tools":{"listChanged":true},"resources":{"subscribe":true,"listChanged":true},"prompts":{"listChanged":true},"logging":{}},"serverInfo":{"name":"mcp-notes-server","version":"1.0.0"}}}// ========== 第3步 Client发送initialized通知 ==========// 通知没有id字段,Server不会回复{"jsonrpc":"2.0","method":"notifications/initialized"}// ========== 第4步 Client请求工具列表 =========={"jsonrpc":"2.0","id":2,"method":"tools/list"}// ========== 第5步 Server返回工具列表 =========={"jsonrpc":"2.0","id":2,"result":{"tools":[{"name":"add_note","description":"添加一条新笔记,返回新笔记的ID","inputSchema":{"type":"object","properties":{"title":{"type":"string","description":"笔记标题"},"content":{"type":"string","description":"笔记正文"},"tags":{"type":"array","items":{"type":"string"},"description":"标签列表,可选"}},"required":["title","content"]}},{"name":"list_notes","description":"列出所有笔记的摘要信息","inputSchema":{"type":"object","properties":{}}}]}}// ========== 第6步 Client调用add_note工具 =========={"jsonrpc":"2.0","id":3,"method":"tools/call","params":{"name":"add_note","arguments":{"title":"学习MCP","content":"今天学习了JSON-RPC 2.0协议","tags":["MCP","协议"]}}}// ========== 第7步 Server返回工具执行结果 =========={"jsonrpc":"2.0","id":3,"result":{"content":[{"type":"text","text":"笔记创建成功,ID为 1\n标题 学习MCP\n时间 2025-06-18T10:30:00.000Z"}]}}// ========== 第8步 Server发送日志通知 ==========// Server可以主动发通知,不需要Client先发请求{"jsonrpc":"2.0","method":"notifications/message","params":{"level":"info","logger":"mcp-notes-server","data":"新笔记已创建,当前共1条笔记"}}上面这8条消息覆盖了MCP通信的核心模式。握手三步走(步骤1到3),请求-响应对(步骤4到5、步骤6到7),服务端主动通知(步骤8)。
与REST和gRPC的对比
MCP选择JSON-RPC而不是REST或gRPC是有道理的。我从几个维度对比。
| 对比维度 | JSON-RPC 2.0 | REST | gRPC |
|---|---|---|---|
| 数据格式 | JSON | JSON/XML/任意 | Protobuf二进制 |
| 传输层 | 任意 | HTTP | HTTP/2 |
| 双向通信 | 原生支持 | 需WebSocket | 原生支持 |
| 流式传输 | 需扩展 | 需SSE/WebSocket | 原生支持 |
| 消息大小 | 中等 | 中等 | 小(二进制编码) |
| 可读性 | 高(纯JSON) | 高 | 低(需解码) |
| 浏览器支持 | 好 | 好 | 差(需gRPC-Web) |
| 工具链 | 简单 | 成熟 | 复杂(需proto编译) |
| 状态管理 | 无状态 | 无状态 | 支持有状态流 |
MCP选JSON-RPC的原因我分析有三个。第一,JSON-RPC传输无关,stdio和HTTP都能跑,符合MCP同时支持本地和远程的设计目标。gRPC绑定HTTP/2,stdio模式跑不了。第二,JSON可读性好,调试时直接看消息内容就行,不用proto解码。第三,JSON-RPC原生支持双向通信,Server可以主动发请求和通知,REST做不到这点,得靠WebSocket或SSE补充。
代价是JSON的序列化效率不如Protobuf,大消息场景下性能差一些。但MCP的消息通常很小(工具参数和返回值),这个开销可以接受。
批处理
JSON-RPC 2.0支持批处理,客户端可以一次发一个数组包含多个请求。服务端处理完后返回一个响应数组。
// 批量请求,一次发两个工具调用[{"jsonrpc":"2.0","id":1,"method":"tools/call","params":{"name":"add_note","arguments":{"title":"笔记A","content":"内容A"}}},{"jsonrpc":"2.0","id":2,"method":"tools/call","params":{"name":"add_note","arguments":{"title":"笔记B","content":"内容B"}}}]// 批量响应,顺序不保证,靠id匹配[{"jsonrpc":"2.0","id":2,"result":{"content":[{"type":"text","text":"笔记创建成功,ID为 2"}]}},{"jsonrpc":"2.0","id":1,"result":{"content":[{"type":"text","text":"笔记创建成功,ID为 1"}]}}]批处理里的通知不会有对应响应。如果整个批处理都是通知,服务端不返回任何内容。
不过MCP协议规范没有明确要求实现批处理,目前大部分MCP SDK和客户端都是一条一条发消息。了解这个特性有助于理解JSON-RPC的完整能力,但实际开发中你可能用不到。
常见问题与避坑
第一个坑,id用浮点数。JSON-RPC规范说id不建议包含小数部分。我见过有人用时间戳做id,时间戳带了毫秒小数,结果JavaScript的浮点精度问题导致请求和响应的id对不上,客户端一直超时。用整数或字符串做id最安全。
第二个坑,请求和通知搞混。有id的是请求,没id的是通知。我写Server时把通知当请求处理了,处理完还发了个响应回去,客户端收到多余的响应消息直接报错。区分方法很简单,看消息里有没有id字段。有id的才需要回复。
第三个坑,错误响应里带了result字段。JSON-RPC规范要求result和error不能同时存在。我见过有人写了{ "result": null, "error": {...} },虽然语义上是错误响应,但带了result字段导致一些严格的客户端解析失败。记住,错误响应只有error,成功响应只有result。
第四个坑,消息里嵌了原始换行符。stdio传输按换行符分隔消息。如果你手动拼JSON字符串没正确转义换行符,一条消息会被截成两条。永远用JSON.stringify或json.dumps序列化,它们会自动把\n转义成\\n。
第五个坑,method名大小写敏感。JSON-RPC规范说method名是大小写敏感的。tools/List和tools/list是两个不同的方法。MCP的method名全用小写加斜线分隔,写代码时注意别手滑大写了。这类错误最恶心的是不报method not found,因为有些Server的实现用了不区分大小写的匹配,换一个Server就挂了。
小结
JSON-RPC 2.0是MCP通信的底层协议,定义了请求、响应、通知三种消息格式和一套错误码体系。MCP在JSON-RPC之上定义了initialize、tools、resources、prompts等方法集,所有MCP功能本质上都是JSON-RPC消息的收发。理解JSON-RPC的关键是区分请求(有id需响应)和通知(无id不响应),确保result和error互斥,用JSON.stringify序列化避免换行符问题。MCP选JSON-RPC而不是REST或gRPC,核心原因是传输无关性和原生双向通信能力。
相关推荐
- MCP协议全景:Host、Client、Server架构详解
- Tools原语深度解析:从定义到调用全流程
- Completions与通知机制:自动补全、进度上报与状态通知