MCP server 说「不」的几种方式,以及哪些会算到它头上
本文译自 2026-09-14 的英文原文。
(原文写于 2026-09-14;之后有两处变化。一是探测器靠一次对不存在工具的调用来观测 server 怎样拒绝,自 2026-09-23 和 2026-09-26 起,凡是读不到工具列表、列表还有后续页、列表里恰好有那个保留的工具名,或握手、工具列表被凭据门控挡住,这次调用就会被扣下,
error_taxonomy改记为未检查(Not checked)并写明原因(被凭据门控挡住时,auth_metadata改读门控本身给出的质询);整个 endpoint 都在鉴权层后面时,通常就是这种情况,详见探测器有时会扣下的那一次调用。二是从检查套件 0.11.0(2026-10-06)起,质询指向的元数据 URL 如果在另一台已要求停止探测的主机上,探测器不会去取,auth_metadata记为未检查(Not checked)并写明原因。)
探测器看到的大多是 server 在说「好」:这是我的协议版本,这是我的工具。这份工作里有意思的部分是拒绝。一台正确地说「不」的 server 表现良好。一台用客户端解析不了的形式说「不」的 server,迟早会在凌晨三点弄崩某个人的 agent。把这两者区分开,就是 error_taxonomy 和 auth_metadata 两项检查的用途。
拒绝一:格式规范的 JSON-RPC 错误
给 MCP server 发一个它无法满足的请求,正确的响应是一个 HTTP 响应体,内容是 JSON-RPC 错误对象:jsonrpc、id,以及一个带数字 code 和 message 的 error。这是协议在正常工作。error_taxonomy 检查把它记为已通过(Verified)。
拒绝二:协议还没开始就回一个光秃秃的 401
有些 server 在 endpoint 前面加了一层鉴权。不带凭据的请求会从这一层拿到 401,JSON-RPC 处理程序根本看不到它。响应体往往根本不是 JSON-RPC,这没关系:拒绝发生在分派之前,它不能说明 server 的错误处理是否规范。error_taxonomy 检查把光秃秃的 401 或 403 当作正当的拒绝,而不是格式问题。
但 401 在别处有后果。如果发现握手碰上 401,探测器就得判断自己看到的是什么,这里细节很重要。对第一个 server/discover 请求回的 401,本身不决定任何事:和任何不带可识别新版错误的 4xx 一样,它让探测器转去走旧版的 initialize 握手;算数的是结束握手的那个响应:
- 如果这个决定性的响应是带有结构合法的
WWW-Authenticate质询的401,它的意思是「你需要凭据」。我们从不发送凭据,所以发现类检查记为未检查(Not checked),原因是需要凭据。不是未通过(Failed)。我们看不到你的门后面,这和门坏了不是一回事。如果你是有意把工具发现放在鉴权后面,这就是你应该预期的结果,不需要修。 - 如果是不带合法质询的
401,那就不是凭据门控。它只是一个没有任何说明的拒绝,探测器不会给它同样的礼遇。
拒绝三:新版协议的拒绝
当前协议修订为「有意拒绝握手」定义了专门的 JSON-RPC 错误码:-32020、-32021、-32022。如果第一个请求就收到其中之一,说明新版握手到达了 server,而 server 拒绝了它。这是真正的客户端/服务端不匹配,要按它自己的方式去修;它不是「我是旧版 server,请回退到 initialize」的信号,这种情况下探测器不会回退。
反过来,新版握手收到的其他任何 4xx,只要响应体不是这三个错误码之一,恰恰就是旧版回退信号,探测器会改用 initialize 重试。429 不是回退信号:它的意思是「慢一点」,这一轮里探测器不再向这台 server 发送任何请求。我们曾经把这条规则实现得太窄,只认精确的 400,结果把一台完全合规的旧版 server 标成三项检查未通过(Failed)。修复和自测方法见「已观测,未签名」常见问题。
更正(2026-10-01):从套件 0.10.0 起,server/discover 收到的 2xx 答复如果既不是 discover 结果,也不是那三个错误码之一,探测器同样会回退到 initialize。
拒绝四:格式不规范的「不」
既不是格式规范的 JSON-RPC 错误、也不是分派之前的 401 或 403 的一切:一个 HTML 错误页、一个带堆栈跟踪的 500、一个响应体为 {"error":"nope"} 却没有 jsonrpc 字段的 200、一个空响应体。server 拒绝了,但客户端没有可靠的办法知道为什么、接下来该怎么办。error_taxonomy 把它记为观察到风险(Observed risk)。不是未通过(Failed),因为拒绝本身不是错误行为;但要标出来,因为这种形式迟早会让某个客户端抛出一个没人处理的异常。例外是「慢一点」的答复,即 429 或带 Retry-After 的 503:探测器停止这一轮,把 error_taxonomy 记为未检查(Not checked),并写明这个原因。
鉴权元数据方面的拒绝
当 server 确实给出一个规范的质询时,auth_metadata 检查会顺着它再往前走一步,因为客户端也会这么做。质询应当指向一份客户端可以获取的受保护资源元数据(protected-resource metadata)。可能的结果如下:
| 我们看到的情况 | 记为 |
|---|---|
| 质询里没有元数据 URL | 未检查(Not checked),附原因 |
| 元数据 URL 返回 HTTP 错误 | 观察到风险(Observed risk) |
| 元数据内容不是合法的 JSON | 观察到风险(Observed risk) |
| 元数据是 JSON,但只是一个裸的字符串、数字、布尔值或 null | 观察到风险(Observed risk) |
| 质询要求的 scope 不在元数据列出的范围里 | 观察到风险(Observed risk) |
这些都不是对你的鉴权是否可靠的判决。它们观测的是:一个遵循规范的客户端,能不能发现该如何进行鉴权。这是「鉴权」里唯一能从外部看到的部分,也是我们声称能看到的唯一部分。
为什么值得做这种区分
把所有拒绝都归成「未通过」,渲染起来更简单,却会让页面在两个方向上说谎:一边惩罚了正确地把发现放在门后的 server,一边又把格式不规范的拒绝和一个诚实的 401 藏在同一个红色方块后面。四种「不」,四种不同的结果。读者可以自己判断,哪些对他们正要连接的 agent 有影响。