feat(compat): McpError/ErrorCode/JSONRPCError/StreamableHTTPError + OAuth subclass aliases#1903
feat(compat): McpError/ErrorCode/JSONRPCError/StreamableHTTPError + OAuth subclass aliases#1903felixweinberger wants to merge 9 commits intomainfrom
Conversation
🦋 Changeset detectedLatest commit: 6221eda The changes in this PR will be included in the next version bump. This PR includes changesets to release 6 packages
Not sure what this means? Click here to learn what changesets are. Click here if you're a maintainer who wants to add another changeset to this PR |
@modelcontextprotocol/client
@modelcontextprotocol/server
@modelcontextprotocol/express
@modelcontextprotocol/fastify
@modelcontextprotocol/hono
@modelcontextprotocol/node
commit: |
8a205d8 to
e14ec72
Compare
|
@claude review |
…Auth subclass aliases
e14ec72 to
0213418
Compare
|
@claude review |
…rt cfWorker.ts import-sort noise
|
@claude review |
…ike v1); fix changeset count
|
@claude review |
…ableHTTPError instanceof+status assertion; drop redundant cast
7cab445 to
36d4fb1
Compare
|
@claude review |
…ngeset collision-symbol reference
| export class CustomOAuthError extends OAuthError { | ||
| static errorCode: string; | ||
| constructor(message: string, errorUri?: string) { | ||
| super((new.target as typeof CustomOAuthError).errorCode, message, errorUri); | ||
| } | ||
| } |
There was a problem hiding this comment.
🟡 User subclasses of CustomOAuthError get .name === 'OAuthError' instead of the subclass name — v1's base OAuthError constructor did this.name = this.constructor.name, so class MyErr extends CustomOAuthError { static errorCode = '…' } produced new MyErr('x').name === 'MyErr'. This is the same .name-fidelity gap already fixed for the 17 sub() classes in 473d2d8, but CustomOAuthError was missed; one-line fix: add this.name = new.target.name; after the super() call.
Extended reasoning...
What's the gap
v1's OAuthError base constructor (verified against src/server/auth/errors.ts @ 1.21.x) did this.name = this.constructor.name. So for the documented v1 pattern this file's JSDoc explicitly preserves —
class MyErr extends CustomOAuthError { static errorCode = 'my_code' }— v1 produced new MyErr('x').name === 'MyErr', and err.toString() / stack traces showed MyErr: x.
In this PR, v2's OAuthError constructor (packages/core/src/auth/errors.ts:108) hardcodes this.name = 'OAuthError', and CustomOAuthError's constructor (lines 109-111) only does super((new.target as typeof CustomOAuthError).errorCode, message, errorUri) without resetting .name. So new MyErr('x').name === 'OAuthError'.
Why nothing else covers it
The 17 sub()-generated classes already handle this — line 23 sets this.name = name in each subclass constructor, and 473d2d8 added Object.defineProperty(Sub, 'name', …) for constructor.name. CustomOAuthError is the one OAuth compat class that doesn't go through sub(), and its constructor was simply not given the equivalent line. Note that CustomOAuthError is a named class declaration, so MyErr.name / err.constructor.name are already correct ('MyErr') — only the instance .name property (and therefore .toString() / stack-trace label) is wrong.
The existing test in compat.test.ts ("CustomOAuthError reads static errorCode from concrete subclass") only asserts e.code and instanceof, so it doesn't catch this.
Step-by-step proof
auth/errors.ts:108— v2OAuthErrorctor:this.name = 'OAuthError'(hardcoded, notthis.constructor.name).oauthErrorsCompat.ts:109-111—CustomOAuthErrorctor:super((new.target as …).errorCode, message, errorUri); nothis.name = …afterward.- User defines
class MyErr extends CustomOAuthError { static override errorCode = 'my_code' }. const e = new MyErr('nope')→ super chain runs step 1 →e.name === 'OAuthError'.e.toString()→'OAuthError: nope'; stack trace header readsOAuthError: nope.- Under v1, step 1 instead did
this.name = this.constructor.name→e.name === 'MyErr',e.toString()→'MyErr: nope'.
Impact
Low — only affects err.name, err.toString(), and stack-trace labeling for the niche user-defined-subclass escape hatch on a @deprecated shim. instanceof MyErr, instanceof OAuthError, .code, .errorCode, and toResponseObject() all work correctly. But it's the same v1-fidelity category the author already chose to fix for sub() in 473d2d8 (resolved thread #3099495886), and this is the sibling code path that was overlooked.
Fix
export class CustomOAuthError extends OAuthError {
static errorCode: string;
constructor(message: string, errorUri?: string) {
super((new.target as typeof CustomOAuthError).errorCode, message, errorUri);
this.name = new.target.name;
}
}new.target already points at the user's concrete subclass (it's used on the line above for errorCode), so new.target.name gives 'MyErr' exactly as v1's this.constructor.name did. Optionally add expect(e.name).toBe('MyError') to the existing CustomOAuthError test.
Part of the v2 backwards-compatibility series — see reviewer guide.
v2 split
McpErrorintoProtocolError/SdkErrorand consolidated 17 OAuth error classes intoOAuthError+OAuthErrorCode. This restores the v1 names as@deprecatedaliases/subclasses.Motivation and Context
v2 split
McpErrorintoProtocolError/SdkErrorand consolidated 17 OAuth error classes intoOAuthError+OAuthErrorCode. This restores the v1 names as@deprecatedaliases/subclasses.v1 vs v2 pattern & evidence
v1 pattern:
v2-native:
Evidence: GitHub code search: ~1,540 files reference
McpError/ErrorCode.ErrorCode.ConnectionClosed/RequestTimeoutshimmed toSdkErrorCodevalues so comparisons work (string-vs-number assignment toerror.code: numberis irreducible — 1-line consumer fix).How Has This Been Tested?
v2-bc-integrationvalidation branchpnpm typecheck:all && pnpm lint:all && pnpm test:allgreenBreaking Changes
None — additive
@deprecatedshim.Types of changes
Checklist
Additional context
Stacks on: C1