Skip to content

Commit ad6a110

Browse files
feat(compat): add McpError/ErrorCode aliases, OAuthError.errorCode getter, and legacy OAuth error subclasses
v1-compat shims (all @deprecated, removed in v3): - McpError -> ProtocolError, ErrorCode -> ProtocolErrorCode (re-export aliases) - OAuthError.errorCode getter -> .code (warns once) - 17 OAuth error subclasses (InvalidTokenError etc.) + CustomOAuthError as thin wrappers around OAuthError(OAuthErrorCode.X, ...), preserving v1 constructor signature, static errorCode, and instanceof OAuthError InvalidRequestError is defined in oauthErrorsCompat.ts but intentionally not re-exported from core/public to avoid colliding with the JSON-RPC InvalidRequestError interface from types.ts; the sdk meta-package server/auth/errors.js subpath will surface it.
1 parent a33c305 commit ad6a110

7 files changed

Lines changed: 233 additions & 1 deletion

File tree

.changeset/error-compat-aliases.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
---
2+
'@modelcontextprotocol/core': patch
3+
---
4+
5+
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`).

packages/core/src/auth/errors.ts

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -107,6 +107,13 @@ export class OAuthError extends Error {
107107
this.name = 'OAuthError';
108108
}
109109

110+
/**
111+
* v1 alias for {@linkcode OAuthError.code | .code}.
112+
*/
113+
get errorCode(): string {
114+
return this.code;
115+
}
116+
110117
/**
111118
* Converts the error to a standard OAuth error response object.
112119
*/
Lines changed: 77 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,77 @@
1+
/**
2+
* v1-compat: OAuth error subclasses.
3+
*
4+
* v1 shipped one `Error` subclass per OAuth error code (e.g. `InvalidTokenError`).
5+
* v2 also exposes the consolidated {@link OAuthError} + {@link OAuthErrorCode} enum.
6+
* These thin wrappers preserve `throw new InvalidTokenError(msg)` and `instanceof`
7+
* patterns from v1 and set `.code` to the matching enum value.
8+
*/
9+
10+
import { OAuthError, OAuthErrorCode } from '../auth/errors.js';
11+
12+
type OAuthErrorSubclass = {
13+
new (message: string, errorUri?: string): OAuthError;
14+
/** v1 static field. v2-preferred is the instance `.code` property. */
15+
errorCode: string;
16+
};
17+
18+
function sub(code: OAuthErrorCode, name: string): OAuthErrorSubclass {
19+
return class extends OAuthError {
20+
static errorCode = code as string;
21+
constructor(message: string, errorUri?: string) {
22+
super(code, message, errorUri);
23+
this.name = name;
24+
}
25+
};
26+
}
27+
28+
/* eslint-disable @typescript-eslint/naming-convention */
29+
30+
/** Equivalent to `new OAuthError(OAuthErrorCode.InvalidRequest, ...)`. */
31+
export const InvalidRequestError = sub(OAuthErrorCode.InvalidRequest, 'InvalidRequestError');
32+
/** Equivalent to `new OAuthError(OAuthErrorCode.InvalidClient, ...)`. */
33+
export const InvalidClientError = sub(OAuthErrorCode.InvalidClient, 'InvalidClientError');
34+
/** Equivalent to `new OAuthError(OAuthErrorCode.InvalidGrant, ...)`. */
35+
export const InvalidGrantError = sub(OAuthErrorCode.InvalidGrant, 'InvalidGrantError');
36+
/** Equivalent to `new OAuthError(OAuthErrorCode.UnauthorizedClient, ...)`. */
37+
export const UnauthorizedClientError = sub(OAuthErrorCode.UnauthorizedClient, 'UnauthorizedClientError');
38+
/** Equivalent to `new OAuthError(OAuthErrorCode.UnsupportedGrantType, ...)`. */
39+
export const UnsupportedGrantTypeError = sub(OAuthErrorCode.UnsupportedGrantType, 'UnsupportedGrantTypeError');
40+
/** Equivalent to `new OAuthError(OAuthErrorCode.InvalidScope, ...)`. */
41+
export const InvalidScopeError = sub(OAuthErrorCode.InvalidScope, 'InvalidScopeError');
42+
/** Equivalent to `new OAuthError(OAuthErrorCode.AccessDenied, ...)`. */
43+
export const AccessDeniedError = sub(OAuthErrorCode.AccessDenied, 'AccessDeniedError');
44+
/** Equivalent to `new OAuthError(OAuthErrorCode.ServerError, ...)`. */
45+
export const ServerError = sub(OAuthErrorCode.ServerError, 'ServerError');
46+
/** Equivalent to `new OAuthError(OAuthErrorCode.TemporarilyUnavailable, ...)`. */
47+
export const TemporarilyUnavailableError = sub(OAuthErrorCode.TemporarilyUnavailable, 'TemporarilyUnavailableError');
48+
/** Equivalent to `new OAuthError(OAuthErrorCode.UnsupportedResponseType, ...)`. */
49+
export const UnsupportedResponseTypeError = sub(OAuthErrorCode.UnsupportedResponseType, 'UnsupportedResponseTypeError');
50+
/** Equivalent to `new OAuthError(OAuthErrorCode.UnsupportedTokenType, ...)`. */
51+
export const UnsupportedTokenTypeError = sub(OAuthErrorCode.UnsupportedTokenType, 'UnsupportedTokenTypeError');
52+
/** Equivalent to `new OAuthError(OAuthErrorCode.InvalidToken, ...)`. */
53+
export const InvalidTokenError = sub(OAuthErrorCode.InvalidToken, 'InvalidTokenError');
54+
/** Equivalent to `new OAuthError(OAuthErrorCode.MethodNotAllowed, ...)`. */
55+
export const MethodNotAllowedError = sub(OAuthErrorCode.MethodNotAllowed, 'MethodNotAllowedError');
56+
/** Equivalent to `new OAuthError(OAuthErrorCode.TooManyRequests, ...)`. */
57+
export const TooManyRequestsError = sub(OAuthErrorCode.TooManyRequests, 'TooManyRequestsError');
58+
/** Equivalent to `new OAuthError(OAuthErrorCode.InvalidClientMetadata, ...)`. */
59+
export const InvalidClientMetadataError = sub(OAuthErrorCode.InvalidClientMetadata, 'InvalidClientMetadataError');
60+
/** Equivalent to `new OAuthError(OAuthErrorCode.InsufficientScope, ...)`. */
61+
export const InsufficientScopeError = sub(OAuthErrorCode.InsufficientScope, 'InsufficientScopeError');
62+
/** Equivalent to `new OAuthError(OAuthErrorCode.InvalidTarget, ...)`. */
63+
export const InvalidTargetError = sub(OAuthErrorCode.InvalidTarget, 'InvalidTargetError');
64+
65+
/**
66+
* v1 base class for custom OAuth error codes.
67+
*
68+
* v1 pattern was `class MyErr extends CustomOAuthError { static errorCode = 'my_code' }`;
69+
* this preserves that by reading `static errorCode` from the concrete subclass.
70+
* v2-preferred is to construct {@link OAuthError} directly with a custom code string.
71+
*/
72+
export class CustomOAuthError extends OAuthError {
73+
static errorCode: string;
74+
constructor(message: string, errorUri?: string) {
75+
super((new.target as typeof CustomOAuthError).errorCode, message, errorUri);
76+
}
77+
}
Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,23 @@
1+
import { deprecate } from '../util/deprecate.js';
2+
3+
/**
4+
* @deprecated v1-compat shim. v2 client transports throw {@linkcode SdkError} with a
5+
* `SdkErrorCode.ClientHttp*` code and the HTTP status under `data.status`; they do not
6+
* throw this class. This shim is **construct-only**: it preserves the v1 shape (`.code`
7+
* is the HTTP status number) so existing `throw new StreamableHTTPError(...)` and `.code`
8+
* usages compile, but `catch`-side code testing `instanceof StreamableHTTPError` will not
9+
* match SDK-thrown errors. Update catch-side code to `instanceof SdkError`.
10+
*/
11+
export class StreamableHTTPError extends Error {
12+
constructor(
13+
public readonly code: number,
14+
message?: string
15+
) {
16+
super(message ?? `HTTP ${code}`);
17+
this.name = 'StreamableHTTPError';
18+
deprecate(
19+
'StreamableHTTPError',
20+
'StreamableHTTPError is not thrown by v2; catch SdkError with a SdkErrorCode.ClientHttp* code instead.'
21+
);
22+
}
23+
}

packages/core/src/exports/public/index.ts

Lines changed: 51 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -100,13 +100,64 @@ export { ProtocolErrorCode } from '../../types/enums.js';
100100
// Error classes
101101
export { ProtocolError, UrlElicitationRequiredError } from '../../types/errors.js';
102102

103+
// --- v1-compat aliases ---
104+
import { SdkErrorCode as _SdkErrorCode } from '../../errors/sdkErrors.js';
105+
import { ProtocolErrorCode as _ProtocolErrorCode } from '../../types/enums.js';
106+
/**
107+
* v1 name. v2 also exposes the underlying split: {@linkcode ProtocolErrorCode} for
108+
* protocol-level (wire) errors and {@linkcode SdkErrorCode} for local SDK errors.
109+
* Note `ConnectionClosed`/`RequestTimeout` moved to `SdkErrorCode` in v2 and are now
110+
* thrown as `SdkError`, not `ProtocolError`.
111+
*/
112+
export const ErrorCode = {
113+
..._ProtocolErrorCode,
114+
/** Now {@linkcode SdkErrorCode.ConnectionClosed}; thrown as `SdkError`, not `McpError`. */
115+
ConnectionClosed: _SdkErrorCode.ConnectionClosed,
116+
/** Now {@linkcode SdkErrorCode.RequestTimeout}; thrown as `SdkError`, not `McpError`. */
117+
RequestTimeout: _SdkErrorCode.RequestTimeout
118+
} as const;
119+
/** See {@linkcode ErrorCode} const. */
120+
export type ErrorCode = _ProtocolErrorCode | typeof _SdkErrorCode.ConnectionClosed | typeof _SdkErrorCode.RequestTimeout;
121+
export {
122+
/** v1 name. v2 also exposes this as {@linkcode ProtocolError}. */
123+
ProtocolError as McpError
124+
} from '../../types/errors.js';
125+
// Note: InvalidRequestError is intentionally omitted here — it collides with the
126+
// JSON-RPC `InvalidRequestError` interface re-exported from types.ts below. v1 users
127+
// imported it from `server/auth/errors.js`, which the sdk meta-package subpath provides.
128+
export {
129+
AccessDeniedError,
130+
CustomOAuthError,
131+
InsufficientScopeError,
132+
InvalidClientError,
133+
InvalidClientMetadataError,
134+
InvalidGrantError,
135+
InvalidScopeError,
136+
InvalidTargetError,
137+
InvalidTokenError,
138+
MethodNotAllowedError,
139+
ServerError,
140+
TemporarilyUnavailableError,
141+
TooManyRequestsError,
142+
UnauthorizedClientError,
143+
UnsupportedGrantTypeError,
144+
UnsupportedResponseTypeError,
145+
UnsupportedTokenTypeError
146+
} from '../../errors/oauthErrorsCompat.js';
147+
export { StreamableHTTPError } from '../../errors/streamableHttpErrorCompat.js';
148+
/** v1 name. v2 also exposes this as {@linkcode JSONRPCErrorResponse}. */
149+
export type { JSONRPCErrorResponse as JSONRPCError } from '../../types/spec.types.js';
150+
// --- end v1-compat ---
151+
103152
// Type guards and message parsing
104153
export {
105154
assertCompleteRequestPrompt,
106155
assertCompleteRequestResourceTemplate,
107156
isCallToolResult,
108157
isInitializedNotification,
109158
isInitializeRequest,
159+
/** v1 name. v2 also exposes this as {@linkcode isJSONRPCErrorResponse}. */
160+
isJSONRPCErrorResponse as isJSONRPCError,
110161
isJSONRPCErrorResponse,
111162
isJSONRPCNotification,
112163
isJSONRPCRequest,

packages/core/src/index.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -48,5 +48,5 @@ export * from './validators/fromJsonSchema.js';
4848
*/
4949

5050
// Core types only - implementations are exported via separate entry points
51-
export type { JsonSchemaType, JsonSchemaValidator, jsonSchemaValidator, JsonSchemaValidatorResult } from './validators/types.js';
5251
export { deprecate } from './util/deprecate.js';
52+
export type { JsonSchemaType, JsonSchemaValidator, jsonSchemaValidator, JsonSchemaValidatorResult } from './validators/types.js';
Lines changed: 69 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,69 @@
1+
import { describe, expect, it, vi } from 'vitest';
2+
import {
3+
ErrorCode,
4+
InvalidTokenError,
5+
McpError,
6+
OAuthError,
7+
OAuthErrorCode,
8+
ProtocolError,
9+
ProtocolErrorCode,
10+
SdkErrorCode,
11+
StreamableHTTPError
12+
} from '../../src/exports/public/index.js';
13+
import { CustomOAuthError, ServerError } from '../../src/errors/oauthErrorsCompat.js';
14+
15+
describe('v1-compat error aliases', () => {
16+
it('McpError / ErrorCode alias ProtocolError / ProtocolErrorCode (+ ConnectionClosed/RequestTimeout from SdkErrorCode)', () => {
17+
expect(McpError).toBe(ProtocolError);
18+
expect(ErrorCode.InvalidParams).toBe(ProtocolErrorCode.InvalidParams);
19+
expect(ErrorCode.ConnectionClosed).toBe(SdkErrorCode.ConnectionClosed);
20+
expect(ErrorCode.RequestTimeout).toBe(SdkErrorCode.RequestTimeout);
21+
const e = new McpError(ErrorCode.InvalidParams, 'x');
22+
expect(e).toBeInstanceOf(ProtocolError);
23+
expect(e.code).toBe(ProtocolErrorCode.InvalidParams);
24+
});
25+
26+
it('OAuthError.errorCode getter returns .code (no warning)', () => {
27+
const spy = vi.spyOn(console, 'warn').mockImplementation(() => {});
28+
const e = new OAuthError(OAuthErrorCode.InvalidToken, 'bad');
29+
expect(e.errorCode).toBe(OAuthErrorCode.InvalidToken);
30+
expect(e.errorCode).toBe('invalid_token');
31+
expect(spy).not.toHaveBeenCalled();
32+
spy.mockRestore();
33+
});
34+
35+
it('InvalidTokenError is an OAuthError with .code = InvalidToken (no warning)', () => {
36+
const spy = vi.spyOn(console, 'warn').mockImplementation(() => {});
37+
const e = new InvalidTokenError('expired');
38+
expect(e).toBeInstanceOf(OAuthError);
39+
expect(e.code).toBe(OAuthErrorCode.InvalidToken);
40+
expect(e.message).toBe('expired');
41+
expect(spy).not.toHaveBeenCalled();
42+
spy.mockRestore();
43+
});
44+
45+
it('subclass static errorCode and toResponseObject() match v1 wire format', () => {
46+
expect(ServerError.errorCode).toBe('server_error');
47+
const e = new ServerError('boom');
48+
expect(e.toResponseObject()).toEqual({ error: 'server_error', error_description: 'boom' });
49+
});
50+
51+
it('CustomOAuthError reads static errorCode from concrete subclass', () => {
52+
class MyError extends CustomOAuthError {
53+
static override errorCode = 'my_custom_code';
54+
}
55+
const e = new MyError('nope');
56+
expect(e).toBeInstanceOf(OAuthError);
57+
expect(e.code).toBe('my_custom_code');
58+
});
59+
60+
it('StreamableHTTPError preserves v1 shape and warns once (construct-only shim)', () => {
61+
const spy = vi.spyOn(console, 'warn').mockImplementation(() => {});
62+
const e = new StreamableHTTPError(503, 'Service Unavailable');
63+
expect(e.code).toBe(503);
64+
expect(e.name).toBe('StreamableHTTPError');
65+
new StreamableHTTPError(404);
66+
expect(spy).toHaveBeenCalledTimes(1);
67+
spy.mockRestore();
68+
});
69+
});

0 commit comments

Comments
 (0)