MCP 错误码详解:协议错误与工具错误
一次工具调用出了问题。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说清楚了:输入校验错误,比如日期格式不对、数值超出范围,属于工具执行错误。协议错误留给根本不符合CallToolRequestschema 的请求,比如没带name的调用。
2026-07-28 的 schema 文件仍然把「invalid tool arguments」列为 -32602 的用途之一,还附了一条关于「缺少必填属性」的示例消息。所以在这一点上,规范自己的材料并不完全一致。
一条和 tools 页面相符的做法:如果模型改一下参数就能解决,回 isError: true,并说清楚该改什么。-32602 留给调用了不存在的工具、或者根本不像一次工具调用的请求。
模型看到的是什么
客户端夹在你的服务器和模型之间。从 2025-11-25 起,规范告诉客户端哪些该转给模型:
- 客户端应该把工具执行错误交给模型,让它自我纠正。
- 客户端可以把协议错误交给模型,不过「这类错误不太可能带来成功的恢复」。
从 2025-06-18 起,schema 在 isError 的注释里也是这个意思:源自工具的错误应该放进结果、把 isError 设上,因为「否则 LLM 就看不到出了错,也就没法自我纠正」。
所以,isError 结果会作为工具的输出进入模型的上下文,和成功的结果走同一条路。协议错误会怎样,取决于客户端:可能交给模型,可能展示给用户,也可能直接结束这一步。即使它到了模型那里,也是以「请求失败」的面目出现,而不是工具的回答。
由此有两个后果:
- 本来模型能修的问题(日期不对、ID 不存在),你回了协议错误,就可能拿走了它唯一的修正机会。
- 本来修不了的问题(工具不存在、服务器坏了),你回了
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 一样从外部连接,并告诉你收到的是哪一类回复。