fix(core): make instanceof on SDK error classes work across bundled copies (#2384)
This commit is contained in:
parent
e6cc5dc633
commit
ce2f65db0e
2 changed files with 1020 additions and 26 deletions
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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.
|
||||
|
|
|
|||
Loading…
Reference in a new issue