2.3 JSON-RPC 与消息流——协议的底层语言
小周理解了三种原语,现在想深入一层:Client 和 Server 之间到底怎么通信?消息长什么样? 他在 Inspector 里看到了一些 JSON 消息,但看不懂格式。
JSON-RPC 2.0
MCP 的所有通信都基于 JSON-RPC 2.0——一个轻量级的远程过程调用协议。
为什么选 JSON-RPC 2.0 而不是 REST?
- REST 是请求-响应模式,单向的
- JSON-RPC 2.0 支持双向通信——Server 也可以主动发请求给 Client
- JSON-RPC 2.0 支持通知——一方发送消息,不需要回复
四种消息类型
1. 请求(Request)
Client 发送请求,期待 Server 的响应。每条请求有唯一的 id。
{
"jsonrpc": "2.0",
"id": 1,
"method": "tools/call",
"params": {
"name": "search-files",
"arguments": { "pattern": "*.ts" }
}
}2. 响应(Response)
Server 收到请求后返回结果,id 与请求匹配。
{
"jsonrpc": "2.0",
"id": 1,
"result": {
"content": [
{ "type": "text", "text": "Found 3 files: a.ts, b.ts, c.ts" }
]
}
}3. 通知(Notification)
没有 id,不期待响应。用于推送事件。
{
"jsonrpc": "2.0",
"method": "notifications/tools/list_changed"
}常见通知:
| 通知 | 说明 |
|---|---|
notifications/tools/list_changed | 工具列表变了,Client 应重新获取 |
notifications/resources/updated | 某个资源内容变了 |
notifications/resources/list_changed | 资源列表变了 |
notifications/prompts/list_changed | 模板列表变了 |
notifications/message | Server 发送日志 |
notifications/cancelled | 取消正在进行的请求 |
notifications/progress | 工具执行进度 |
4. 错误(Error)
请求处理失败时返回错误。
{
"jsonrpc": "2.0",
"id": 1,
"error": {
"code": -32602,
"message": "Unknown tool: invalid_tool_name"
}
}标准错误码:
| 代码 | 含义 | 典型场景 |
|---|---|---|
| -32700 | Parse error | stdout 被非协议数据污染 |
| -32600 | Invalid Request | 请求格式不符合规范 |
| -32601 | Method not found | 调用了不支持的方法 |
| -32602 | Invalid params | 参数格式错误 |
| -32603 | Internal error | Server 内部异常 |
两种错误,两种处理方式
MCP 有两种不同的错误报告机制,理解它们的区别非常重要:
v4 的关键变更:输入验证错误(如日期格式错误)应作为 Tool Execution Error 返回,而不是 Protocol Error。这样 LLM 能看到错误描述并修正参数。
// Tool Execution Error 示例——LLM 看到后可以修正日期格式
{
"result": {
"content": [
{ "type": "text", "text": "Invalid date: must be in YYYY-MM-DD format. Got: 08/08/2025" }
],
"isError": true
}
}完整通信流程
小周在 Claude Desktop 中输入「搜索项目中的 TypeScript 文件」,完整的消息流如下:
initialize,携带 {tools: {}, resources: {}}。Server 回复自己的能力。Client 发送 initialized 通知。tools/list,Server 返回所有可用工具的列表和 Schema。search-files,参数 {"pattern": "*.ts"}。tools/call,Server 执行搜索并返回结果。tool_result 附加到对话中,再次调用 Claude 模型。分页
当 Server 返回大量数据时,使用 cursor-based 分页:
场景:一个 GitHub Server 可能暴露 200+ 个工具(每个 API 端点一个)。一次性返回全部会浪费带宽、拖慢初始化。分页让 Client 按需加载。
// 请求(获取第一页)
{ "method": "tools/list", "params": {} }
// 响应(有更多数据)
{ "result": { "tools": [...], "nextCursor": "page-2-token" } }
// 请求(获取下一页)
{ "method": "tools/list", "params": { "cursor": "page-2-token" } }
// 响应(最后一页)
{ "result": { "tools": [...] } }Client 应循环获取直到 nextCursor 为空。
取消
任一方可以发送 notifications/cancelled 通知取消正在进行的请求:
{
"method": "notifications/cancelled",
"params": {
"requestId": "123",
"reason": "用户请求取消"
}
}收到取消通知后,接收方应停止处理、释放资源、不发送响应。
本节核心要点
- MCP 基于 JSON-RPC 2.0,支持请求-响应、通知、错误四种消息
- 选择 JSON-RPC 而非 REST,因为需要双向通信能力
- 两种错误机制:Protocol Error(LLM 无法修正)vs Tool Execution Error(LLM 可自我修正)
- v4 起,输入验证错误应作为 Tool Execution Error 返回
- 支持分页(cursor-based)和取消机制
思考题:为什么 Tool Execution Error 要设计成 isError: true 的正常响应,而不是直接返回 JSON-RPC 错误?
参考思路
关键在于"LLM 能否自我修正":Tool Execution Error 是正常响应的一部分,LLM 可以看到错误描述(如"日期格式需要 YYYY-MM-DD")并修正参数重试。如果返回 JSON-RPC Error,LLM 看到的是协议层面的失败,无法理解具体哪里出错。
进入实战前的自测
在继续之前,确认你能回答以下问题:
- MCP 的三层架构是什么?Client 和 Server 是几对几关系?
- 三种原语分别由谁控制?
- 为什么 MCP 选择 JSON-RPC 而不是 REST?
- stdio 传输中,日志应该写到哪里?