Skip to content

Commit 0f0a4eb

Browse files
v2: Errors refactor (ProtocolError, SdkError, OAuthError) (#1454)
1 parent 160902e commit 0f0a4eb

33 files changed

Lines changed: 1086 additions & 705 deletions

CLAUDE.md

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -29,8 +29,7 @@ When making breaking changes, document them in **both**:
2929
- `docs/migration.md` — human-readable guide with before/after code examples
3030
- `docs/migration-SKILL.md` — LLM-optimized mapping tables for mechanical migration
3131

32-
Include what changed, why, and how to migrate. Search for related sections and group
33-
related changes together rather than adding new standalone sections.
32+
Include what changed, why, and how to migrate. Search for related sections and group related changes together rather than adding new standalone sections.
3433

3534
## Code Style Guidelines
3635

docs/migration-SKILL.md

Lines changed: 199 additions & 80 deletions
Large diffs are not rendered by default.

docs/migration.md

Lines changed: 276 additions & 67 deletions
Large diffs are not rendered by default.

examples/client/src/elicitationUrlExample.ts

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -21,10 +21,10 @@ import type {
2121
import {
2222
CallToolResultSchema,
2323
Client,
24-
ErrorCode,
2524
getDisplayName,
2625
ListToolsResultSchema,
27-
McpError,
26+
ProtocolError,
27+
ProtocolErrorCode,
2828
StreamableHTTPClientTransport,
2929
UnauthorizedError,
3030
UrlElicitationRequiredError
@@ -337,7 +337,7 @@ async function handleElicitationRequest(request: ElicitRequest): Promise<ElicitR
337337
} else {
338338
// Should not happen because the client declares its capabilities to the server,
339339
// but being defensive is a good practice:
340-
throw new McpError(ErrorCode.InvalidParams, `Unsupported elicitation mode: ${mode}`);
340+
throw new ProtocolError(ProtocolErrorCode.InvalidParams, `Unsupported elicitation mode: ${mode}`);
341341
}
342342
}
343343

examples/client/src/simpleStreamableHttp.ts

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -12,13 +12,13 @@ import type {
1212
import {
1313
CallToolResultSchema,
1414
Client,
15-
ErrorCode,
1615
getDisplayName,
1716
GetPromptResultSchema,
1817
ListPromptsResultSchema,
1918
ListResourcesResultSchema,
2019
ListToolsResultSchema,
21-
McpError,
20+
ProtocolError,
21+
ProtocolErrorCode,
2222
ReadResourceResultSchema,
2323
RELATED_TASK_META_KEY,
2424
StreamableHTTPClientTransport
@@ -270,7 +270,7 @@ async function connect(url?: string): Promise<void> {
270270
// Set up elicitation request handler with proper validation
271271
client.setRequestHandler('elicitation/create', async request => {
272272
if (request.params.mode !== 'form') {
273-
throw new McpError(ErrorCode.InvalidParams, `Unsupported elicitation mode: ${request.params.mode}`);
273+
throw new ProtocolError(ProtocolErrorCode.InvalidParams, `Unsupported elicitation mode: ${request.params.mode}`);
274274
}
275275
console.log('\n🔔 Elicitation (form) Request Received:');
276276
console.log(`Message: ${request.params.message}`);

examples/client/src/simpleTaskInteractiveClient.ts

Lines changed: 8 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,13 @@
1010
import { createInterface } from 'node:readline';
1111

1212
import type { CreateMessageRequest, CreateMessageResult, TextContent } from '@modelcontextprotocol/client';
13-
import { CallToolResultSchema, Client, ErrorCode, McpError, StreamableHTTPClientTransport } from '@modelcontextprotocol/client';
13+
import {
14+
CallToolResultSchema,
15+
Client,
16+
ProtocolError,
17+
ProtocolErrorCode,
18+
StreamableHTTPClientTransport
19+
} from '@modelcontextprotocol/client';
1420

1521
// Create readline interface for user input
1622
const readline = createInterface({
@@ -96,7 +102,7 @@ async function run(url: string): Promise<void> {
96102
// Set up elicitation request handler
97103
client.setRequestHandler('elicitation/create', async request => {
98104
if (request.params.mode && request.params.mode !== 'form') {
99-
throw new McpError(ErrorCode.InvalidParams, `Unsupported elicitation mode: ${request.params.mode}`);
105+
throw new ProtocolError(ProtocolErrorCode.InvalidParams, `Unsupported elicitation mode: ${request.params.mode}`);
100106
}
101107
return elicitationCallback(request.params);
102108
});

packages/client/src/client/auth.ts

Lines changed: 16 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -11,21 +11,16 @@ import type {
1111
} from '@modelcontextprotocol/core';
1212
import {
1313
checkResourceAllowed,
14-
InvalidClientError,
15-
InvalidClientMetadataError,
16-
InvalidGrantError,
1714
LATEST_PROTOCOL_VERSION,
18-
OAUTH_ERRORS,
1915
OAuthClientInformationFullSchema,
2016
OAuthError,
17+
OAuthErrorCode,
2118
OAuthErrorResponseSchema,
2219
OAuthMetadataSchema,
2320
OAuthProtectedResourceMetadataSchema,
2421
OAuthTokensSchema,
2522
OpenIdProviderDiscoveryMetadataSchema,
26-
resourceUrlFromServerUrl,
27-
ServerError,
28-
UnauthorizedClientError
23+
resourceUrlFromServerUrl
2924
} from '@modelcontextprotocol/core';
3025
import pkceChallenge from 'pkce-challenge';
3126

@@ -328,7 +323,7 @@ function applyPublicAuth(clientId: string, params: URLSearchParams): void {
328323
* Parses an OAuth error response from a string or Response object.
329324
*
330325
* If the input is a standard OAuth2.0 error response, it will be parsed according to the spec
331-
* and an instance of the appropriate OAuthError subclass will be returned.
326+
* and an OAuthError will be returned with the appropriate error code.
332327
* If parsing fails, it falls back to a generic ServerError that includes
333328
* the response status (if available) and original content.
334329
*
@@ -341,13 +336,11 @@ export async function parseErrorResponse(input: Response | string): Promise<OAut
341336

342337
try {
343338
const result = OAuthErrorResponseSchema.parse(JSON.parse(body));
344-
const { error, error_description, error_uri } = result;
345-
const errorClass = OAUTH_ERRORS[error] || ServerError;
346-
return new errorClass(error_description || '', error_uri);
339+
return OAuthError.fromResponse(result);
347340
} catch (error) {
348341
// Not a valid OAuth error response, but try to inform the user of the raw data anyway
349342
const errorMessage = `${statusCode ? `HTTP ${statusCode}: ` : ''}Invalid OAuth error response: ${error}. Raw body: ${body}`;
350-
return new ServerError(errorMessage);
343+
return new OAuthError(OAuthErrorCode.ServerError, errorMessage);
351344
}
352345
}
353346

@@ -371,12 +364,14 @@ export async function auth(
371364
return await authInternal(provider, options);
372365
} catch (error) {
373366
// Handle recoverable error types by invalidating credentials and retrying
374-
if (error instanceof InvalidClientError || error instanceof UnauthorizedClientError) {
375-
await provider.invalidateCredentials?.('all');
376-
return await authInternal(provider, options);
377-
} else if (error instanceof InvalidGrantError) {
378-
await provider.invalidateCredentials?.('tokens');
379-
return await authInternal(provider, options);
367+
if (error instanceof OAuthError) {
368+
if (error.code === OAuthErrorCode.InvalidClient || error.code === OAuthErrorCode.UnauthorizedClient) {
369+
await provider.invalidateCredentials?.('all');
370+
return await authInternal(provider, options);
371+
} else if (error.code === OAuthErrorCode.InvalidGrant) {
372+
await provider.invalidateCredentials?.('tokens');
373+
return await authInternal(provider, options);
374+
}
380375
}
381376

382377
// Throw otherwise
@@ -437,7 +432,8 @@ async function authInternal(
437432
const clientMetadataUrl = provider.clientMetadataUrl;
438433

439434
if (clientMetadataUrl && !isHttpsUrl(clientMetadataUrl)) {
440-
throw new InvalidClientMetadataError(
435+
throw new OAuthError(
436+
OAuthErrorCode.InvalidClientMetadata,
441437
`clientMetadataUrl must be a valid HTTPS URL with a non-root pathname, got: ${clientMetadataUrl}`
442438
);
443439
}
@@ -502,7 +498,7 @@ async function authInternal(
502498
return 'AUTHORIZED';
503499
} catch (error) {
504500
// If this is a ServerError, or an unknown type, log it out and try to continue. Otherwise, escalate so we can fix things and retry.
505-
if (!(error instanceof OAuthError) || error instanceof ServerError) {
501+
if (!(error instanceof OAuthError) || error.code === OAuthErrorCode.ServerError) {
506502
// Could not refresh OAuth tokens
507503
} else {
508504
// Refresh failed for another reason, re-throw

0 commit comments

Comments
 (0)