Skip to content

Commit fa0b0b6

Browse files
Merge remote-tracking branch 'origin/fweinberger/v2-bc-error-aliases' into fweinberger/v2-bc-d1-base
2 parents 8739509 + 6221eda commit fa0b0b6

9 files changed

Lines changed: 319 additions & 21 deletions

File tree

.changeset/error-compat-aliases.md

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

packages/client/src/client/streamableHttp.ts

Lines changed: 35 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -9,8 +9,8 @@ import {
99
isJSONRPCResultResponse,
1010
JSONRPCMessageSchema,
1111
normalizeHeaders,
12-
SdkError,
13-
SdkErrorCode
12+
SdkErrorCode,
13+
StreamableHTTPError
1414
} from '@modelcontextprotocol/core';
1515
import { EventSourceParserStream } from 'eventsource-parser/stream';
1616

@@ -273,9 +273,13 @@ export class StreamableHTTPClientTransport implements Transport {
273273
}
274274
await response.text?.().catch(() => {});
275275
if (isAuthRetry) {
276-
throw new SdkError(SdkErrorCode.ClientHttpAuthentication, 'Server returned 401 after re-authentication', {
277-
status: 401
278-
});
276+
throw new StreamableHTTPError(
277+
SdkErrorCode.ClientHttpAuthentication,
278+
'Server returned 401 after re-authentication',
279+
{
280+
status: 401
281+
}
282+
);
279283
}
280284
throw new UnauthorizedError();
281285
}
@@ -288,10 +292,14 @@ export class StreamableHTTPClientTransport implements Transport {
288292
return;
289293
}
290294

291-
throw new SdkError(SdkErrorCode.ClientHttpFailedToOpenStream, `Failed to open SSE stream: ${response.statusText}`, {
292-
status: response.status,
293-
statusText: response.statusText
294-
});
295+
throw new StreamableHTTPError(
296+
SdkErrorCode.ClientHttpFailedToOpenStream,
297+
`Failed to open SSE stream: ${response.statusText}`,
298+
{
299+
status: response.status,
300+
statusText: response.statusText
301+
}
302+
);
295303
}
296304

297305
this._handleSseStream(response.body, options, true);
@@ -581,9 +589,13 @@ export class StreamableHTTPClientTransport implements Transport {
581589
}
582590
await response.text?.().catch(() => {});
583591
if (isAuthRetry) {
584-
throw new SdkError(SdkErrorCode.ClientHttpAuthentication, 'Server returned 401 after re-authentication', {
585-
status: 401
586-
});
592+
throw new StreamableHTTPError(
593+
SdkErrorCode.ClientHttpAuthentication,
594+
'Server returned 401 after re-authentication',
595+
{
596+
status: 401
597+
}
598+
);
587599
}
588600
throw new UnauthorizedError();
589601
}
@@ -598,7 +610,7 @@ export class StreamableHTTPClientTransport implements Transport {
598610

599611
// Check if we've already tried upscoping with this header to prevent infinite loops.
600612
if (this._lastUpscopingHeader === wwwAuthHeader) {
601-
throw new SdkError(SdkErrorCode.ClientHttpForbidden, 'Server returned 403 after trying upscoping', {
613+
throw new StreamableHTTPError(SdkErrorCode.ClientHttpForbidden, 'Server returned 403 after trying upscoping', {
602614
status: 403,
603615
text
604616
});
@@ -629,7 +641,7 @@ export class StreamableHTTPClientTransport implements Transport {
629641
}
630642
}
631643

632-
throw new SdkError(SdkErrorCode.ClientHttpNotImplemented, `Error POSTing to endpoint: ${text}`, {
644+
throw new StreamableHTTPError(SdkErrorCode.ClientHttpNotImplemented, `Error POSTing to endpoint: ${text}`, {
633645
status: response.status,
634646
text
635647
});
@@ -675,7 +687,7 @@ export class StreamableHTTPClientTransport implements Transport {
675687
}
676688
} else {
677689
await response.text?.().catch(() => {});
678-
throw new SdkError(SdkErrorCode.ClientHttpUnexpectedContent, `Unexpected content type: ${contentType}`, {
690+
throw new StreamableHTTPError(SdkErrorCode.ClientHttpUnexpectedContent, `Unexpected content type: ${contentType}`, {
679691
contentType
680692
});
681693
}
@@ -725,10 +737,14 @@ export class StreamableHTTPClientTransport implements Transport {
725737
// We specifically handle 405 as a valid response according to the spec,
726738
// meaning the server does not support explicit session termination
727739
if (!response.ok && response.status !== 405) {
728-
throw new SdkError(SdkErrorCode.ClientHttpFailedToTerminateSession, `Failed to terminate session: ${response.statusText}`, {
729-
status: response.status,
730-
statusText: response.statusText
731-
});
740+
throw new StreamableHTTPError(
741+
SdkErrorCode.ClientHttpFailedToTerminateSession,
742+
`Failed to terminate session: ${response.statusText}`,
743+
{
744+
status: response.status,
745+
statusText: response.statusText
746+
}
747+
);
732748
}
733749

734750
this._sessionId = undefined;

packages/client/test/client/streamableHttp.test.ts

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
import type { JSONRPCMessage, JSONRPCRequest } from '@modelcontextprotocol/core';
2-
import { OAuthError, OAuthErrorCode, SdkError, SdkErrorCode } from '@modelcontextprotocol/core';
2+
import { OAuthError, OAuthErrorCode, SdkError, SdkErrorCode, StreamableHTTPError } from '@modelcontextprotocol/core';
33
import type { Mock, Mocked } from 'vitest';
44

55
import type { OAuthClientProvider } from '../../src/client/auth.js';
@@ -240,7 +240,7 @@ describe('StreamableHTTPClientTransport', () => {
240240
transport.onerror = errorSpy;
241241

242242
await expect(transport.send(message)).rejects.toThrow(
243-
new SdkError(SdkErrorCode.ClientHttpNotImplemented, 'Error POSTing to endpoint: Session not found', {
243+
new StreamableHTTPError(SdkErrorCode.ClientHttpNotImplemented, 'Error POSTing to endpoint: Session not found', {
244244
status: 404,
245245
text: 'Session not found'
246246
})
@@ -1872,7 +1872,9 @@ describe('StreamableHTTPClientTransport', () => {
18721872

18731873
const error = await transport.send(message).catch(e => e);
18741874
expect(error).toBeInstanceOf(SdkError);
1875+
expect(error).toBeInstanceOf(StreamableHTTPError);
18751876
expect((error as SdkError).code).toBe(SdkErrorCode.ClientHttpAuthentication);
1877+
expect((error as StreamableHTTPError).status).toBe(401);
18761878
expect(mockAuthProvider.saveTokens).toHaveBeenCalledWith({
18771879
access_token: 'new-access-token',
18781880
token_type: 'Bearer',

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+
* @deprecated Use {@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: 112 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,112 @@
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+
/** @deprecated Use the instance `.code` property. */
15+
errorCode: string;
16+
};
17+
18+
function sub(code: OAuthErrorCode, name: string): OAuthErrorSubclass {
19+
const Sub = class extends OAuthError {
20+
static errorCode = code;
21+
constructor(message: string, errorUri?: string) {
22+
super(code, message, errorUri);
23+
this.name = name;
24+
}
25+
};
26+
Object.defineProperty(Sub, 'name', { value: name, configurable: true });
27+
return Sub;
28+
}
29+
30+
/* eslint-disable @typescript-eslint/naming-convention, @typescript-eslint/no-redeclare */
31+
32+
/** @deprecated Use `OAuthError` with `OAuthErrorCode.InvalidRequest`. */
33+
export const InvalidRequestError = sub(OAuthErrorCode.InvalidRequest, 'InvalidRequestError');
34+
/** @deprecated Use `OAuthError` with `OAuthErrorCode.InvalidRequest`. */
35+
export type InvalidRequestError = InstanceType<typeof InvalidRequestError>;
36+
/** @deprecated Use `OAuthError` with `OAuthErrorCode.InvalidClient`. */
37+
export const InvalidClientError = sub(OAuthErrorCode.InvalidClient, 'InvalidClientError');
38+
/** @deprecated Use `OAuthError` with `OAuthErrorCode.InvalidClient`. */
39+
export type InvalidClientError = InstanceType<typeof InvalidClientError>;
40+
/** @deprecated Use `OAuthError` with `OAuthErrorCode.InvalidGrant`. */
41+
export const InvalidGrantError = sub(OAuthErrorCode.InvalidGrant, 'InvalidGrantError');
42+
/** @deprecated Use `OAuthError` with `OAuthErrorCode.InvalidGrant`. */
43+
export type InvalidGrantError = InstanceType<typeof InvalidGrantError>;
44+
/** @deprecated Use `OAuthError` with `OAuthErrorCode.UnauthorizedClient`. */
45+
export const UnauthorizedClientError = sub(OAuthErrorCode.UnauthorizedClient, 'UnauthorizedClientError');
46+
/** @deprecated Use `OAuthError` with `OAuthErrorCode.UnauthorizedClient`. */
47+
export type UnauthorizedClientError = InstanceType<typeof UnauthorizedClientError>;
48+
/** @deprecated Use `OAuthError` with `OAuthErrorCode.UnsupportedGrantType`. */
49+
export const UnsupportedGrantTypeError = sub(OAuthErrorCode.UnsupportedGrantType, 'UnsupportedGrantTypeError');
50+
/** @deprecated Use `OAuthError` with `OAuthErrorCode.UnsupportedGrantType`. */
51+
export type UnsupportedGrantTypeError = InstanceType<typeof UnsupportedGrantTypeError>;
52+
/** @deprecated Use `OAuthError` with `OAuthErrorCode.InvalidScope`. */
53+
export const InvalidScopeError = sub(OAuthErrorCode.InvalidScope, 'InvalidScopeError');
54+
/** @deprecated Use `OAuthError` with `OAuthErrorCode.InvalidScope`. */
55+
export type InvalidScopeError = InstanceType<typeof InvalidScopeError>;
56+
/** @deprecated Use `OAuthError` with `OAuthErrorCode.AccessDenied`. */
57+
export const AccessDeniedError = sub(OAuthErrorCode.AccessDenied, 'AccessDeniedError');
58+
/** @deprecated Use `OAuthError` with `OAuthErrorCode.AccessDenied`. */
59+
export type AccessDeniedError = InstanceType<typeof AccessDeniedError>;
60+
/** @deprecated Use `OAuthError` with `OAuthErrorCode.ServerError`. */
61+
export const ServerError = sub(OAuthErrorCode.ServerError, 'ServerError');
62+
/** @deprecated Use `OAuthError` with `OAuthErrorCode.ServerError`. */
63+
export type ServerError = InstanceType<typeof ServerError>;
64+
/** @deprecated Use `OAuthError` with `OAuthErrorCode.TemporarilyUnavailable`. */
65+
export const TemporarilyUnavailableError = sub(OAuthErrorCode.TemporarilyUnavailable, 'TemporarilyUnavailableError');
66+
/** @deprecated Use `OAuthError` with `OAuthErrorCode.TemporarilyUnavailable`. */
67+
export type TemporarilyUnavailableError = InstanceType<typeof TemporarilyUnavailableError>;
68+
/** @deprecated Use `OAuthError` with `OAuthErrorCode.UnsupportedResponseType`. */
69+
export const UnsupportedResponseTypeError = sub(OAuthErrorCode.UnsupportedResponseType, 'UnsupportedResponseTypeError');
70+
/** @deprecated Use `OAuthError` with `OAuthErrorCode.UnsupportedResponseType`. */
71+
export type UnsupportedResponseTypeError = InstanceType<typeof UnsupportedResponseTypeError>;
72+
/** @deprecated Use `OAuthError` with `OAuthErrorCode.UnsupportedTokenType`. */
73+
export const UnsupportedTokenTypeError = sub(OAuthErrorCode.UnsupportedTokenType, 'UnsupportedTokenTypeError');
74+
/** @deprecated Use `OAuthError` with `OAuthErrorCode.UnsupportedTokenType`. */
75+
export type UnsupportedTokenTypeError = InstanceType<typeof UnsupportedTokenTypeError>;
76+
/** @deprecated Use `OAuthError` with `OAuthErrorCode.InvalidToken`. */
77+
export const InvalidTokenError = sub(OAuthErrorCode.InvalidToken, 'InvalidTokenError');
78+
/** @deprecated Use `OAuthError` with `OAuthErrorCode.InvalidToken`. */
79+
export type InvalidTokenError = InstanceType<typeof InvalidTokenError>;
80+
/** @deprecated Use `OAuthError` with `OAuthErrorCode.MethodNotAllowed`. */
81+
export const MethodNotAllowedError = sub(OAuthErrorCode.MethodNotAllowed, 'MethodNotAllowedError');
82+
/** @deprecated Use `OAuthError` with `OAuthErrorCode.MethodNotAllowed`. */
83+
export type MethodNotAllowedError = InstanceType<typeof MethodNotAllowedError>;
84+
/** @deprecated Use `OAuthError` with `OAuthErrorCode.TooManyRequests`. */
85+
export const TooManyRequestsError = sub(OAuthErrorCode.TooManyRequests, 'TooManyRequestsError');
86+
/** @deprecated Use `OAuthError` with `OAuthErrorCode.TooManyRequests`. */
87+
export type TooManyRequestsError = InstanceType<typeof TooManyRequestsError>;
88+
/** @deprecated Use `OAuthError` with `OAuthErrorCode.InvalidClientMetadata`. */
89+
export const InvalidClientMetadataError = sub(OAuthErrorCode.InvalidClientMetadata, 'InvalidClientMetadataError');
90+
/** @deprecated Use `OAuthError` with `OAuthErrorCode.InvalidClientMetadata`. */
91+
export type InvalidClientMetadataError = InstanceType<typeof InvalidClientMetadataError>;
92+
/** @deprecated Use `OAuthError` with `OAuthErrorCode.InsufficientScope`. */
93+
export const InsufficientScopeError = sub(OAuthErrorCode.InsufficientScope, 'InsufficientScopeError');
94+
/** @deprecated Use `OAuthError` with `OAuthErrorCode.InsufficientScope`. */
95+
export type InsufficientScopeError = InstanceType<typeof InsufficientScopeError>;
96+
/** @deprecated Use `OAuthError` with `OAuthErrorCode.InvalidTarget`. */
97+
export const InvalidTargetError = sub(OAuthErrorCode.InvalidTarget, 'InvalidTargetError');
98+
/** @deprecated Use `OAuthError` with `OAuthErrorCode.InvalidTarget`. */
99+
export type InvalidTargetError = InstanceType<typeof InvalidTargetError>;
100+
101+
/**
102+
* @deprecated Construct {@link OAuthError} directly with a custom code string.
103+
*
104+
* v1 pattern was `class MyErr extends CustomOAuthError { static errorCode = 'my_code' }`;
105+
* this preserves that by reading `static errorCode` from the concrete subclass.
106+
*/
107+
export class CustomOAuthError extends OAuthError {
108+
static errorCode: string;
109+
constructor(message: string, errorUri?: string) {
110+
super((new.target as typeof CustomOAuthError).errorCode, message, errorUri);
111+
}
112+
}
Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,20 @@
1+
import type { SdkErrorCode } from './sdkErrors.js';
2+
import { SdkError } from './sdkErrors.js';
3+
4+
/**
5+
* @deprecated Use {@linkcode SdkError}.
6+
*
7+
* Subclass thrown by the StreamableHTTP client transport for HTTP-level errors.
8+
* `instanceof StreamableHTTPError` and `instanceof SdkError` both match. Note that
9+
* `.code` is now the {@linkcode SdkErrorCode} (a `ClientHttp*` string), not the HTTP
10+
* status number as in v1; the status is available as `.status`.
11+
*/
12+
export class StreamableHTTPError extends SdkError {
13+
public readonly status: number | undefined;
14+
15+
constructor(code: SdkErrorCode, message: string, data?: { status?: number } & Record<string, unknown>) {
16+
super(code, message, data);
17+
this.name = 'StreamableHTTPError';
18+
this.status = data?.status;
19+
}
20+
}

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

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

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

packages/core/src/index.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
export * from './auth/errors.js';
22
export * from './errors/sdkErrors.js';
3+
export * from './errors/streamableHttpErrorCompat.js';
34
export * from './shared/auth.js';
45
export * from './shared/authUtils.js';
56
export * from './shared/metadataUtils.js';

0 commit comments

Comments
 (0)