菜单

MCP 错误码详解:协议错误与工具错误

2026-10-27MCP Checkup

一次工具调用出了问题。MCP 给你的服务器两种方式来表达,而这两种方式最后去的地方不一样。一种是 JSON-RPC 错误:这个请求作为请求失败了。另一种是一个普通的工具结果,带 isError: true:请求本身没问题,工具也跑了,是工具在报告一个问题。前一种可能根本到不了模型那里;后一种本来就是写给模型看的。

这篇文章列出规范定义的错误码、各自出自哪个版本,讲清楚每类失败该归到哪一边,最后给出可以对着你自己的服务器跑的 curl 命令。把 https://your-server.example.com/mcp 换成你的地址就行。

两种失败方式

协议错误是一个 JSON-RPC 错误响应。里面没有 result,只有 error,带一个整数 code 和一段 message:

{
  "jsonrpc": "2.0",
  "id": 3,
  "error": {
    "code": -32602,
    "message": "Unknown tool: invalid_tool_name"
  }
}

工具执行错误是一个成功的 JSON-RPC 响应,只是它的 result 说工具失败了:

{
  "jsonrpc": "2.0",
  "id": 4,
  "result": {
    "resultType": "complete",
    "content": [
      {
        "type": "text",
        "text": "Invalid departure date: must be in the future. Current date is 08/08/2025."
      }
    ],
    "isError": true
  }
}

两个例子都出自规范的 tools 页面。resultType 是 2026-07-28 新增的。更早的版本返回同样的结果,只是没有这个字段,客户端会把缺失的 resultType 当作 "complete"。

2025-11-25 和 2026-07-28 的 tools 页面是这样划线的:

协议错误 工具执行错误(isError: true)
工具不存在 API 调用失败
请求格式不对:不符合 CallToolRequest 的 schema 输入校验错误,比如日期格式不对、数值超出范围
服务器内部错误 业务逻辑错误

规范也给了理由。协议错误说明「请求结构本身有问题,模型不太可能自己修好」;工具执行错误则「带有可操作的反馈,语言模型可以据此自我纠正,调整参数后重试」。

JSON-RPC 错误码

MCP 的消息遵循 JSON-RPC 2.0,所以它的五个标准错误码在每个版本里都适用。右边一栏列的是 2026-07-28 的 schema 给出的用途:

错误码 JSON-RPC 名称 MCP server 什么时候发
-32700 Parse error 请求体不是合法的 JSON
-32600 Invalid Request 是合法的 JSON,但不是合法的请求对象:缺了 jsonrpc 或 method,或者两者类型不对
-32601 Method not found 方法不存在,或者它属于服务器没有声明的能力,比如对不支持 prompts 的服务器调 prompts/list
-32602 Invalid params 工具名或 prompt 名不存在、分页 cursor 无效或已过期、日志级别无效;从 2026-07-28 起,资源不存在也用它
-32603 Internal error 服务器内部出了故障

如果服务器根本读不出请求的 id,比如遇到解析错误,回复就没法原样带回它。JSON-RPC 2.0 规定这时 id 为 null;MCP 的 schema 把错误响应里的 id 定为可选,所以你也可能看到干脆没有 id。

在 2026-07-28 的 Streamable HTTP 下,其中两个错误码有固定搭配的 HTTP 状态码。服务器没有实现的方法,回 404 Not Found 加 -32601。缺了必需的 _meta 字段(io.modelcontextprotocol/protocolVersion 或 io.modelcontextprotocol/clientCapabilities)的请求,回 400 Bad Request 加 -32602。在它之前的 Streamable HTTP 版本(2025-03-26 到 2025-11-25)没有给 JSON-RPC 错误指定 HTTP 状态码,而且在这些版本里 404 另有含义:会话 ID 已经失效。

MCP 自己定义的错误码

JSON-RPC 把 -32000 到 -32099 留给由实现自行定义的服务器错误。2026-07-28 把这一段一分为二:

  • **-32000 到 -32019 是历史遗留段。**在这条规则出现之前,各家实现已经在用这些码。新的实现不应该再用;收到这些码的一方也不能假定它们有固定含义,只有下面说到的 -32002 例外。
  • **-32020 到 -32099 归规范所有。**规范没有定义的码,实现方不能从这一段里发;已定义的码,只能按定义的含义使用。

目前这一段里定义了三个:

错误码 名称 什么时候发 HTTP 状态码
-32020 HeaderMismatch 必需的请求头(MCP-Protocol-Version、Mcp-Method、Mcp-Name)缺失、格式不对,或者和请求体对不上 400
-32021 MissingRequiredClientCapability 处理这个请求需要某项客户端能力,而请求里没有声明;data.requiredCapabilities 列出缺了哪些 400
-32022 UnsupportedProtocolVersion 服务器没有实现请求的版本;data.supported 列出它支持的版本 400

有两个旧的码已经退役,以后也不会挪作他用:

  • -32002,资源不存在,一直用到 2025-11-25。在 2026-07-28 下,资源不存在改用 -32602,这一版的服务器不能再发 -32002。客户端对更早版本的服务器发来的 -32002 仍然应该接受。
  • -32042,需要 URL elicitation,只存在于 2025-11-25。

如果你的服务器需要自己的错误码,规范的要求是:选在 JSON-RPC 保留段 -32768 到 -32000 之外。

还有一处版本差异。在带 initialize 握手的版本里(2025-11-25 及更早),规范对「不支持的协议版本」给的例子是 -32602,附带 data.supported 和 data.requested。到了 2026-07-28,同样的情况用 -32022。

工具不存在:每个版本里都是协议错误

2025-06-18、2025-11-25 和 2026-07-28 三个版本都把「工具不存在」归为协议错误,用的也是同一个例子:-32602,消息为 Unknown tool: <name>。

所以,客户端调了一个你没有的工具时,不要这样回:

  • 回一个 isError: true 的结果,等于告诉模型这个工具存在,而且跑过了;
  • 回一个不带 isError 的结果,等于告诉它调用成功了;
  • 回一个 HTML 错误页或空响应体,客户端根本没法解析。

「工具不存在」也不同于「方法不存在」。tools/call 是你的服务器认识的方法,工具名只是它的一个参数。所以错误码是 -32602 而不是 -32601,2026-07-28 里「-32601 配 404」的规定也管不到它。

我们会对目录里的每台服务器调用一个编造的工具名。看每台服务器最近一次检查(截至 2026 年 10 月 26 日的一周内),有 3,265 台直接回答了这次调用,没有先用 401 要求凭据。其中 1,390 台(43%)回的是 isError: true 的结果,28 台回的是一个普通结果、根本没报错,19 台回的不是合法的 JSON-RPC。其余 1,828 台回的是 JSON-RPC 错误,或者用 403 直接拒绝了调用;我们的记录分不出这两种。

参数错误:各版本说法不一的地方

这是规范唯一改过口径的地方。

  • 2025-06-18 把「invalid arguments」(参数无效)列在协议错误下,又把「invalid input data」(输入数据无效)列在工具执行错误下,但没说两者的分界在哪里。
  • 2025-11-25 和 2026-07-28 说清楚了:输入校验错误,比如日期格式不对、数值超出范围,属于工具执行错误。协议错误留给根本不符合 CallToolRequest schema 的请求,比如没带 name 的调用。

2026-07-28 的 schema 文件仍然把「invalid tool arguments」列为 -32602 的用途之一,还附了一条关于「缺少必填属性」的示例消息。所以在这一点上,规范自己的材料并不完全一致。

一条和 tools 页面相符的做法:如果模型改一下参数就能解决,回 isError: true,并说清楚该改什么。-32602 留给调用了不存在的工具、或者根本不像一次工具调用的请求。

模型看到的是什么

客户端夹在你的服务器和模型之间。从 2025-11-25 起,规范告诉客户端哪些该转给模型:

  • 客户端应该把工具执行错误交给模型,让它自我纠正。
  • 客户端可以把协议错误交给模型,不过「这类错误不太可能带来成功的恢复」。

从 2025-06-18 起,schema 在 isError 的注释里也是这个意思:源自工具的错误应该放进结果、把 isError 设上,因为「否则 LLM 就看不到出了错,也就没法自我纠正」。

所以,isError 结果会作为工具的输出进入模型的上下文,和成功的结果走同一条路。协议错误会怎样,取决于客户端:可能交给模型,可能展示给用户,也可能直接结束这一步。即使它到了模型那里,也是以「请求失败」的面目出现,而不是工具的回答。

由此有两个后果:

  1. 本来模型能修的问题(日期不对、ID 不存在),你回了协议错误,就可能拿走了它唯一的修正机会。
  2. 本来修不了的问题(工具不存在、服务器坏了),你回了 isError: true,模型就可能一直重试一件不可能成功的事。

写一个模型用得上的 isError 结果

content 里的文字就是全部信息,所以要具体:

  • 说清楚哪里错了、什么值才有效。「start_date 必须是今天或之后;今天是 2026-10-27」让模型有事可做,「输入无效」不行。
  • **用 inputSchema 里的参数名,**这样模型能把这条消息对应到它刚发出的调用。
  • 说明重试有没有用。「被限流了,30 秒后重试」和「这个账号无权访问该项目」,下一步完全不同。
  • **不要带堆栈、内部主机名和文件路径。**它们帮不了模型,还会出现在一段有人在读的对话里。
  • **显式设置 isError: true。**失败只写在文字里、isError 缺省或为 false,在任何检查这个标志的客户端看来都是成功。

2026-07-28 的 tools 页面对跨调用保存状态的工具也是同样的思路:用已过期或不存在的句柄调用时,「应该返回一个说明这一点的工具执行错误,好让模型新建一个来恢复」。

用 curl 检查

下面的命令用的是 2026-07-28 的请求头。如果服务器是更早的版本,先跑 initialize,改发你的会话头,并去掉 _meta 部分。这些命令要看的 JSON-RPC 错误码在 2025-06-18、2025-11-25 和 2026-07-28 里都一样;只有「方法不存在回 404」是 2026-07-28 新加的。

先定义一个小函数:

MCP_URL=https://your-server.example.com/mcp
META='"_meta":{"io.modelcontextprotocol/protocolVersion":"2026-07-28","io.modelcontextprotocol/clientInfo":{"name":"curl-test","version":"0.1"},"io.modelcontextprotocol/clientCapabilities":{}}'
call() {  # call METHOD PARAMS [more curl options]
  m=$1 p=$2; shift 2
  curl -sS "$MCP_URL" -w '\nHTTP %{http_code}\n' \
    -H 'Content-Type: application/json' \
    -H 'Accept: application/json, text/event-stream' \
    -H 'MCP-Protocol-Version: 2026-07-28' \
    -H "Mcp-Method: $m" "$@" \
    -d '{"jsonrpc":"2.0","id":1,"method":"'"$m"'","params":'"$p"'}'
}

**方法不存在。**应该得到 -32601,在 2026-07-28 下还有 HTTP 404:

call no/such_method '{'"$META"'}'

**工具不存在。**应该得到 JSON-RPC error,错误码 -32602。不是 result,也不是 HTML:

call tools/call '{"name":"definitely_not_a_tool","arguments":{},'"$META"'}' \
  -H 'Mcp-Name: definitely_not_a_tool'

**参数错误。**把 get_weather 和参数换成你自己的某个工具,以及一个它应该拒绝的值。应该得到一个 "isError": true 的 result,外加一句点名该参数的说明:

call tools/call '{"name":"get_weather","arguments":{"date":"not-a-date"},'"$META"'}' \
  -H 'Mcp-Name: get_weather'

**非法 JSON。**应该得到 -32700,id 为 null 或者没有 id:

curl -sS "$MCP_URL" -w '\nHTTP %{http_code}\n' \
  -H 'Content-Type: application/json' \
  -H 'Accept: application/json, text/event-stream' \
  -H 'MCP-Protocol-Version: 2026-07-28' \
  -H 'Mcp-Method: tools/list' \
  -d '{"jsonrpc":"2.0","id":1,"method":"tools/list"'

如果回复是 text/event-stream,JSON 在 data: 行里。无论哪种情况,HTML 页面、空响应体或者连接被断开,都让客户端无从解析:不管出了什么问题,那条本可以解释原因的 JSON-RPC 错误都没有送到。

清单

情况 服务器应该发什么
调用了你没有的工具 JSON-RPC 错误 -32602,不是 isError,也不是 HTML
调用了你没实现的方法 JSON-RPC 错误 -32601;在 2026-07-28 下配 HTTP 404
请求体不是 JSON JSON-RPC 错误 -32700,id 为 null 或缺省
模型能改正的参数值 isError: true 的结果,点名该参数并说明什么值有效
工具内部失败(上游不可用、被限流) isError: true 的结果,说明重试有没有用
资源不存在 2026-07-28 下用 -32602;-32002 只用于更早的版本
不支持的版本(2026-07-28) -32022,附 data.supported,HTTP 400
请求头和请求体对不上(2026-07-28) -32020,HTTP 400
你自己的错误码 选在 -32768 到 -32000 之外;绝不在 -32020 到 -32099 里发未定义的码
任何错误文字 不带堆栈、内部主机名或文件路径

想看看你自己的服务器怎么回应一个不存在的工具,可以在 mcpcheckup.com 粘贴你的地址。它会像 agent 一样从外部连接,并告诉你收到的是哪一类回复。