skip to content

In MCP 2026-07-28, what must a server return when resources/read names a missing resource?

level: middleimportance: should knowfreq 38%

answer

  1. success with nothing inside is a lie
  2. it is treated as a params problem
  3. the standard invalid-params code
  4. -32002 only tolerated from older servers
  5. never an empty contents array

basics

~20 s

A JSON-RPC error with code -32602, invalid params. Revision 2026-07-28 folded resource-not-found into the standard invalid-params code; clients still accept -32002 from older servers. A server MUST NOT answer with a success result carrying an empty contents array.

solid answer

~50 s

Under revision 2026-07-28 a nonexistent resource is a **params** problem: the server answers `resources/read` with a JSON-RPC error, code **-32602** (invalid params), typically naming the offending URI in the error data. Earlier revisions used a resource-specific **-32002**; the current spec keeps clients tolerant, so a client SHOULD still accept -32002 from an older server, but a 2026-07-28 server should emit -32602. The second half of the rule is stricter and more often broken: a server **MUST NOT** return a successful result with an empty `contents` array for a resource that does not exist. An empty array means "this resource exists and has no content", which is a different and much less actionable statement — the host cannot distinguish a typo, a revoked permission or a deleted file from genuinely empty data, so the failure surfaces as a silent gap in context rather than an error someone can fix.

code

json · 9 lines
json
{
  "jsonrpc": "2.0",
  "id": 7,
  "error": {
    "code": -32602,
    "message": "Resource not found",
    "data": { "uri": "file:///srv/app/missing.md" }
  }
}

go deeper

for a junior

Remember that a missing resource is an error, not an empty success, and that the current code is -32602, invalid params.

for a middle

Explain why absence is modelled as invalid params, why -32002 is still accepted inbound from older servers, and what an empty contents array legitimately means.

for a senior

Show that you think about the host's experience: distinguishable failures, whether not-found is used to mask authorization, and not caching a failed read against a listing's TTL.

for a principal

Own the error contract across a server fleet — a uniform existence-disclosure policy, no private codes in the reserved range, and a client tolerance window while the deployed servers still straddle both protocol eras.

## The two halves of the rule Revision 2026-07-28 says two things about reading a resource that is not there, and interviewers usually probe the second. 1. **Which code**: resource-not-found is reported as JSON-RPC **-32602, invalid params**. The URI is a request parameter, and naming something that does not exist makes that parameter invalid. Earlier revisions defined a dedicated **-32002** for the case; the current revision consolidates on the standard code, while clients remain tolerant and SHOULD still accept -32002 when it arrives from an older server. A modern server emitting -32002 is writing to a superseded rule. 2. **Which shape**: a server **MUST NOT** return a success result whose `contents` array is empty in order to signal absence. Absence is an error, not a result. ## Why the empty array is forbidden An empty `contents` array is a legitimate answer to a real question: this resource exists and currently holds nothing — an empty file, a table with no rows today. Overloading it to also mean "no such resource" destroys that distinction, and destroys it exactly where the host needs it. Concretely, the host cannot tell apart: - a URI the user mistyped, - a resource that was deleted since it was listed, - a resource the presented credential is no longer allowed to see, - a resource that is genuinely empty. An error carries a code, a message and data the host can show, log, or act on. A silent empty result becomes a silent gap in the model's context, and the model will happily reason from the absence as though it were evidence. That is the failure mode the MUST NOT exists to prevent. ## Errors versus in-result failure Resources have no in-result error channel. Reading a resource either produces content or produces a JSON-RPC error; there is no soft-failure flag on a read result the way there is for tool execution. This is deliberate: a read is a retrieval, and a retrieval that could not retrieve is a protocol-level failure, not a domain outcome the model should interpret. That also means the error text is user-facing and model-adjacent. It should say which URI failed and, where safe, why — without leaking whether a path exists but is forbidden, when that distinction is itself sensitive. ## Not-found versus not-allowed Authorization failures are a separate axis. On a remote server behind OAuth 2.1, an unauthorized read is answered at the HTTP layer — for example a 403 with `error="insufficient_scope"` to drive a step-up — rather than dressed up as a missing resource. Where the server would rather not reveal existence at all, reporting not-found for a resource the caller may not see is a defensible privacy choice, but it should be a deliberate policy, uniformly applied, not an accident of error handling. A related subtlety: revision 2026-07-28 states the resource set MUST NOT vary per connection, though it MAY vary by the authorization presented. So a URI that was listed under one credential and errors under another is consistent with the spec; a URI that appears and disappears depending on which connection asked is not. ## Envelope context An error is a JSON-RPC error object, not a result, so it carries no `resultType`, no `ttlMs` and no `cacheScope` — those belong to results. Nor should a client cache a not-found from a listing TTL; the listing's TTL governs the catalogue, not the outcome of a specific read. Other codes a resources implementation touches: **-32601** for a method the server does not implement (paired with HTTP 404 on Streamable HTTP), **-32020** `HeaderMismatchError` when the `MCP-Protocol-Version` header disagrees with the `_meta` value, **-32021** `MissingRequiredClientCapabilityError`, and **-32022** `UnsupportedProtocolVersionError`. The range -32020 through -32099 is reserved to the specification, so a server must not mint private codes there. ## Client-side handling A well-built client treats -32602 and legacy -32002 from `resources/read` the same way: surface the failure against the URI that caused it, drop that URI from any cached listing, and — importantly — do not retry blindly. Under MCP's stateless model a retry with the same URI is a fresh, self-contained request that will fail identically; nothing about the connection changes the answer. ## Common mistakes Returning `{ "contents": [] }` for a missing file is the classic. Close behind: emitting -32002 from a new server, inventing a private error code in the reserved spec range, and returning a 200-level success with an error string stuffed in a text content entry, where only the model ever sees it.

  • When is an empty contents array actually a legal answer to resources/read?
    When the resource genuinely exists and currently holds nothing — an empty file, a query view with no rows today. That is a true statement about real data, and the host can distinguish it from failure precisely because absence is reported as an error instead. The MUST NOT forbids using the empty array to mean "no such resource", not the empty array itself.
  • A client receives -32002 from a resources/read. What should it do?
    Treat it as resource-not-found. Revision 2026-07-28 moved the case to -32602 but keeps clients tolerant of -32002 arriving from older servers, since the deployed fleet straddles both eras. A new server should emit -32602; a client that hard-fails on -32002 breaks needlessly against servers that have not migrated.
  • Should an unauthorized resource read be reported as not-found?
    Not by default. On a remote server the authorization failure belongs at the HTTP layer — a 403 with error="insufficient_scope" lets the client attempt a step-up. Reporting not-found to hide existence is a legitimate privacy policy, but it must be a deliberate, uniformly applied choice, because it costs the client any chance of recovering by obtaining broader scope.

saying these in an interview costs you the question

  • Returning an empty contents array for a missing resource
  • Emitting -32002 from a server built to the current revision
  • Putting the error message in a text content block instead
  • Minting a custom code inside the reserved -32020 to -32099 range
  • Retrying the same read and expecting a different answer

context