MCP 报错分不清?先看响应里是 result 还是 error

MCP 里的失败分两条完全不同的通道。第一条是工具执行失败 :它是一次成功的 JSON-RPC 响应,result 里带 isError: true。消息内容会被模型读到,模型有机会自己修正后重试S2。第二条是协议错误 :它是一次 JSON-RPC error 响应,顶层是 error: {code, message}。这类响应由宿主程序的 MCP 客户端代码处理,模型根本看不到它S2。

区分方法只需要一步:看原始响应的顶层字段是 result 还是 error。适用情形是你在调试 tools/call 的报错、决定错误信息写给谁看,或者在排查"模型为什么没反应"。不适用于本地网络层故障------那不是任何一方的协议消息,后面会说边界。

假设一个具体的排查场景

假设(例如)你接了一个天气查询的 MCP server,某天模型连续三次调用 get_weather 都没成功,用户界面只显示一个笼统的"调用失败"。你打开日志,看到两种长得有点像的东西混在一起:有的响应是 {result: {content: [...], isError: true}},有的是 {error: {code: -32602, message: "..."}}。如果你把两者都当成同一种"工具报错"处理------比如都往用户界面上抛、或者都塞回给模型------其中一半信息会被送错对象。

为什么协议要分两条通道

MCP 的底层是 JSON-RPC 2.0,一次请求的响应只有两种形态:带 result 的成功响应,或带 error 的错误响应S4。工具执行失败被放在前一种里------JSON-RPC 层面"成功"地返回了一个结果,只是这个结果声明自己 isError: true。TypeScript SDK 的错误文档把区别讲得很直白:工具错误是"模型读到并从中恢复"的东西,协议错误是"模型永远看不到"的东西S2。

判断标准不是"错误严不严重",而是谁能修正它 。tools/call 由模型驱动,模型可以改参数重试------记录不存在、日期格式错、上游限流,这些放进 isError: true,且文字里要写清楚怎么改,因为那是模型唯一能拿到的东西S2。而 resources/read、prompts/get 这类请求由宿主应用驱动,失败就是抛给调用方代码的协议错误S2。所以同一个"参数格式不对",在工具调用里通常该是工具错误,在资源读取里就是 -32602。

第三方框架的实践也印证这条分界:请求结构问题(未知工具名、schema 校验失败、无效游标)走协议错误;模型能自纠的输入校验失败、业务逻辑错误、超时、限流走 isError: trueS3。

可复现的检查步骤

前提:能拿到一条 tools/call 的原始 JSON-RPC 报文。stdio 传输可以加一层日志代理打印双向消息;Streamable HTTP 可以在客户端记录响应体。关键是看线上原始字节,不要只看 SDK 抛出的异常------原因见下节。

检查本身三步:

  1. 从日志里取出那次失败的 tools/call 响应原文。
  2. 看顶层有 error 字段还是 result 字段。
  3. 如果是 result,再看里面的 isError 值。

对照一个决策表:

响应形态 归属 该给谁看
result + isError: true 工具执行失败 模型(写明修正线索)S2
result + isError: false 不是失败,是正常返回 检查是不是内容本身被误读了
顶层 error + code 协议错误 宿主客户端代码处理S2

三个探针请求(示例,未在本文环境实测)分别命中三种形态。对一个存在的工具传错误日期格式,预期得到 result + isError: true,message 里应有可操作的提示S3;对不存在的工具名发 tools/call,预期得到顶层 error,常见 code 是 -32602S3;用 SDK 的 in-memory client 调用,TS SDK 会把后者表现为 reject 出来的 ProtocolError,携带 {code: -32602, message: ...} 线上字段S2。

失败边界:这个检查在哪里会失灵

SDK 会抹掉信封差异。 在 TypeScript SDK 里,JSON-RPC error 响应会让 client.callTool 直接 reject 一个 ProtocolError,而 isError: true 是正常 resolve 的返回值S2。如果只在 catch 块里打日志,你看到的都是异常,分不出哪条是协议错误、哪条被框架包装过。所以检查必须落在原始报文层。

服务端抛异常不等于协议错误。 TS SDK 会把工具 handler 抛出的任何异常(包括抛出的 ProtocolError)转成 isError: true 的结果,异常 message 变成 content 文本S2。也就是说,"工具 handler 里 throw"和"显式返回 isError"对客户端是同一形态。唯一例外是 UrlElicitationRequiredError,它会作为 JSON-RPC error 透传给宿主S2。

本地超时不是任何一方的协议错误。 规范明确说,纯粹本地的错误(比如 SDK 内部产生的请求超时)目前没有被分配错误码,实现者在 JSON-RPC 形状的结构里暴露本地错误时,要确保它不会被误认成对端发来的错误S4。如果你把客户端超时异常打印出来发现也有 code 和 message,先确认它是本地造的还是服务端回的------看原始报文里有没有对应的响应。

旧版本 code 会撞车。 -32002(资源不存在)是 2025-11-25 及更早版本的 code,本版本实现不得再发出它,但客户端仍应接受旧 server 发来的这个 codeS4。排查时看到 -32002 不要直接当"未知错误",先查对端的协议版本。

另外注意错误码的区间划分:-32000 到 -32019 是各实现的历史遗留,新代码不得在此分配,接收方除 -32002 外不应假设其含义;-32020 到 -32099 保留给规范本身(目前有 -32020、-32021、-32022)S4。如果你的日志里出现这个区间的陌生 code,它不该来自一个合规的对端。

一条可以带走的检查动作

下次遇到"MCP 工具报错",先别改代码,把那条失败的 tools/call 响应原文找出来,回答两个问题:顶层是 result 还是 error?如果是 result,isError 是不是 true?第一个答案决定错误给模型还是给宿主代码,第二个答案防止你把正常返回误判成失败。跑完之后,欢迎把你的传输方式(stdio / Streamable HTTP)、SDK 和协议版本贴出来对照------上面 TS SDK 的行为是文档结论,你环境里的实际形态值得单独验证。

参考资料

相关推荐
程序员Flycan1 小时前
🚀 跨域终结者:前端代理服务器(Proxy)原理解析与配置总结
前端
怕浪猫1 小时前
顶级模型一句话,AI 写出了能玩的 QQ飞车
前端·面试·github
呃呃呃呃ex1 小时前
10. 现代前端工程化:ES6 React 项目实战与原理解析
前端·javascript
呃呃呃呃ex1 小时前
9. TypeScript:从类型系统到工程实践
前端·面试
粥里有勺糖1 小时前
视野修炼-技术周刊第134期 | 豆豆眼头像
前端·github·aigc
浪遏1 小时前
D2C 系统架构复盘:从设计稿到代码,我们踩过的坑和最终方案
前端·javascript·后端
光影少年1 小时前
Fabric渲染流程
前端·react native·react.js
IT_陈寒1 小时前
我的JavaScript代码为啥在forEach里没按预期执行?
前端·人工智能·后端
再吃一根胡萝卜1 小时前
从零实现一个带虚拟滚动的 Select
前端