The several ways an MCP server says "no", and which ones we count against it
Most of what a probe sees is a server saying yes: here is my protocol version, here are my tools. The interesting part of the job is the refusals. A server that says no correctly is behaving well. A server that says no in a shape a client can’t parse is going to break somebody’s agent at three in the morning. Telling those apart is what the error_taxonomy and auth_metadata checks are for.
Refusal 1: a well-formed JSON-RPC error
Send an MCP server a request it can’t honour, and the correct response is an HTTP body that is a JSON-RPC error object: jsonrpc, id, and an error with a numeric code and a message. That is the protocol working. The error_taxonomy check records this as held.
Refusal 2: a bare 401 before the protocol even starts
Some servers put an auth layer in front of the endpoint. A request without credentials gets a 401 from that layer, and the JSON-RPC handler never sees it. The body is often not JSON-RPC at all, and that is fine: the refusal happened before dispatch, and it says nothing about whether the server’s error handling is well-formed. The error_taxonomy check treats a bare 401 or 403 as a legitimate refusal, not a shape problem.
But a 401 has consequences elsewhere. If the discovery handshake is met with 401, the probe has to decide what it is looking at, and here the details matter. A 401 to the first server/discover request decides nothing on its own: like any 4xx without a recognized modern error, it sends the probe to the legacy initialize handshake, and what counts is the response that ends the handshake:
- If that deciding response is a
401with a structurally validWWW-Authenticatechallenge, it means “you need credentials.” We never send any, so the discovery checks are recorded as not checked, credential required. Not failed. We could not see past your gate, which is not the same as the gate being broken. If you deliberately put tool discovery behind authentication, this is the outcome you should expect and it is not something to fix. - If it is a
401without a valid challenge, it is not a credential gate. It is just a refusal with no instructions, and the probe does not extend it the same courtesy.
Refusal 3: a modern protocol rejection
The current protocol revision defines specific JSON-RPC error codes for the handshake being rejected on purpose: -32020, -32021, -32022. If the very first request comes back with one of those, the modern handshake reached the server and the server declined it. That is a real client/server mismatch to be fixed on its own terms; it is not the “I am a legacy server, fall back to initialize” signal, and the probe does not fall back in that case.
Conversely, any other 4xx on the modern handshake, with a body that is not one of those three codes, is exactly the legacy fallback signal, and the probe retries with initialize. A 429 is not a fallback signal: it means “slow down”, and the probe sends nothing more to that server in that round. We once implemented that too narrowly, accepting only an exact 400, and marked a fully compliant legacy server as failing three checks. The fix and the self-test are in the observed-unsigned FAQ.
Correction (2026-10-01): from suite 0.10.0 the probe also falls back to initialize when server/discover gets a 2xx answer that is neither a discovery result nor one of those three codes.
Refusal 4: the malformed no
Everything that is neither a well-formed JSON-RPC error nor a pre-dispatch 401 or 403: an HTML error page, a 500 with a stack trace, a 200 whose body is {"error":"nope"} with no jsonrpc field, an empty body. The server refused, but a client has no reliable way to know why or how to proceed. error_taxonomy records this as risk observed. Not failed, because a refusal is not incorrect behaviour, but flagged, because its shape will cost some client an unhandled exception. The exception is a “slow down” reply, a 429 or a 503 with Retry-After: the probe stops for that round and records error_taxonomy as not checked, with that reason.
The auth metadata refusals
When a server does present a proper challenge, the auth_metadata check follows it one step further, since a client would too. The challenge should point at protected-resource metadata a client can fetch. The outcomes:
| What we see | Recorded as |
|---|---|
| Challenge with no metadata URL | not checked, with reason |
| Metadata URL returns an HTTP error | risk observed |
| Metadata body is not valid JSON | risk observed |
| Metadata is JSON but a bare string, number, boolean or null | risk observed |
| The challenge asks for a scope the metadata does not list | risk observed |
None of these is a verdict on whether your authentication is sound. They are observations about whether a client following the spec can discover how to authenticate. That is the only part of “auth” visible from outside, and the only part we claim to see.
Why the distinction earns its keep
Collapsing all refusals into “failed” would be simpler to render and would make the page lie in both directions: penalising servers that correctly gate discovery, and hiding the malformed refusals behind the same red square as an honest 401. Four kinds of no, four different outcomes. The reader can decide which ones matter for the agent they are about to connect.