Skip to content
7 changes: 3 additions & 4 deletions .changeset/fix-unknown-tool-protocol-error.md
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,6 @@ Fix error handling for unknown tools and resources per MCP spec.
code `-32602` (InvalidParams) instead of `CallToolResult` with `isError: true`.
Callers who checked `result.isError` for unknown tools should catch rejected promises instead.

**Resources:** Unknown resource reads now return error code `-32002` (ResourceNotFound)
instead of `-32602` (InvalidParams).

Added `ProtocolErrorCode.ResourceNotFound`.
**Resources:** Added `ProtocolErrorCode.ResourceNotFound` (`-32002`) as receive-tolerated
vocabulary. The wire code emitted for an unknown `resources/read` URI is `-32602`
(Invalid Params) — see the `resource-not-found-32602` changeset.
24 changes: 24 additions & 0 deletions .changeset/resource-not-found-32602.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
---
"@modelcontextprotocol/core": minor
"@modelcontextprotocol/server": major
"@modelcontextprotocol/client": minor
---

`resources/read` for an unknown URI now answers with JSON-RPC error code `-32602`
(Invalid Params) on every protocol revision, with `error.data.uri` echoing the
requested URI. The 2026-07-28 specification requires `-32602`; the v1.x SDK already
emitted `-32602` on earlier revisions, so v1.x peers see no change.

This supersedes an interim `-32002` emission that shipped in earlier v2 alphas. The
era-aware encode seam (`WireCodec.encodeErrorCode`) maps any handler-thrown `-32002`
to `-32602` on the wire; note that a `-32002` thrown without `data.uri` is emitted as
a bare `-32602` and is no longer recognizable as resource-not-found — throw
`ResourceNotFoundError` (or include `data: { uri }`) to preserve the classification.

`ProtocolErrorCode.ResourceNotFound` (`-32002`) remains importable as receive-tolerated
vocabulary; clients should accept both `-32602` and `-32002` from peers (the
specification's backwards-compatibility clause). The new typed `ResourceNotFoundError`
class carries `data.uri`, and `ProtocolError.fromError` reconstructs it from a `-32602`
only when `error.data` is exactly `{ uri: string }` (and nothing else), and from a
legacy `-32002` whenever `data.uri` is a string; a bare `-32002` without `data.uri`
stays a generic `ProtocolError`.
6 changes: 6 additions & 0 deletions .changeset/sep-2577-deprecate-type-stacks.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
---
'@modelcontextprotocol/core': patch
'@modelcontextprotocol/client': patch
---

Complete the SEP-2577 `@deprecated` sweep on the public type surface (SEP-2596 Tier-1 obligation). Marks the full Logging type stack (`LoggingLevel`, `SetLevelRequest`, `LoggingMessageNotification` and params), the full Sampling type stack (`CreateMessageRequest`/`Result`, `SamplingMessage`, `ModelPreferences`/`ModelHint`, `ToolChoice`, `ToolUseContent`/`ToolResultContent`, and the `includeContext` enum values), the full Roots type stack (`Root`, `ListRootsRequest`/`Result`, `RootsListChangedNotification`), and `registerClient` (Dynamic Client Registration; prefer Client ID Metadata Documents per SEP-991). Mirrors the markers already present on the per-revision reference types. JSDoc only — wire behavior is unchanged; everything remains fully functional during the deprecation window (at least twelve months).
2 changes: 2 additions & 0 deletions docs/migration-SKILL.md
Original file line number Diff line number Diff line change
Expand Up @@ -582,6 +582,8 @@ into the same routing.

No code changes required; these are wire-behavior notes:

- `resources/read` for an unknown URI answers JSON-RPC error code `-32602` (Invalid Params) on every protocol revision, with `error.data.uri` echoing the requested URI. Earlier v2 alphas emitted `-32002`; v1.x already emitted `-32602`, so v1.x peers see no change. Throw the typed
`ResourceNotFoundError` from a custom resource handler; a handler-thrown `-32002` is still mapped to `-32602` on the wire by the encode seam. Clients accept both codes; `ProtocolErrorCode.ResourceNotFound` (`-32002`) stays importable as receive-tolerated vocabulary.
- Resumability behavior (SSE priming events, `closeSSEStream` / `closeStandaloneSSEStream` callbacks) is only enabled for protocol versions in the transport's supported-versions list that are `>= 2025-11-25`. Unknown future version strings in an `initialize` request body no
longer enable it. Behavior for all currently supported protocol versions is unchanged.
- Session-ID mismatch still responds `404 Not Found` with JSON-RPC error code `-32001` (`Session not found`), unchanged from v1. This `-32001` usage is an SDK convention, not a spec-assigned code, and may be re-derived as 2026 protocol revision error handling is adopted —
Expand Down
41 changes: 41 additions & 0 deletions docs/migration.md
Original file line number Diff line number Diff line change
Expand Up @@ -1286,6 +1286,31 @@ identifiers from rejections. See `examples/mrtr/server.ts` for a worked end-to-e
keep returning their plain result types — the interactive rounds happen inside the call, and a registered handler written for the 2025 flow keeps working unchanged. Configure or opt out via `ClientOptions.inputRequired` (`{ autoFulfill: false }`), drive the flow manually per call
with the `allowInputRequired: true` request option plus the `withInputRequired()` schema wrapper, and expect the typed `InputRequiredRoundsExceeded` error when the round cap is exhausted. 2025-era connections are unaffected (the legacy wire has no `input_required` vocabulary).

### Resource not found is `-32602` on every revision; typed `ResourceNotFoundError`

`resources/read` for an unknown URI now answers with JSON-RPC error code **`-32602` (Invalid Params)** on every protocol revision, with `error.data.uri` echoing the requested URI. The 2026-07-28 specification requires `-32602`; the v1.x SDK already emitted `-32602` on earlier
revisions, so v1.x peers see no change. An interim `-32002` emission that shipped in earlier v2 alphas is reverted: the era encode seam maps any handler-thrown `-32002` to `-32602` on the wire; note that a `-32002` thrown without `data.uri` is emitted as a bare `-32602` and is no longer recognizable as resource-not-found — throw `ResourceNotFoundError` (or include `data: { uri }`) to preserve the classification.

`ProtocolErrorCode.ResourceNotFound` (`-32002`) **remains importable** as receive-tolerated vocabulary: clients should accept both `-32602` and `-32002` from peers (the specification's backwards-compatibility clause). The new typed `ResourceNotFoundError` class carries the URI on
`.uri`, and `ProtocolError.fromError` reconstructs it from a `-32602` only when `error.data` is exactly `{ uri: string }` (and nothing else), and from a legacy `-32002` whenever `data.uri` is a string (a bare `-32002` without `data.uri` stays a generic `ProtocolError`) — recognize peers' errors by their code and `error.data`, not by `instanceof`, which does not survive
bundling. Servers must not return an empty `contents` array for a non-existent resource (an empty array is ambiguous between "exists but empty" and "does not exist").

```typescript
import { ProtocolError, ResourceNotFoundError } from '@modelcontextprotocol/client';

try {
await client.readResource({ uri: 'file:///nope' });
} catch (error) {
// fromError reconstructs the typed class from code + data alone, so this
// works even when `error` crossed a bundle boundary and `instanceof` on
// the thrown object would not match.
const e = error as ProtocolError;
if (ProtocolError.fromError(e.code, e.message, e.data) instanceof ResourceNotFoundError) {
console.log('not found:', (e.data as { uri: string }).uri);
}
}
```

### Typed `-32003` missing-client-capability error

`MissingRequiredClientCapabilityError` is the typed error class for the 2026-07-28 `-32003` protocol error: processing a request requires a capability the client did not declare in the request's `clientCapabilities`. Its `data.requiredCapabilities` lists the missing capabilities,
Expand Down Expand Up @@ -1394,6 +1419,22 @@ subpath in some files and rely on the default in others.

To replace validation wholesale rather than customizing the built-in classes, implement the `jsonSchemaValidator` interface and pass your own implementation through the option above.

## Specification clarifications adopted (no SDK behavior change)

The 2026-07-28 specification revision includes a number of documentation-only clarifications that do not change SDK wire behavior or public surface. They are recorded here so an audit of the revision's changelog against this guide is complete; nothing in this section requires
code changes.

- **Timeouts** — the specification's per-operation timeout guidance section was removed; the SDK's `RequestOptions.timeout` and `DEFAULT_REQUEST_TIMEOUT_MSEC` are unchanged.
- **stdio shutdown** — the specification clarifies stdio shutdown/termination wording; `StdioServerTransport`/`StdioClientTransport` close semantics are unchanged.
- **Transports as bindings** — the specification reframes transports as bindings of one protocol; the SDK's `Transport` interface is unchanged.

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🟡 The PR description's 'Small additives + doc closeout' section claims 'empty-string cursor pass-through verified', but no test in this diff or anywhere in the repo at the PR head sends an empty-string cursor — the only new test file (listOrdering.test.ts) never uses a cursor, and the pre-existing pagination tests only exercise non-empty cursors. The migration.md bullet itself is accurate (CursorSchema is z.string(), passed through verbatim), so this is a PR-description-accuracy nit: either add the small empty-cursor pass-through pin or drop the 'verified' wording so reviewers/changelog readers don't assume coverage that doesn't exist.

Extended reasoning...

What the issue is. The PR description's "Small additives + doc closeout" section lists two parallel bullets: "tools/list / prompts/list / resources/list deterministic registration-order verified at the wire (test pins existing behavior)" and "empty-string cursor pass-through verified". The first bullet is backed by a real deliverable in this diff (packages/server/test/server/listOrdering.test.ts). The second is not backed by anything: no test in this PR or anywhere in the repo at the PR head sends an empty-string cursor.

Verification. A repo-wide search at the PR head for an empty-string cursor assertion (cursor: '', \"cursor\": \"\", "empty cursor") across packages/server/test, packages/core/test, packages/client/test, test/integration, and test/e2e returns nothing. The existing cursor coverage (test/e2e/scenarios/pagination.test.ts and related cells) only exercises non-empty/opaque cursors plus an invalid-cursor rejection, and is untouched by this PR. The only new test file in this PR, listOrdering.test.ts, pins list ordering and never sends a cursor at all. The .changeset mention of empty-string cursors elsewhere in the repo belongs to a different PR.

Step-by-step reading a reviewer would make. (1) The sibling bullet says "verified at the wire (test pins existing behavior)" and ships a test. (2) The cursor bullet, in the same list, says "empty-string cursor pass-through verified". (3) The natural reading is that a similar small pin was added for the cursor case. (4) Searching the diff for any cursor test finds none — the claimed verification was not added. A future contributor or changelog reader relying on the description would believe coverage exists that doesn't, and a later refactor of cursor handling (e.g. a well-meaning cursor || undefined normalization) would not be caught by any pin.

Why the shipped artifacts are fine (and why this is only a nit). The new docs/migration.md bullet at this location is accurate: CursorSchema is z.string(), PaginatedRequestParamsSchema uses CursorSchema.optional(), and the client only gates on cursor !== undefined, so an empty-string cursor genuinely passes through verbatim and is not treated as end-of-results. Nothing shipped in the diff is wrong, and there is no runtime impact.

Addressing the counter-argument. One reviewer noted the bullet does not literally say "test pins" the way its sibling does, so "verified" could be read as the doc-closeout conclusion recorded in migration.md (schema/code inspection rather than a new test cell). That reading is possible, but the bullet sits in a list whose other verification item explicitly means a test pin, and "How Has This Been Tested" doesn't disambiguate — so at minimum the wording is ambiguous in a way this repo's conventions on prose-vs-diff accuracy try to avoid. Either resolution is cheap.

How to fix. Prose-only, pick one: (a) drop or reword the bullet (e.g. "empty-string cursor pass-through confirmed by inspection — CursorSchema is z.string(), no normalization on the path"), or (b) add the small pin the wording implies — a list request with cursor: '' reaching the handler verbatim (and/or a PaginatedRequestParamsSchema.parse({ cursor: '' }) round-trip), a few lines next to the new listOrdering test. Not blocking either way.

- **`resources/read` clarifications** — wording-only; behavior unchanged. The `file://` path-sanitization MUST is server-author guidance: a resource handler that resolves `file://` URIs to real paths is responsible for rejecting traversal (`..`) and symlink escapes itself — the SDK does not interpose on the path.
- **`PromptMessage` resource links** — `ContentBlock` already includes `ResourceLink` on every revision; no change.
- **Completion `ref/resource` URI templates** — documentation alignment; the SDK's `completion/complete` handling is unchanged.
- **Pagination cursors** — the specification clarifies that an empty-string cursor is a valid opaque cursor; the SDK already passes `cursor` through verbatim (it is `z.string().optional()`).
- **Sampling** — documentation of host requirements; no SDK surface change.
- **Elicitation** — the specification relaxes elicitation statefulness wording and removes a rate-limiting SHOULD; no SDK surface change.
- **Cosmetic schema/JSDoc sweeps** — phrasing alignment with the draft specification; the per-revision generated reference types remain pinned to the specification anchor.

## Unchanged APIs

The following APIs are unchanged between v1 and v2 (only the import paths changed):
Expand Down
6 changes: 6 additions & 0 deletions packages/client/src/client/auth.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1699,6 +1699,12 @@ export async function fetchToken(
* If `scope` is provided, it overrides `clientMetadata.scope` in the registration
* request body. This allows callers to apply the Scope Selection Strategy (SEP-835)
* consistently across both DCR and the subsequent authorization request.
*
* @deprecated Dynamic Client Registration is deprecated as of protocol version
* 2026-07-28 (SEP-2577) in favor of Client ID Metadata Documents (SEP-991).
* Remains functional during the deprecation window (at least twelve months).
* Prefer a CIMD URL `client_id` when the authorization server advertises
* `client_id_metadata_document_supported`; the SDK already gates on this for you.
*/
export async function registerClient(
authorizationServerUrl: string | URL,
Expand Down
1 change: 1 addition & 0 deletions packages/core/src/exports/public/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -101,6 +101,7 @@ export { ProtocolErrorCode } from '../../types/enums.js';
export {
MissingRequiredClientCapabilityError,
ProtocolError,
ResourceNotFoundError,
UnsupportedProtocolVersionError,
UrlElicitationRequiredError
} from '../../types/errors.js';
Comment thread
claude[bot] marked this conversation as resolved.
Expand Down
7 changes: 6 additions & 1 deletion packages/core/src/shared/protocol.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1055,11 +1055,16 @@ export abstract class Protocol<ContextT extends BaseContext> {
return;
}

// The error half of the encode seam: the era codec selects
// the wire code for a handler-thrown error, so per-era
// wire-code policy lives in the codec rather than in any
// handler. Non-integer codes still fall through to −32603.
const thrownCode = Number.isSafeInteger(error['code']) ? (error['code'] as number) : ProtocolErrorCode.InternalError;
const errorResponse: JSONRPCErrorResponse = {
jsonrpc: '2.0',
id: request.id,
error: {
code: Number.isSafeInteger(error['code']) ? error['code'] : ProtocolErrorCode.InternalError,
code: codec.encodeErrorCode(thrownCode),
message: error.message ?? 'Internal error',
...(error['data'] !== undefined && { data: error['data'] })
}
Expand Down
11 changes: 11 additions & 0 deletions packages/core/src/types/enums.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,17 @@ export enum ProtocolErrorCode {
InternalError = -32_603,

// MCP-specific error codes
/**
* Resource not found.
*
* Receive-tolerated only: the SDK never EMITS `-32002` — `resources/read`
* misses answer `-32602` (Invalid Params) on every protocol revision per
* the 2026-07-28 spec MUST, and a handler-thrown `-32002` is mapped to
* `-32602` at the era encode seam. The member stays importable so clients
* can recognise `-32002` from peers built on earlier SDK releases (the
* spec's "clients SHOULD also accept `-32002`" backwards-compatibility
* clause). Throw `ResourceNotFoundError` instead.
*/
ResourceNotFound = -32_002,
/**
* Processing the request requires a capability the client did not declare
Expand Down
46 changes: 46 additions & 0 deletions packages/core/src/types/errors.ts
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,24 @@ export class ProtocolError extends Error {
}
}

// Resource not found is recognised on BOTH the spec-mandated −32602 and
// the legacy −32002 (the spec's "clients SHOULD also accept −32002"
// backwards-compatibility clause). On −32602 the data-shape parse is
// narrowed to "exactly `{ uri: string }` and nothing else": a server's
// own Invalid Params that happens to carry `data.uri` alongside other
// keys (e.g. `{ uri, reason: 'uri must be https' }`) stays a generic
// ProtocolError. On −32002 the code itself is the discriminator, so any
// `data.uri` string suffices.
if (code === ProtocolErrorCode.InvalidParams || code === ProtocolErrorCode.ResourceNotFound) {
const errorData = data as Record<string, unknown> | undefined;
if (
typeof errorData?.uri === 'string' &&
(code === ProtocolErrorCode.ResourceNotFound || Object.keys(errorData).length === 1)
) {
return new ResourceNotFoundError(errorData.uri, message);
}
}

Comment thread
claude[bot] marked this conversation as resolved.
if (code === ProtocolErrorCode.MissingRequiredClientCapability && data) {
const errorData = data as Partial<MissingRequiredClientCapabilityErrorData>;
if (
Expand All @@ -55,6 +73,34 @@ export class ProtocolError extends Error {
}
}

/**
* Error type for a `resources/read` miss: the requested resource does not
* exist. The wire code is `-32602` (Invalid Params) on every protocol
* revision — the spec MUST for revision 2026-07-28, and the value the v1.x
* SDK has always emitted on earlier revisions. The error data echoes the
* requested URI.
*
* Recognise this error by checking `error.data` is exactly `{ uri: string }`
* (a `-32602` whose data carries `uri` and nothing else is resource-not-found;
* any other `-32602` is an ordinary Invalid Params). For backwards compatibility, clients should also
* accept `-32002` as resource not found — earlier SDK builds emitted that
* code, and {@linkcode ProtocolError.fromError} reconstructs this class for
* either code **when `error.data` carries `uri`** (a bare `-32002` without
* `data.uri` stays a generic {@linkcode ProtocolError}). Do not rely on
* `instanceof` — it does not work across separately bundled copies of the
* SDK.
*/
export class ResourceNotFoundError extends ProtocolError {
constructor(uri: string, message: string = `Resource not found: ${uri}`) {
super(ProtocolErrorCode.InvalidParams, message, { uri });
}

/** The URI that was requested and not found. */
get uri(): string {
return (this.data as { uri: string }).uri;
}
}

/**
* Specialized error type when a tool requires a URL mode elicitation.
* This makes it nicer for the client to handle since there is specific data to work with instead of just a code to check against.
Expand Down
Loading
Loading