From 021341891982807746997c92dfc952741d6f3487 Mon Sep 17 00:00:00 2001 From: Felix Weinberger Date: Wed, 15 Apr 2026 12:21:11 +0000 Subject: [PATCH 1/7] feat(compat): McpError/ErrorCode/JSONRPCError/StreamableHTTPError + OAuth subclass aliases --- .changeset/error-compat-aliases.md | 5 ++ packages/client/src/client/streamableHttp.ts | 54 ++++++++----- packages/client/src/validators/cfWorker.ts | 2 +- .../client/test/client/streamableHttp.test.ts | 4 +- packages/core/src/auth/errors.ts | 7 ++ packages/core/src/errors/oauthErrorsCompat.ts | 76 +++++++++++++++++++ .../src/errors/streamableHttpErrorCompat.ts | 20 +++++ packages/core/src/exports/public/index.ts | 51 +++++++++++++ packages/core/src/index.ts | 1 + packages/core/test/errors/compat.test.ts | 68 +++++++++++++++++ 10 files changed, 266 insertions(+), 22 deletions(-) create mode 100644 .changeset/error-compat-aliases.md create mode 100644 packages/core/src/errors/oauthErrorsCompat.ts create mode 100644 packages/core/src/errors/streamableHttpErrorCompat.ts create mode 100644 packages/core/test/errors/compat.test.ts diff --git a/.changeset/error-compat-aliases.md b/.changeset/error-compat-aliases.md new file mode 100644 index 000000000..a1bf31054 --- /dev/null +++ b/.changeset/error-compat-aliases.md @@ -0,0 +1,5 @@ +--- +'@modelcontextprotocol/core': patch +--- + +Add v1-compat error aliases: `McpError`/`ErrorCode` (alias `ProtocolError`/`ProtocolErrorCode`, with `ConnectionClosed`/`RequestTimeout` from `SdkErrorCode`), `OAuthError.errorCode` getter (alias `.code`), `JSONRPCError`/`isJSONRPCError`, the 17 v1 OAuth error subclasses (`InvalidTokenError`, `ServerError`, …) as thin wrappers around `OAuthError` + `OAuthErrorCode`, and `StreamableHTTPError` (construct-only `@deprecated` shim; v2 throws `SdkError`). diff --git a/packages/client/src/client/streamableHttp.ts b/packages/client/src/client/streamableHttp.ts index cd643c96d..855f59334 100644 --- a/packages/client/src/client/streamableHttp.ts +++ b/packages/client/src/client/streamableHttp.ts @@ -9,8 +9,8 @@ import { isJSONRPCResultResponse, JSONRPCMessageSchema, normalizeHeaders, - SdkError, - SdkErrorCode + SdkErrorCode, + StreamableHTTPError } from '@modelcontextprotocol/core'; import { EventSourceParserStream } from 'eventsource-parser/stream'; @@ -273,9 +273,13 @@ export class StreamableHTTPClientTransport implements Transport { } await response.text?.().catch(() => {}); if (isAuthRetry) { - throw new SdkError(SdkErrorCode.ClientHttpAuthentication, 'Server returned 401 after re-authentication', { - status: 401 - }); + throw new StreamableHTTPError( + SdkErrorCode.ClientHttpAuthentication, + 'Server returned 401 after re-authentication', + { + status: 401 + } + ); } throw new UnauthorizedError(); } @@ -288,10 +292,14 @@ export class StreamableHTTPClientTransport implements Transport { return; } - throw new SdkError(SdkErrorCode.ClientHttpFailedToOpenStream, `Failed to open SSE stream: ${response.statusText}`, { - status: response.status, - statusText: response.statusText - }); + throw new StreamableHTTPError( + SdkErrorCode.ClientHttpFailedToOpenStream, + `Failed to open SSE stream: ${response.statusText}`, + { + status: response.status, + statusText: response.statusText + } + ); } this._handleSseStream(response.body, options, true); @@ -581,9 +589,13 @@ export class StreamableHTTPClientTransport implements Transport { } await response.text?.().catch(() => {}); if (isAuthRetry) { - throw new SdkError(SdkErrorCode.ClientHttpAuthentication, 'Server returned 401 after re-authentication', { - status: 401 - }); + throw new StreamableHTTPError( + SdkErrorCode.ClientHttpAuthentication, + 'Server returned 401 after re-authentication', + { + status: 401 + } + ); } throw new UnauthorizedError(); } @@ -598,7 +610,7 @@ export class StreamableHTTPClientTransport implements Transport { // Check if we've already tried upscoping with this header to prevent infinite loops. if (this._lastUpscopingHeader === wwwAuthHeader) { - throw new SdkError(SdkErrorCode.ClientHttpForbidden, 'Server returned 403 after trying upscoping', { + throw new StreamableHTTPError(SdkErrorCode.ClientHttpForbidden, 'Server returned 403 after trying upscoping', { status: 403, text }); @@ -629,7 +641,7 @@ export class StreamableHTTPClientTransport implements Transport { } } - throw new SdkError(SdkErrorCode.ClientHttpNotImplemented, `Error POSTing to endpoint: ${text}`, { + throw new StreamableHTTPError(SdkErrorCode.ClientHttpNotImplemented, `Error POSTing to endpoint: ${text}`, { status: response.status, text }); @@ -675,7 +687,7 @@ export class StreamableHTTPClientTransport implements Transport { } } else { await response.text?.().catch(() => {}); - throw new SdkError(SdkErrorCode.ClientHttpUnexpectedContent, `Unexpected content type: ${contentType}`, { + throw new StreamableHTTPError(SdkErrorCode.ClientHttpUnexpectedContent, `Unexpected content type: ${contentType}`, { contentType }); } @@ -725,10 +737,14 @@ export class StreamableHTTPClientTransport implements Transport { // We specifically handle 405 as a valid response according to the spec, // meaning the server does not support explicit session termination if (!response.ok && response.status !== 405) { - throw new SdkError(SdkErrorCode.ClientHttpFailedToTerminateSession, `Failed to terminate session: ${response.statusText}`, { - status: response.status, - statusText: response.statusText - }); + throw new StreamableHTTPError( + SdkErrorCode.ClientHttpFailedToTerminateSession, + `Failed to terminate session: ${response.statusText}`, + { + status: response.status, + statusText: response.statusText + } + ); } this._sessionId = undefined; diff --git a/packages/client/src/validators/cfWorker.ts b/packages/client/src/validators/cfWorker.ts index b068e69a1..7d1c843e5 100644 --- a/packages/client/src/validators/cfWorker.ts +++ b/packages/client/src/validators/cfWorker.ts @@ -6,5 +6,5 @@ * import { CfWorkerJsonSchemaValidator } from '@modelcontextprotocol/client/validators/cf-worker'; * ``` */ -export { CfWorkerJsonSchemaValidator } from '@modelcontextprotocol/core'; export type { CfWorkerSchemaDraft } from '@modelcontextprotocol/core'; +export { CfWorkerJsonSchemaValidator } from '@modelcontextprotocol/core'; diff --git a/packages/client/test/client/streamableHttp.test.ts b/packages/client/test/client/streamableHttp.test.ts index b2138b3fa..19290f7b0 100644 --- a/packages/client/test/client/streamableHttp.test.ts +++ b/packages/client/test/client/streamableHttp.test.ts @@ -1,5 +1,5 @@ import type { JSONRPCMessage, JSONRPCRequest } from '@modelcontextprotocol/core'; -import { OAuthError, OAuthErrorCode, SdkError, SdkErrorCode } from '@modelcontextprotocol/core'; +import { OAuthError, OAuthErrorCode, SdkError, SdkErrorCode, StreamableHTTPError } from '@modelcontextprotocol/core'; import type { Mock, Mocked } from 'vitest'; import type { OAuthClientProvider } from '../../src/client/auth.js'; @@ -240,7 +240,7 @@ describe('StreamableHTTPClientTransport', () => { transport.onerror = errorSpy; await expect(transport.send(message)).rejects.toThrow( - new SdkError(SdkErrorCode.ClientHttpNotImplemented, 'Error POSTing to endpoint: Session not found', { + new StreamableHTTPError(SdkErrorCode.ClientHttpNotImplemented, 'Error POSTing to endpoint: Session not found', { status: 404, text: 'Session not found' }) diff --git a/packages/core/src/auth/errors.ts b/packages/core/src/auth/errors.ts index 30c874160..594213a85 100644 --- a/packages/core/src/auth/errors.ts +++ b/packages/core/src/auth/errors.ts @@ -107,6 +107,13 @@ export class OAuthError extends Error { this.name = 'OAuthError'; } + /** + * @deprecated Use {@linkcode OAuthError.code | .code}. + */ + get errorCode(): string { + return this.code; + } + /** * Converts the error to a standard OAuth error response object. */ diff --git a/packages/core/src/errors/oauthErrorsCompat.ts b/packages/core/src/errors/oauthErrorsCompat.ts new file mode 100644 index 000000000..e81e9eb2d --- /dev/null +++ b/packages/core/src/errors/oauthErrorsCompat.ts @@ -0,0 +1,76 @@ +/** + * v1-compat: OAuth error subclasses. + * + * v1 shipped one `Error` subclass per OAuth error code (e.g. `InvalidTokenError`). + * v2 also exposes the consolidated {@link OAuthError} + {@link OAuthErrorCode} enum. + * These thin wrappers preserve `throw new InvalidTokenError(msg)` and `instanceof` + * patterns from v1 and set `.code` to the matching enum value. + */ + +import { OAuthError, OAuthErrorCode } from '../auth/errors.js'; + +type OAuthErrorSubclass = { + new (message: string, errorUri?: string): OAuthError; + /** @deprecated Use the instance `.code` property. */ + errorCode: string; +}; + +function sub(code: OAuthErrorCode, name: string): OAuthErrorSubclass { + return class extends OAuthError { + static errorCode = code as string; + constructor(message: string, errorUri?: string) { + super(code, message, errorUri); + this.name = name; + } + }; +} + +/* eslint-disable @typescript-eslint/naming-convention */ + +/** @deprecated Use `OAuthError` with `OAuthErrorCode.InvalidRequest`. */ +export const InvalidRequestError = sub(OAuthErrorCode.InvalidRequest, 'InvalidRequestError'); +/** @deprecated Use `OAuthError` with `OAuthErrorCode.InvalidClient`. */ +export const InvalidClientError = sub(OAuthErrorCode.InvalidClient, 'InvalidClientError'); +/** @deprecated Use `OAuthError` with `OAuthErrorCode.InvalidGrant`. */ +export const InvalidGrantError = sub(OAuthErrorCode.InvalidGrant, 'InvalidGrantError'); +/** @deprecated Use `OAuthError` with `OAuthErrorCode.UnauthorizedClient`. */ +export const UnauthorizedClientError = sub(OAuthErrorCode.UnauthorizedClient, 'UnauthorizedClientError'); +/** @deprecated Use `OAuthError` with `OAuthErrorCode.UnsupportedGrantType`. */ +export const UnsupportedGrantTypeError = sub(OAuthErrorCode.UnsupportedGrantType, 'UnsupportedGrantTypeError'); +/** @deprecated Use `OAuthError` with `OAuthErrorCode.InvalidScope`. */ +export const InvalidScopeError = sub(OAuthErrorCode.InvalidScope, 'InvalidScopeError'); +/** @deprecated Use `OAuthError` with `OAuthErrorCode.AccessDenied`. */ +export const AccessDeniedError = sub(OAuthErrorCode.AccessDenied, 'AccessDeniedError'); +/** @deprecated Use `OAuthError` with `OAuthErrorCode.ServerError`. */ +export const ServerError = sub(OAuthErrorCode.ServerError, 'ServerError'); +/** @deprecated Use `OAuthError` with `OAuthErrorCode.TemporarilyUnavailable`. */ +export const TemporarilyUnavailableError = sub(OAuthErrorCode.TemporarilyUnavailable, 'TemporarilyUnavailableError'); +/** @deprecated Use `OAuthError` with `OAuthErrorCode.UnsupportedResponseType`. */ +export const UnsupportedResponseTypeError = sub(OAuthErrorCode.UnsupportedResponseType, 'UnsupportedResponseTypeError'); +/** @deprecated Use `OAuthError` with `OAuthErrorCode.UnsupportedTokenType`. */ +export const UnsupportedTokenTypeError = sub(OAuthErrorCode.UnsupportedTokenType, 'UnsupportedTokenTypeError'); +/** @deprecated Use `OAuthError` with `OAuthErrorCode.InvalidToken`. */ +export const InvalidTokenError = sub(OAuthErrorCode.InvalidToken, 'InvalidTokenError'); +/** @deprecated Use `OAuthError` with `OAuthErrorCode.MethodNotAllowed`. */ +export const MethodNotAllowedError = sub(OAuthErrorCode.MethodNotAllowed, 'MethodNotAllowedError'); +/** @deprecated Use `OAuthError` with `OAuthErrorCode.TooManyRequests`. */ +export const TooManyRequestsError = sub(OAuthErrorCode.TooManyRequests, 'TooManyRequestsError'); +/** @deprecated Use `OAuthError` with `OAuthErrorCode.InvalidClientMetadata`. */ +export const InvalidClientMetadataError = sub(OAuthErrorCode.InvalidClientMetadata, 'InvalidClientMetadataError'); +/** @deprecated Use `OAuthError` with `OAuthErrorCode.InsufficientScope`. */ +export const InsufficientScopeError = sub(OAuthErrorCode.InsufficientScope, 'InsufficientScopeError'); +/** @deprecated Use `OAuthError` with `OAuthErrorCode.InvalidTarget`. */ +export const InvalidTargetError = sub(OAuthErrorCode.InvalidTarget, 'InvalidTargetError'); + +/** + * @deprecated Construct {@link OAuthError} directly with a custom code string. + * + * v1 pattern was `class MyErr extends CustomOAuthError { static errorCode = 'my_code' }`; + * this preserves that by reading `static errorCode` from the concrete subclass. + */ +export class CustomOAuthError extends OAuthError { + static errorCode: string; + constructor(message: string, errorUri?: string) { + super((new.target as typeof CustomOAuthError).errorCode, message, errorUri); + } +} diff --git a/packages/core/src/errors/streamableHttpErrorCompat.ts b/packages/core/src/errors/streamableHttpErrorCompat.ts new file mode 100644 index 000000000..959f0b3a6 --- /dev/null +++ b/packages/core/src/errors/streamableHttpErrorCompat.ts @@ -0,0 +1,20 @@ +import type { SdkErrorCode } from './sdkErrors.js'; +import { SdkError } from './sdkErrors.js'; + +/** + * @deprecated Use {@linkcode SdkError}. + * + * Subclass thrown by the StreamableHTTP client transport for HTTP-level errors. + * `instanceof StreamableHTTPError` and `instanceof SdkError` both match. Note that + * `.code` is now the {@linkcode SdkErrorCode} (a `ClientHttp*` string), not the HTTP + * status number as in v1; the status is available as `.status`. + */ +export class StreamableHTTPError extends SdkError { + public readonly status: number | undefined; + + constructor(code: SdkErrorCode, message: string, data?: { status?: number } & Record) { + super(code, message, data); + this.name = 'StreamableHTTPError'; + this.status = data?.status; + } +} diff --git a/packages/core/src/exports/public/index.ts b/packages/core/src/exports/public/index.ts index 2dc1e13a8..126444ff4 100644 --- a/packages/core/src/exports/public/index.ts +++ b/packages/core/src/exports/public/index.ts @@ -100,6 +100,55 @@ export { ProtocolErrorCode } from '../../types/enums.js'; // Error classes export { ProtocolError, UrlElicitationRequiredError } from '../../types/errors.js'; +// --- v1-compat aliases --- +import { SdkErrorCode as _SdkErrorCode } from '../../errors/sdkErrors.js'; +import { ProtocolErrorCode as _ProtocolErrorCode } from '../../types/enums.js'; +/** + * @deprecated Use {@linkcode ProtocolErrorCode} for protocol-level (wire) errors + * or {@linkcode SdkErrorCode} for local SDK errors. Note `ConnectionClosed` / + * `RequestTimeout` moved to `SdkErrorCode` in v2 and are now thrown as `SdkError`, + * not `ProtocolError`. + */ +export const ErrorCode = { + ..._ProtocolErrorCode, + /** Now {@linkcode SdkErrorCode.ConnectionClosed}; thrown as `SdkError`, not `McpError`. */ + ConnectionClosed: _SdkErrorCode.ConnectionClosed, + /** Now {@linkcode SdkErrorCode.RequestTimeout}; thrown as `SdkError`, not `McpError`. */ + RequestTimeout: _SdkErrorCode.RequestTimeout +} as const; +/** @deprecated Use `ProtocolErrorCode` / `SdkErrorCode`. See {@linkcode ErrorCode} const. */ +export type ErrorCode = _ProtocolErrorCode | typeof _SdkErrorCode.ConnectionClosed | typeof _SdkErrorCode.RequestTimeout; +export { + /** @deprecated Use {@linkcode ProtocolError} (or `SdkError` for transport-level errors). */ + ProtocolError as McpError +} from '../../types/errors.js'; +// Note: InvalidRequestError is intentionally omitted here — it collides with the +// JSON-RPC `InvalidRequestError` interface re-exported from types.ts below. v1 users +// imported it from `server/auth/errors.js`, which the sdk meta-package subpath provides. +export { + AccessDeniedError, + CustomOAuthError, + InsufficientScopeError, + InvalidClientError, + InvalidClientMetadataError, + InvalidGrantError, + InvalidScopeError, + InvalidTargetError, + InvalidTokenError, + MethodNotAllowedError, + ServerError, + TemporarilyUnavailableError, + TooManyRequestsError, + UnauthorizedClientError, + UnsupportedGrantTypeError, + UnsupportedResponseTypeError, + UnsupportedTokenTypeError +} from '../../errors/oauthErrorsCompat.js'; +export { StreamableHTTPError } from '../../errors/streamableHttpErrorCompat.js'; +/** @deprecated Use {@linkcode JSONRPCErrorResponse}. */ +export type { JSONRPCErrorResponse as JSONRPCError } from '../../types/spec.types.js'; +// --- end v1-compat --- + // Type guards and message parsing export { assertCompleteRequestPrompt, @@ -107,6 +156,8 @@ export { isCallToolResult, isInitializedNotification, isInitializeRequest, + /** @deprecated Use {@linkcode isJSONRPCErrorResponse}. */ + isJSONRPCErrorResponse as isJSONRPCError, isJSONRPCErrorResponse, isJSONRPCNotification, isJSONRPCRequest, diff --git a/packages/core/src/index.ts b/packages/core/src/index.ts index e707d9939..54fac282d 100644 --- a/packages/core/src/index.ts +++ b/packages/core/src/index.ts @@ -1,5 +1,6 @@ export * from './auth/errors.js'; export * from './errors/sdkErrors.js'; +export * from './errors/streamableHttpErrorCompat.js'; export * from './shared/auth.js'; export * from './shared/authUtils.js'; export * from './shared/metadataUtils.js'; diff --git a/packages/core/test/errors/compat.test.ts b/packages/core/test/errors/compat.test.ts new file mode 100644 index 000000000..7855c4fc7 --- /dev/null +++ b/packages/core/test/errors/compat.test.ts @@ -0,0 +1,68 @@ +import { describe, expect, it, vi } from 'vitest'; +import { + ErrorCode, + InvalidTokenError, + McpError, + OAuthError, + OAuthErrorCode, + ProtocolError, + ProtocolErrorCode, + SdkError, + SdkErrorCode, + StreamableHTTPError +} from '../../src/exports/public/index.js'; +import { CustomOAuthError, ServerError } from '../../src/errors/oauthErrorsCompat.js'; + +describe('v1-compat error aliases', () => { + it('McpError / ErrorCode alias ProtocolError / ProtocolErrorCode (+ ConnectionClosed/RequestTimeout from SdkErrorCode)', () => { + expect(McpError).toBe(ProtocolError); + expect(ErrorCode.InvalidParams).toBe(ProtocolErrorCode.InvalidParams); + expect(ErrorCode.ConnectionClosed).toBe(SdkErrorCode.ConnectionClosed); + expect(ErrorCode.RequestTimeout).toBe(SdkErrorCode.RequestTimeout); + const e = new McpError(ErrorCode.InvalidParams, 'x'); + expect(e).toBeInstanceOf(ProtocolError); + expect(e.code).toBe(ProtocolErrorCode.InvalidParams); + }); + + it('OAuthError.errorCode getter returns .code (no warning)', () => { + const spy = vi.spyOn(console, 'warn').mockImplementation(() => {}); + const e = new OAuthError(OAuthErrorCode.InvalidToken, 'bad'); + expect(e.errorCode).toBe(OAuthErrorCode.InvalidToken); + expect(e.errorCode).toBe('invalid_token'); + expect(spy).not.toHaveBeenCalled(); + spy.mockRestore(); + }); + + it('InvalidTokenError is an OAuthError with .code = InvalidToken (no warning)', () => { + const spy = vi.spyOn(console, 'warn').mockImplementation(() => {}); + const e = new InvalidTokenError('expired'); + expect(e).toBeInstanceOf(OAuthError); + expect(e.code).toBe(OAuthErrorCode.InvalidToken); + expect(e.message).toBe('expired'); + expect(spy).not.toHaveBeenCalled(); + spy.mockRestore(); + }); + + it('subclass static errorCode and toResponseObject() match v1 wire format', () => { + expect(ServerError.errorCode).toBe('server_error'); + const e = new ServerError('boom'); + expect(e.toResponseObject()).toEqual({ error: 'server_error', error_description: 'boom' }); + }); + + it('CustomOAuthError reads static errorCode from concrete subclass', () => { + class MyError extends CustomOAuthError { + static override errorCode = 'my_custom_code'; + } + const e = new MyError('nope'); + expect(e).toBeInstanceOf(OAuthError); + expect(e.code).toBe('my_custom_code'); + }); + + it('StreamableHTTPError is an SdkError subclass with .status from data', () => { + const e = new StreamableHTTPError(SdkErrorCode.ClientHttpFailedToOpenStream, 'Service Unavailable', { status: 503 }); + expect(e).toBeInstanceOf(SdkError); + expect(e.code).toBe(SdkErrorCode.ClientHttpFailedToOpenStream); + expect(e.status).toBe(503); + expect(e.name).toBe('StreamableHTTPError'); + }); +}); From 57c85bc327c7eb28b5701ffddf6a3051af46f671 Mon Sep 17 00:00:00 2001 From: Felix Weinberger Date: Thu, 16 Apr 2026 20:55:17 +0000 Subject: [PATCH 2/7] ci: retrigger (cfworker port-8787 flake) From 582ac6792846e855277420aca5faf8a04a3db0d8 Mon Sep 17 00:00:00 2001 From: Felix Weinberger Date: Thu, 16 Apr 2026 21:12:40 +0000 Subject: [PATCH 3/7] fix: changeset includes client+server (re-exports flow to both); revert cfWorker.ts import-sort noise --- .changeset/error-compat-aliases.md | 5 +++-- packages/client/src/validators/cfWorker.ts | 2 +- 2 files changed, 4 insertions(+), 3 deletions(-) diff --git a/.changeset/error-compat-aliases.md b/.changeset/error-compat-aliases.md index a1bf31054..50b6e647f 100644 --- a/.changeset/error-compat-aliases.md +++ b/.changeset/error-compat-aliases.md @@ -1,5 +1,6 @@ --- -'@modelcontextprotocol/core': patch +'@modelcontextprotocol/client': patch +'@modelcontextprotocol/server': patch --- -Add v1-compat error aliases: `McpError`/`ErrorCode` (alias `ProtocolError`/`ProtocolErrorCode`, with `ConnectionClosed`/`RequestTimeout` from `SdkErrorCode`), `OAuthError.errorCode` getter (alias `.code`), `JSONRPCError`/`isJSONRPCError`, the 17 v1 OAuth error subclasses (`InvalidTokenError`, `ServerError`, …) as thin wrappers around `OAuthError` + `OAuthErrorCode`, and `StreamableHTTPError` (construct-only `@deprecated` shim; v2 throws `SdkError`). +Add v1-compat `@deprecated` error aliases: `McpError`/`ErrorCode` (alias `ProtocolError`/`ProtocolErrorCode`, with `ConnectionClosed`/`RequestTimeout` from `SdkErrorCode`), `OAuthError.errorCode` getter (alias `.code`), `JSONRPCError`/`isJSONRPCError`, the 17 v1 OAuth error subclasses (`InvalidTokenError`, `ServerError`, …) as thin wrappers around `OAuthError` + `OAuthErrorCode`, and `StreamableHTTPError` as an `SdkError` subclass that the StreamableHTTP client transport now throws (so `instanceof StreamableHTTPError` matches; `.status` carries the HTTP status code). diff --git a/packages/client/src/validators/cfWorker.ts b/packages/client/src/validators/cfWorker.ts index 7d1c843e5..b068e69a1 100644 --- a/packages/client/src/validators/cfWorker.ts +++ b/packages/client/src/validators/cfWorker.ts @@ -6,5 +6,5 @@ * import { CfWorkerJsonSchemaValidator } from '@modelcontextprotocol/client/validators/cf-worker'; * ``` */ -export type { CfWorkerSchemaDraft } from '@modelcontextprotocol/core'; export { CfWorkerJsonSchemaValidator } from '@modelcontextprotocol/core'; +export type { CfWorkerSchemaDraft } from '@modelcontextprotocol/core'; From 473d2d8dec7bc69028f9173b69ea73d76b16437f Mon Sep 17 00:00:00 2001 From: Felix Weinberger Date: Fri, 17 Apr 2026 10:31:15 +0000 Subject: [PATCH 4/7] fix(compat): set class .name on OAuth subclass factory to match v1 named declarations --- packages/core/src/errors/oauthErrorsCompat.ts | 4 +++- packages/core/test/errors/compat.test.ts | 7 +++++++ 2 files changed, 10 insertions(+), 1 deletion(-) diff --git a/packages/core/src/errors/oauthErrorsCompat.ts b/packages/core/src/errors/oauthErrorsCompat.ts index e81e9eb2d..7d8b72f47 100644 --- a/packages/core/src/errors/oauthErrorsCompat.ts +++ b/packages/core/src/errors/oauthErrorsCompat.ts @@ -16,13 +16,15 @@ type OAuthErrorSubclass = { }; function sub(code: OAuthErrorCode, name: string): OAuthErrorSubclass { - return class extends OAuthError { + const Sub = class extends OAuthError { static errorCode = code as string; constructor(message: string, errorUri?: string) { super(code, message, errorUri); this.name = name; } }; + Object.defineProperty(Sub, 'name', { value: name, configurable: true }); + return Sub; } /* eslint-disable @typescript-eslint/naming-convention */ diff --git a/packages/core/test/errors/compat.test.ts b/packages/core/test/errors/compat.test.ts index 7855c4fc7..d98be10dc 100644 --- a/packages/core/test/errors/compat.test.ts +++ b/packages/core/test/errors/compat.test.ts @@ -43,6 +43,13 @@ describe('v1-compat error aliases', () => { spy.mockRestore(); }); + it('OAuth subclass class .name matches v1 named-declaration behavior', () => { + expect(InvalidTokenError.name).toBe('InvalidTokenError'); + const e = new InvalidTokenError('expired'); + expect(e.name).toBe('InvalidTokenError'); + expect(e.constructor.name).toBe('InvalidTokenError'); + }); + it('subclass static errorCode and toResponseObject() match v1 wire format', () => { expect(ServerError.errorCode).toBe('server_error'); const e = new ServerError('boom'); From 28c5c7a142ad2c49167f300e8cb434f9a8becb6a Mon Sep 17 00:00:00 2001 From: Felix Weinberger Date: Fri, 17 Apr 2026 10:53:55 +0000 Subject: [PATCH 5/7] fix(compat): export OAuth subclass type aliases (value+type binding like v1); fix changeset count --- .changeset/error-compat-aliases.md | 2 +- packages/core/src/errors/oauthErrorsCompat.ts | 19 ++++++++++++++++++- packages/core/test/errors/compat.test.ts | 7 +++++++ 3 files changed, 26 insertions(+), 2 deletions(-) diff --git a/.changeset/error-compat-aliases.md b/.changeset/error-compat-aliases.md index 50b6e647f..d13286e6f 100644 --- a/.changeset/error-compat-aliases.md +++ b/.changeset/error-compat-aliases.md @@ -3,4 +3,4 @@ '@modelcontextprotocol/server': patch --- -Add v1-compat `@deprecated` error aliases: `McpError`/`ErrorCode` (alias `ProtocolError`/`ProtocolErrorCode`, with `ConnectionClosed`/`RequestTimeout` from `SdkErrorCode`), `OAuthError.errorCode` getter (alias `.code`), `JSONRPCError`/`isJSONRPCError`, the 17 v1 OAuth error subclasses (`InvalidTokenError`, `ServerError`, …) as thin wrappers around `OAuthError` + `OAuthErrorCode`, and `StreamableHTTPError` as an `SdkError` subclass that the StreamableHTTP client transport now throws (so `instanceof StreamableHTTPError` matches; `.status` carries the HTTP status code). +Add v1-compat `@deprecated` error aliases: `McpError`/`ErrorCode` (alias `ProtocolError`/`ProtocolErrorCode`, with `ConnectionClosed`/`RequestTimeout` from `SdkErrorCode`), `OAuthError.errorCode` getter (alias `.code`), `JSONRPCError`/`isJSONRPCError`, 16 of the 17 v1 OAuth error subclasses (`InvalidTokenError`, `ServerError`, … — `InvalidRequestError` is omitted from the public surface to avoid colliding with the JSON-RPC `InvalidRequest` type) as thin wrappers around `OAuthError` + `OAuthErrorCode`, and `StreamableHTTPError` as an `SdkError` subclass that the StreamableHTTP client transport now throws (so `instanceof StreamableHTTPError` matches; `.status` carries the HTTP status code). diff --git a/packages/core/src/errors/oauthErrorsCompat.ts b/packages/core/src/errors/oauthErrorsCompat.ts index 7d8b72f47..0686a50b7 100644 --- a/packages/core/src/errors/oauthErrorsCompat.ts +++ b/packages/core/src/errors/oauthErrorsCompat.ts @@ -27,42 +27,59 @@ function sub(code: OAuthErrorCode, name: string): OAuthErrorSubclass { return Sub; } -/* eslint-disable @typescript-eslint/naming-convention */ +/* eslint-disable @typescript-eslint/naming-convention, @typescript-eslint/no-redeclare */ /** @deprecated Use `OAuthError` with `OAuthErrorCode.InvalidRequest`. */ export const InvalidRequestError = sub(OAuthErrorCode.InvalidRequest, 'InvalidRequestError'); +export type InvalidRequestError = InstanceType; /** @deprecated Use `OAuthError` with `OAuthErrorCode.InvalidClient`. */ export const InvalidClientError = sub(OAuthErrorCode.InvalidClient, 'InvalidClientError'); +export type InvalidClientError = InstanceType; /** @deprecated Use `OAuthError` with `OAuthErrorCode.InvalidGrant`. */ export const InvalidGrantError = sub(OAuthErrorCode.InvalidGrant, 'InvalidGrantError'); +export type InvalidGrantError = InstanceType; /** @deprecated Use `OAuthError` with `OAuthErrorCode.UnauthorizedClient`. */ export const UnauthorizedClientError = sub(OAuthErrorCode.UnauthorizedClient, 'UnauthorizedClientError'); +export type UnauthorizedClientError = InstanceType; /** @deprecated Use `OAuthError` with `OAuthErrorCode.UnsupportedGrantType`. */ export const UnsupportedGrantTypeError = sub(OAuthErrorCode.UnsupportedGrantType, 'UnsupportedGrantTypeError'); +export type UnsupportedGrantTypeError = InstanceType; /** @deprecated Use `OAuthError` with `OAuthErrorCode.InvalidScope`. */ export const InvalidScopeError = sub(OAuthErrorCode.InvalidScope, 'InvalidScopeError'); +export type InvalidScopeError = InstanceType; /** @deprecated Use `OAuthError` with `OAuthErrorCode.AccessDenied`. */ export const AccessDeniedError = sub(OAuthErrorCode.AccessDenied, 'AccessDeniedError'); +export type AccessDeniedError = InstanceType; /** @deprecated Use `OAuthError` with `OAuthErrorCode.ServerError`. */ export const ServerError = sub(OAuthErrorCode.ServerError, 'ServerError'); +export type ServerError = InstanceType; /** @deprecated Use `OAuthError` with `OAuthErrorCode.TemporarilyUnavailable`. */ export const TemporarilyUnavailableError = sub(OAuthErrorCode.TemporarilyUnavailable, 'TemporarilyUnavailableError'); +export type TemporarilyUnavailableError = InstanceType; /** @deprecated Use `OAuthError` with `OAuthErrorCode.UnsupportedResponseType`. */ export const UnsupportedResponseTypeError = sub(OAuthErrorCode.UnsupportedResponseType, 'UnsupportedResponseTypeError'); +export type UnsupportedResponseTypeError = InstanceType; /** @deprecated Use `OAuthError` with `OAuthErrorCode.UnsupportedTokenType`. */ export const UnsupportedTokenTypeError = sub(OAuthErrorCode.UnsupportedTokenType, 'UnsupportedTokenTypeError'); +export type UnsupportedTokenTypeError = InstanceType; /** @deprecated Use `OAuthError` with `OAuthErrorCode.InvalidToken`. */ export const InvalidTokenError = sub(OAuthErrorCode.InvalidToken, 'InvalidTokenError'); +export type InvalidTokenError = InstanceType; /** @deprecated Use `OAuthError` with `OAuthErrorCode.MethodNotAllowed`. */ export const MethodNotAllowedError = sub(OAuthErrorCode.MethodNotAllowed, 'MethodNotAllowedError'); +export type MethodNotAllowedError = InstanceType; /** @deprecated Use `OAuthError` with `OAuthErrorCode.TooManyRequests`. */ export const TooManyRequestsError = sub(OAuthErrorCode.TooManyRequests, 'TooManyRequestsError'); +export type TooManyRequestsError = InstanceType; /** @deprecated Use `OAuthError` with `OAuthErrorCode.InvalidClientMetadata`. */ export const InvalidClientMetadataError = sub(OAuthErrorCode.InvalidClientMetadata, 'InvalidClientMetadataError'); +export type InvalidClientMetadataError = InstanceType; /** @deprecated Use `OAuthError` with `OAuthErrorCode.InsufficientScope`. */ export const InsufficientScopeError = sub(OAuthErrorCode.InsufficientScope, 'InsufficientScopeError'); +export type InsufficientScopeError = InstanceType; /** @deprecated Use `OAuthError` with `OAuthErrorCode.InvalidTarget`. */ export const InvalidTargetError = sub(OAuthErrorCode.InvalidTarget, 'InvalidTargetError'); +export type InvalidTargetError = InstanceType; /** * @deprecated Construct {@link OAuthError} directly with a custom code string. diff --git a/packages/core/test/errors/compat.test.ts b/packages/core/test/errors/compat.test.ts index d98be10dc..ee97ff148 100644 --- a/packages/core/test/errors/compat.test.ts +++ b/packages/core/test/errors/compat.test.ts @@ -50,6 +50,13 @@ describe('v1-compat error aliases', () => { expect(e.constructor.name).toBe('InvalidTokenError'); }); + it('OAuth subclasses are usable in type position (value + type binding like v1 classes)', () => { + const e: InvalidTokenError = new InvalidTokenError('expired'); + const handle = (err: ServerError): string => err.code; + expect(handle(new ServerError('boom'))).toBe('server_error'); + expect(e.code).toBe('invalid_token'); + }); + it('subclass static errorCode and toResponseObject() match v1 wire format', () => { expect(ServerError.errorCode).toBe('server_error'); const e = new ServerError('boom'); From 36d4fb134b53767d47b8ee23bdfd2dfb30a505c3 Mon Sep 17 00:00:00 2001 From: Felix Weinberger Date: Fri, 17 Apr 2026 12:42:17 +0000 Subject: [PATCH 6/7] test(compat): import OAuth subclasses from public surface; add StreamableHTTPError instanceof+status assertion; drop redundant cast --- packages/client/test/client/streamableHttp.test.ts | 2 ++ packages/core/src/errors/oauthErrorsCompat.ts | 2 +- packages/core/test/errors/compat.test.ts | 3 ++- 3 files changed, 5 insertions(+), 2 deletions(-) diff --git a/packages/client/test/client/streamableHttp.test.ts b/packages/client/test/client/streamableHttp.test.ts index 19290f7b0..214cac2c3 100644 --- a/packages/client/test/client/streamableHttp.test.ts +++ b/packages/client/test/client/streamableHttp.test.ts @@ -1872,7 +1872,9 @@ describe('StreamableHTTPClientTransport', () => { const error = await transport.send(message).catch(e => e); expect(error).toBeInstanceOf(SdkError); + expect(error).toBeInstanceOf(StreamableHTTPError); expect((error as SdkError).code).toBe(SdkErrorCode.ClientHttpAuthentication); + expect((error as StreamableHTTPError).status).toBe(401); expect(mockAuthProvider.saveTokens).toHaveBeenCalledWith({ access_token: 'new-access-token', token_type: 'Bearer', diff --git a/packages/core/src/errors/oauthErrorsCompat.ts b/packages/core/src/errors/oauthErrorsCompat.ts index 0686a50b7..86cdb1a4c 100644 --- a/packages/core/src/errors/oauthErrorsCompat.ts +++ b/packages/core/src/errors/oauthErrorsCompat.ts @@ -17,7 +17,7 @@ type OAuthErrorSubclass = { function sub(code: OAuthErrorCode, name: string): OAuthErrorSubclass { const Sub = class extends OAuthError { - static errorCode = code as string; + static errorCode = code; constructor(message: string, errorUri?: string) { super(code, message, errorUri); this.name = name; diff --git a/packages/core/test/errors/compat.test.ts b/packages/core/test/errors/compat.test.ts index ee97ff148..b5b2bc051 100644 --- a/packages/core/test/errors/compat.test.ts +++ b/packages/core/test/errors/compat.test.ts @@ -1,5 +1,6 @@ import { describe, expect, it, vi } from 'vitest'; import { + CustomOAuthError, ErrorCode, InvalidTokenError, McpError, @@ -9,9 +10,9 @@ import { ProtocolErrorCode, SdkError, SdkErrorCode, + ServerError, StreamableHTTPError } from '../../src/exports/public/index.js'; -import { CustomOAuthError, ServerError } from '../../src/errors/oauthErrorsCompat.js'; describe('v1-compat error aliases', () => { it('McpError / ErrorCode alias ProtocolError / ProtocolErrorCode (+ ConnectionClosed/RequestTimeout from SdkErrorCode)', () => { From ca2cab0dc278747a72641ac6aec8a40d71224f91 Mon Sep 17 00:00:00 2001 From: Felix Weinberger Date: Fri, 17 Apr 2026 13:05:11 +0000 Subject: [PATCH 7/7] docs(compat): add @deprecated to OAuth subclass type aliases; fix changeset collision-symbol reference --- .changeset/error-compat-aliases.md | 2 +- packages/core/src/errors/oauthErrorsCompat.ts | 17 +++++++++++++++++ 2 files changed, 18 insertions(+), 1 deletion(-) diff --git a/.changeset/error-compat-aliases.md b/.changeset/error-compat-aliases.md index d13286e6f..0c8f02a3b 100644 --- a/.changeset/error-compat-aliases.md +++ b/.changeset/error-compat-aliases.md @@ -3,4 +3,4 @@ '@modelcontextprotocol/server': patch --- -Add v1-compat `@deprecated` error aliases: `McpError`/`ErrorCode` (alias `ProtocolError`/`ProtocolErrorCode`, with `ConnectionClosed`/`RequestTimeout` from `SdkErrorCode`), `OAuthError.errorCode` getter (alias `.code`), `JSONRPCError`/`isJSONRPCError`, 16 of the 17 v1 OAuth error subclasses (`InvalidTokenError`, `ServerError`, … — `InvalidRequestError` is omitted from the public surface to avoid colliding with the JSON-RPC `InvalidRequest` type) as thin wrappers around `OAuthError` + `OAuthErrorCode`, and `StreamableHTTPError` as an `SdkError` subclass that the StreamableHTTP client transport now throws (so `instanceof StreamableHTTPError` matches; `.status` carries the HTTP status code). +Add v1-compat `@deprecated` error aliases: `McpError`/`ErrorCode` (alias `ProtocolError`/`ProtocolErrorCode`, with `ConnectionClosed`/`RequestTimeout` from `SdkErrorCode`), `OAuthError.errorCode` getter (alias `.code`), `JSONRPCError`/`isJSONRPCError`, 16 of the 17 v1 OAuth error subclasses (`InvalidTokenError`, `ServerError`, … — `InvalidRequestError` is omitted from the public surface to avoid colliding with the `InvalidRequestError` interface in `types.ts`) as thin wrappers around `OAuthError` + `OAuthErrorCode`, and `StreamableHTTPError` as an `SdkError` subclass that the StreamableHTTP client transport now throws (so `instanceof StreamableHTTPError` matches; `.status` carries the HTTP status code). diff --git a/packages/core/src/errors/oauthErrorsCompat.ts b/packages/core/src/errors/oauthErrorsCompat.ts index 86cdb1a4c..d51b67a33 100644 --- a/packages/core/src/errors/oauthErrorsCompat.ts +++ b/packages/core/src/errors/oauthErrorsCompat.ts @@ -31,54 +31,71 @@ function sub(code: OAuthErrorCode, name: string): OAuthErrorSubclass { /** @deprecated Use `OAuthError` with `OAuthErrorCode.InvalidRequest`. */ export const InvalidRequestError = sub(OAuthErrorCode.InvalidRequest, 'InvalidRequestError'); +/** @deprecated Use `OAuthError` with `OAuthErrorCode.InvalidRequest`. */ export type InvalidRequestError = InstanceType; /** @deprecated Use `OAuthError` with `OAuthErrorCode.InvalidClient`. */ export const InvalidClientError = sub(OAuthErrorCode.InvalidClient, 'InvalidClientError'); +/** @deprecated Use `OAuthError` with `OAuthErrorCode.InvalidClient`. */ export type InvalidClientError = InstanceType; /** @deprecated Use `OAuthError` with `OAuthErrorCode.InvalidGrant`. */ export const InvalidGrantError = sub(OAuthErrorCode.InvalidGrant, 'InvalidGrantError'); +/** @deprecated Use `OAuthError` with `OAuthErrorCode.InvalidGrant`. */ export type InvalidGrantError = InstanceType; /** @deprecated Use `OAuthError` with `OAuthErrorCode.UnauthorizedClient`. */ export const UnauthorizedClientError = sub(OAuthErrorCode.UnauthorizedClient, 'UnauthorizedClientError'); +/** @deprecated Use `OAuthError` with `OAuthErrorCode.UnauthorizedClient`. */ export type UnauthorizedClientError = InstanceType; /** @deprecated Use `OAuthError` with `OAuthErrorCode.UnsupportedGrantType`. */ export const UnsupportedGrantTypeError = sub(OAuthErrorCode.UnsupportedGrantType, 'UnsupportedGrantTypeError'); +/** @deprecated Use `OAuthError` with `OAuthErrorCode.UnsupportedGrantType`. */ export type UnsupportedGrantTypeError = InstanceType; /** @deprecated Use `OAuthError` with `OAuthErrorCode.InvalidScope`. */ export const InvalidScopeError = sub(OAuthErrorCode.InvalidScope, 'InvalidScopeError'); +/** @deprecated Use `OAuthError` with `OAuthErrorCode.InvalidScope`. */ export type InvalidScopeError = InstanceType; /** @deprecated Use `OAuthError` with `OAuthErrorCode.AccessDenied`. */ export const AccessDeniedError = sub(OAuthErrorCode.AccessDenied, 'AccessDeniedError'); +/** @deprecated Use `OAuthError` with `OAuthErrorCode.AccessDenied`. */ export type AccessDeniedError = InstanceType; /** @deprecated Use `OAuthError` with `OAuthErrorCode.ServerError`. */ export const ServerError = sub(OAuthErrorCode.ServerError, 'ServerError'); +/** @deprecated Use `OAuthError` with `OAuthErrorCode.ServerError`. */ export type ServerError = InstanceType; /** @deprecated Use `OAuthError` with `OAuthErrorCode.TemporarilyUnavailable`. */ export const TemporarilyUnavailableError = sub(OAuthErrorCode.TemporarilyUnavailable, 'TemporarilyUnavailableError'); +/** @deprecated Use `OAuthError` with `OAuthErrorCode.TemporarilyUnavailable`. */ export type TemporarilyUnavailableError = InstanceType; /** @deprecated Use `OAuthError` with `OAuthErrorCode.UnsupportedResponseType`. */ export const UnsupportedResponseTypeError = sub(OAuthErrorCode.UnsupportedResponseType, 'UnsupportedResponseTypeError'); +/** @deprecated Use `OAuthError` with `OAuthErrorCode.UnsupportedResponseType`. */ export type UnsupportedResponseTypeError = InstanceType; /** @deprecated Use `OAuthError` with `OAuthErrorCode.UnsupportedTokenType`. */ export const UnsupportedTokenTypeError = sub(OAuthErrorCode.UnsupportedTokenType, 'UnsupportedTokenTypeError'); +/** @deprecated Use `OAuthError` with `OAuthErrorCode.UnsupportedTokenType`. */ export type UnsupportedTokenTypeError = InstanceType; /** @deprecated Use `OAuthError` with `OAuthErrorCode.InvalidToken`. */ export const InvalidTokenError = sub(OAuthErrorCode.InvalidToken, 'InvalidTokenError'); +/** @deprecated Use `OAuthError` with `OAuthErrorCode.InvalidToken`. */ export type InvalidTokenError = InstanceType; /** @deprecated Use `OAuthError` with `OAuthErrorCode.MethodNotAllowed`. */ export const MethodNotAllowedError = sub(OAuthErrorCode.MethodNotAllowed, 'MethodNotAllowedError'); +/** @deprecated Use `OAuthError` with `OAuthErrorCode.MethodNotAllowed`. */ export type MethodNotAllowedError = InstanceType; /** @deprecated Use `OAuthError` with `OAuthErrorCode.TooManyRequests`. */ export const TooManyRequestsError = sub(OAuthErrorCode.TooManyRequests, 'TooManyRequestsError'); +/** @deprecated Use `OAuthError` with `OAuthErrorCode.TooManyRequests`. */ export type TooManyRequestsError = InstanceType; /** @deprecated Use `OAuthError` with `OAuthErrorCode.InvalidClientMetadata`. */ export const InvalidClientMetadataError = sub(OAuthErrorCode.InvalidClientMetadata, 'InvalidClientMetadataError'); +/** @deprecated Use `OAuthError` with `OAuthErrorCode.InvalidClientMetadata`. */ export type InvalidClientMetadataError = InstanceType; /** @deprecated Use `OAuthError` with `OAuthErrorCode.InsufficientScope`. */ export const InsufficientScopeError = sub(OAuthErrorCode.InsufficientScope, 'InsufficientScopeError'); +/** @deprecated Use `OAuthError` with `OAuthErrorCode.InsufficientScope`. */ export type InsufficientScopeError = InstanceType; /** @deprecated Use `OAuthError` with `OAuthErrorCode.InvalidTarget`. */ export const InvalidTargetError = sub(OAuthErrorCode.InvalidTarget, 'InvalidTargetError'); +/** @deprecated Use `OAuthError` with `OAuthErrorCode.InvalidTarget`. */ export type InvalidTargetError = InstanceType; /**