先记住这个答案
request 带 method 和请求 id,用来发起需要结果的操作;response 用同一个 id 返回 result 或 error,两者不能同时存在;notification 带 method 但没有 id,接收方不返回 JSON-RPC 响应。MCP 客户端和服务端都可能发起请求,消息方向不能单独决定类型。按 2025-11-25,正常请求 id 是字符串或整数且不能为 null,同一请求方在同一会话内不能复用。并发调用必须按连接、方向和 id 关联,不能靠响应到达顺序配对。
- 带 method 的消息可能是请求也可能是通知
- result 与 error 是响应的互斥分支
- 响应到达次序不保证与请求发送次序相同
看字段角色而不是看消息来自哪一端
tools/list 通常由客户端向服务端请求,roots/list 则可以由服务端向客户端请求。两者都是 request;它们的结果都由接收请求的一端返回。若接收循环写成“服务器发来的全是响应”,反向请求就会被错误地丢进待完成请求表。
示意中两个列表请求分别使用独立 id,工具列表的响应先回来不影响资源列表仍在等待。通知不登记为待完成请求,也不需要为它创建一个迟早超时的 Promise;这能避免长连接运行一段时间后等待表不断增长。
C -> S {"jsonrpc":"2.0","id":"r-1","method":"resources/list"}
C -> S {"jsonrpc":"2.0","id":"t-2","method":"tools/list"}
S -> C {"jsonrpc":"2.0","id":"t-2","result":{"tools":[]}}
S -> C {"jsonrpc":"2.0","method":"notifications/tools/list_changed"}
S -> C {"jsonrpc":"2.0","id":"r-1","result":{"resources":[]}}五行分别表示五条独立协议消息,假设初始化与相关能力声明已经完成。关联表应先完成 t-2,再处理无 id 的变化通知,最后完成 r-1,不能按发送顺序取出第一个等待者。
错误响应与工具失败不是同一个分支
JSON-RPC error 描述请求无法按协议正常完成,例如方法不存在或报文参数结构无效。工具执行结果也可能通过 result 正常返回,但内部含 isError: true;这时外层响应关联仍然成功,业务动作却没有按预期完成。
因此调用层应先验证外层结构并匹配 id,再交给相应方法的结果解析器。不能仅凭 HTTP 200 或存在 result 就显示“任务成功”,也不能把所有方法的结果都当成 tools/call 的结果去寻找 isError 字段。
断线和未知响应需要清楚的清理策略
连接关闭时应结束或转移它所属的等待项,晚到的响应不能完成新连接里另一个碰巧同名的请求。保留连接身份、发起方向和 id 的关联信息,有助于发现重复响应、未知 id 与跨连接投递问题。
id 的唯一性是会话内的协议关联规则,不是业务幂等键。网络超时之后换一个新 id 重发写操作,仍可能重复产生副作用;反过来重复使用旧 id 也不会自动获得服务端去重保证。业务重试必须另有状态查询或幂等契约。
容易答错的地方
- 没有 id 的消息都是错误响应
- 正常通知就没有 id;应结合 method、result 和 error 的结构分类。损坏请求的错误关联还存在特殊情况,不能用单个字段包办全部解析。
- 每个响应都按发送顺序移出等待队列
- 并发请求的处理时间不同,响应可以乱序到达;必须按连接内正确的请求标识配对,否则用户会收到另一个工具的结果。
面试官还会怎么问?
客户端和服务端能否分别使用相同的请求 id?
唯一性要求针对请求方在会话内的使用记录,因此实现还要区分消息方向。不要把双方发出的请求塞进没有方向信息的同一张等待表。
请求超时后迟到的响应是否还能改变任务状态?
应根据明确的终态策略处理,通常不能让已经结束的等待重新变成成功。仍可记录迟到结果用于核对副作用,但不能偷偷覆盖用户已看到的结论。
通知处理失败要不要返回一个 error?
JSON-RPC 通知没有响应通道,不能为它新增错误响应。可以记录诊断并采取协议允许的恢复动作;需要调用方确认结果的操作应设计为请求。
参考资料
示例用于理解所注明的运行环境与边界;延伸学习可结合原文中的更多案例。