fix(core): make instanceof on SDK error classes work across bundled copies (#2384)

This commit is contained in:
Felix Weinberger 2026-07-06 15:53:12 +01:00 • committed by GitHub
commit ce2f65db0e
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
2 changed files with 1020 additions and 26 deletions

View file

@ -825,6 +825,28 @@ The SDK now distinguishes three error kinds:
3. **`SdkHttpError`** (extends `SdkError`) — HTTP transport errors with typed `.status`
and `.statusText`.
These classes (and `OAuthError`, the client's `SseError`, `UnauthorizedError`, and the
OAuth-client-flow error family) brand-match under `instanceof`, so checks work across
separately bundled copies of the SDK — e.g. a process using both
`@modelcontextprotocol/client` and `@modelcontextprotocol/server`. Each branded
hierarchy also exposes the same check as an explicit static guard
(`SdkError.isInstance(err)`, `ProtocolError.isInstance(err)`, …) that narrows in
TypeScript — use whichever style your codebase prefers; both read the same brand.
Fine print (applies equally to `instanceof` and `isInstance`):
- **Version skew** — matching needs *both* copies at a brand-aware release; against an
older copy, behavior degrades to plain prototype `instanceof` (false across bundles).
During mixed-version rollouts, recognize errors without class identity: match
`error.name` plus the class's discriminant field (`code`, `status`), or reconstruct
typed protocol errors with `ProtocolError.fromError(code, message, data)`.
- **Worker boundaries** — `structuredClone`/`postMessage` drop the (symbol-keyed) brand,
so a rehydrated error no longer brand-matches; recognize forwarded errors by
`code`/`data` instead.
- **Brands assert identity, not shape** — a matched instance from another SDK version
may lack newer fields; read fields defensively.
- **Re-bundling with property mangling** (`mangle.props` and similar) breaks the brand
statics; default esbuild/webpack/terser settings are safe.
The codemod renames `McpError` → `ProtocolError`, `ErrorCode` → `ProtocolErrorCode`
(routing `RequestTimeout` / `ConnectionClosed` to `SdkErrorCode`), and
`StreamableHTTPError` → `SdkHttpError`. After the codemod runs, your `instanceof`
@ -982,7 +1004,8 @@ peers as `-32602` — a server can no longer emit `-32002` on the wire.
`ProtocolErrorCode.ResourceNotFound` (`-32002`) stays importable as
receive-tolerated vocabulary — accept both `-32602` and `-32002` from peers.
`ProtocolError.fromError(code, message, data)` reconstructs the typed subclass from
code + data alone, so it works across bundle boundaries where `instanceof` doesn't.
code + data alone — the version-agnostic path: it also works on plain wire shapes and
against SDK copies that predate brand-matched `instanceof`.
The default message text changed alongside: v1's unknown-resource error read
`Resource <uri> not found`; v2's `ResourceNotFoundError` default is
`Resource not found: <uri>` (the code is unchanged). Tests pinning the exact string

View file

@ -179,7 +179,7 @@ Three more subclasses cover the other structured protocol errors:
- `UnsupportedProtocolVersionError({ supported, requested })` — `-32022`; `data.supported` lets the peer pick a version and retry.
- `MissingRequiredClientCapabilityError({ requiredCapabilities })` — `-32021`; `data.requiredCapabilities` names exactly what the client must declare.
Match these by `code` and `data` shape, not by `instanceof` — `instanceof` fails across separately bundled copies of the SDK.
Match these by `code` and `data` shape when peers may run pre-brand SDK copies or hand you plain wire shapes; on brand-aware releases `instanceof` also matches across separately bundled copies of the SDK. The same check is available as an explicit static guard — `ProtocolError.isInstance(err)`, `ResourceNotFoundError.isInstance(err)` — which narrows in TypeScript and reads the same brand.
## Look up a protocol error code
@ -205,5 +205,5 @@ Match these by `code` and `data` shape, not by `instanceof` — `instanceof` fai
- A tool handler that throws produces the same `isError: true` result; the exception's `message` becomes the `content` text.
- A tool handler cannot produce a protocol error — only `UrlElicitationRequiredError` escapes.
- `ProtocolError` and its subclasses, thrown from resource, prompt, and completion callbacks, become JSON-RPC error responses the model never sees.
- `ResourceNotFoundError` and the other subclasses pick the code and pack structured `data`; match them by `code` and `data`, not `instanceof`.
- `ResourceNotFoundError` and the other subclasses pick the code and pack structured `data`; match them by `code` and `data` — or, on brand-aware releases, by `instanceof`.
- The table above lists every `ProtocolErrorCode` member.