Skip to content

Commit 03ad73e

Browse files
committed
lefthook merge commit
2 parents 91989fc + 0f0a4eb commit 03ad73e

31 files changed

Lines changed: 930 additions & 556 deletions

docs/migration-SKILL.md

Lines changed: 118 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -82,19 +82,127 @@ Notes:
8282

8383
## 5. Removed / Renamed Type Aliases and Symbols
8484

85-
| v1 (removed) | v2 (replacement) |
86-
| ---------------------------------------- | ------------------------------------------------ |
87-
| `JSONRPCError` | `JSONRPCErrorResponse` |
88-
| `JSONRPCErrorSchema` | `JSONRPCErrorResponseSchema` |
89-
| `isJSONRPCError` | `isJSONRPCErrorResponse` |
90-
| `isJSONRPCResponse` | `isJSONRPCResultResponse` |
91-
| `ResourceReference` | `ResourceTemplateReference` |
92-
| `ResourceReferenceSchema` | `ResourceTemplateReferenceSchema` |
93-
| `IsomorphicHeaders` | REMOVED (use Web Standard `Headers`) |
94-
| `AuthInfo` (from `server/auth/types.js`) | `AuthInfo` (now in `@modelcontextprotocol/core`) |
85+
| v1 (removed) | v2 (replacement) |
86+
| ---------------------------------------- | -------------------------------------------------------- |
87+
| `JSONRPCError` | `JSONRPCErrorResponse` |
88+
| `JSONRPCErrorSchema` | `JSONRPCErrorResponseSchema` |
89+
| `isJSONRPCError` | `isJSONRPCErrorResponse` |
90+
| `isJSONRPCResponse` | `isJSONRPCResultResponse` |
91+
| `ResourceReference` | `ResourceTemplateReference` |
92+
| `ResourceReferenceSchema` | `ResourceTemplateReferenceSchema` |
93+
| `IsomorphicHeaders` | REMOVED (use Web Standard `Headers`) |
94+
| `AuthInfo` (from `server/auth/types.js`) | `AuthInfo` (now in `@modelcontextprotocol/core`) |
95+
| `McpError` | `ProtocolError` |
96+
| `ErrorCode` | `ProtocolErrorCode` |
97+
| `ErrorCode.RequestTimeout` | `SdkErrorCode.RequestTimeout` |
98+
| `ErrorCode.ConnectionClosed` | `SdkErrorCode.ConnectionClosed` |
99+
| `StreamableHTTPError` | REMOVED (use `SdkError` with `SdkErrorCode.ClientHttp*`) |
95100

96101
All other symbols from `@modelcontextprotocol/sdk/types.js` retain their original names (e.g., `CallToolResultSchema`, `ListToolsResultSchema`, etc.).
97102

103+
### Error class changes
104+
105+
Two error classes now exist:
106+
107+
- **`ProtocolError`** (renamed from `McpError`): Protocol errors that cross the wire as JSON-RPC responses
108+
- **`SdkError`** (new): Local SDK errors that never cross the wire
109+
110+
| Error scenario | v1 type | v2 type |
111+
| -------------------------------- | -------------------------------------------- | ----------------------------------------------------------------- |
112+
| Request timeout | `McpError` with `ErrorCode.RequestTimeout` | `SdkError` with `SdkErrorCode.RequestTimeout` |
113+
| Connection closed | `McpError` with `ErrorCode.ConnectionClosed` | `SdkError` with `SdkErrorCode.ConnectionClosed` |
114+
| Capability not supported | `new Error(...)` | `SdkError` with `SdkErrorCode.CapabilityNotSupported` |
115+
| Not connected | `new Error('Not connected')` | `SdkError` with `SdkErrorCode.NotConnected` |
116+
| Invalid params (server response) | `McpError` with `ErrorCode.InvalidParams` | `ProtocolError` with `ProtocolErrorCode.InvalidParams` |
117+
| HTTP transport error | `StreamableHTTPError` | `SdkError` with `SdkErrorCode.ClientHttp*` |
118+
| Failed to open SSE stream | `StreamableHTTPError` | `SdkError` with `SdkErrorCode.ClientHttpFailedToOpenStream` |
119+
| 401 after auth flow | `StreamableHTTPError` | `SdkError` with `SdkErrorCode.ClientHttpAuthentication` |
120+
| 403 after upscoping | `StreamableHTTPError` | `SdkError` with `SdkErrorCode.ClientHttpForbidden` |
121+
| Unexpected content type | `StreamableHTTPError` | `SdkError` with `SdkErrorCode.ClientHttpUnexpectedContent` |
122+
| Session termination failed | `StreamableHTTPError` | `SdkError` with `SdkErrorCode.ClientHttpFailedToTerminateSession` |
123+
124+
New `SdkErrorCode` enum values:
125+
126+
- `SdkErrorCode.NotConnected` = `'NOT_CONNECTED'`
127+
- `SdkErrorCode.AlreadyConnected` = `'ALREADY_CONNECTED'`
128+
- `SdkErrorCode.NotInitialized` = `'NOT_INITIALIZED'`
129+
- `SdkErrorCode.CapabilityNotSupported` = `'CAPABILITY_NOT_SUPPORTED'`
130+
- `SdkErrorCode.RequestTimeout` = `'REQUEST_TIMEOUT'`
131+
- `SdkErrorCode.ConnectionClosed` = `'CONNECTION_CLOSED'`
132+
- `SdkErrorCode.SendFailed` = `'SEND_FAILED'`
133+
- `SdkErrorCode.ClientHttpNotImplemented` = `'CLIENT_HTTP_NOT_IMPLEMENTED'`
134+
- `SdkErrorCode.ClientHttpAuthentication` = `'CLIENT_HTTP_AUTHENTICATION'`
135+
- `SdkErrorCode.ClientHttpForbidden` = `'CLIENT_HTTP_FORBIDDEN'`
136+
- `SdkErrorCode.ClientHttpUnexpectedContent` = `'CLIENT_HTTP_UNEXPECTED_CONTENT'`
137+
- `SdkErrorCode.ClientHttpFailedToOpenStream` = `'CLIENT_HTTP_FAILED_TO_OPEN_STREAM'`
138+
- `SdkErrorCode.ClientHttpFailedToTerminateSession` = `'CLIENT_HTTP_FAILED_TO_TERMINATE_SESSION'`
139+
140+
Update error handling:
141+
142+
```typescript
143+
// v1
144+
if (error instanceof McpError && error.code === ErrorCode.RequestTimeout) { ... }
145+
146+
// v2
147+
import { SdkError, SdkErrorCode } from '@modelcontextprotocol/core';
148+
if (error instanceof SdkError && error.code === SdkErrorCode.RequestTimeout) { ... }
149+
```
150+
151+
Update HTTP transport error handling:
152+
153+
```typescript
154+
// v1
155+
import { StreamableHTTPError } from '@modelcontextprotocol/sdk/client/streamableHttp.js';
156+
if (error instanceof StreamableHTTPError) {
157+
console.log('HTTP status:', error.code);
158+
}
159+
160+
// v2
161+
import { SdkError, SdkErrorCode } from '@modelcontextprotocol/core';
162+
if (error instanceof SdkError && error.code === SdkErrorCode.ClientHttpFailedToOpenStream) {
163+
const status = (error.data as { status?: number })?.status;
164+
}
165+
```
166+
167+
### OAuth error consolidation
168+
169+
Individual OAuth error classes replaced with single `OAuthError` class and `OAuthErrorCode` enum:
170+
171+
| v1 Class | v2 Equivalent |
172+
| ------------------------------ | ---------------------------------------------------------- |
173+
| `InvalidRequestError` | `OAuthError` with `OAuthErrorCode.InvalidRequest` |
174+
| `InvalidClientError` | `OAuthError` with `OAuthErrorCode.InvalidClient` |
175+
| `InvalidGrantError` | `OAuthError` with `OAuthErrorCode.InvalidGrant` |
176+
| `UnauthorizedClientError` | `OAuthError` with `OAuthErrorCode.UnauthorizedClient` |
177+
| `UnsupportedGrantTypeError` | `OAuthError` with `OAuthErrorCode.UnsupportedGrantType` |
178+
| `InvalidScopeError` | `OAuthError` with `OAuthErrorCode.InvalidScope` |
179+
| `AccessDeniedError` | `OAuthError` with `OAuthErrorCode.AccessDenied` |
180+
| `ServerError` | `OAuthError` with `OAuthErrorCode.ServerError` |
181+
| `TemporarilyUnavailableError` | `OAuthError` with `OAuthErrorCode.TemporarilyUnavailable` |
182+
| `UnsupportedResponseTypeError` | `OAuthError` with `OAuthErrorCode.UnsupportedResponseType` |
183+
| `UnsupportedTokenTypeError` | `OAuthError` with `OAuthErrorCode.UnsupportedTokenType` |
184+
| `InvalidTokenError` | `OAuthError` with `OAuthErrorCode.InvalidToken` |
185+
| `MethodNotAllowedError` | `OAuthError` with `OAuthErrorCode.MethodNotAllowed` |
186+
| `TooManyRequestsError` | `OAuthError` with `OAuthErrorCode.TooManyRequests` |
187+
| `InvalidClientMetadataError` | `OAuthError` with `OAuthErrorCode.InvalidClientMetadata` |
188+
| `InsufficientScopeError` | `OAuthError` with `OAuthErrorCode.InsufficientScope` |
189+
| `InvalidTargetError` | `OAuthError` with `OAuthErrorCode.InvalidTarget` |
190+
| `CustomOAuthError` | `new OAuthError(customCode, message)` |
191+
192+
Removed: `OAUTH_ERRORS` constant.
193+
194+
Update OAuth error handling:
195+
196+
```typescript
197+
// v1
198+
import { InvalidClientError, InvalidGrantError } from '@modelcontextprotocol/core';
199+
if (error instanceof InvalidClientError) { ... }
200+
201+
// v2
202+
import { OAuthError, OAuthErrorCode } from '@modelcontextprotocol/core';
203+
if (error instanceof OAuthError && error.code === OAuthErrorCode.InvalidClient) { ... }
204+
```
205+
98206
**Unchanged APIs** (only import paths changed): `Client` constructor and methods, `McpServer` constructor, `server.connect()`, `server.close()`, all client transports (`StreamableHTTPClientTransport`, `SSEClientTransport`, `StdioClientTransport`), `StdioServerTransport`, all Zod
99207
schemas, all callback return types.
100208

docs/migration.md

Lines changed: 202 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -344,6 +344,208 @@ import { JSONRPCError, ResourceReference, isJSONRPCError } from '@modelcontextpr
344344
import { JSONRPCErrorResponse, ResourceTemplateReference, isJSONRPCErrorResponse } from '@modelcontextprotocol/core';
345345
```
346346

347+
### Error hierarchy refactoring
348+
349+
The SDK now distinguishes between two types of errors:
350+
351+
1. **`ProtocolError`** (renamed from `McpError`): Protocol errors that cross the wire as JSON-RPC error responses
352+
2. **`SdkError`**: Local SDK errors that never cross the wire (timeouts, connection issues, capability checks)
353+
354+
#### Renamed exports
355+
356+
| v1 | v2 |
357+
| ---------------------------- | ------------------------------- |
358+
| `McpError` | `ProtocolError` |
359+
| `ErrorCode` | `ProtocolErrorCode` |
360+
| `ErrorCode.RequestTimeout` | `SdkErrorCode.RequestTimeout` |
361+
| `ErrorCode.ConnectionClosed` | `SdkErrorCode.ConnectionClosed` |
362+
363+
**Before (v1):**
364+
365+
```typescript
366+
import { McpError, ErrorCode } from '@modelcontextprotocol/sdk/types.js';
367+
368+
try {
369+
await client.callTool({ name: 'test', arguments: {} });
370+
} catch (error) {
371+
if (error instanceof McpError && error.code === ErrorCode.RequestTimeout) {
372+
console.log('Request timed out');
373+
}
374+
if (error instanceof McpError && error.code === ErrorCode.InvalidParams) {
375+
console.log('Invalid parameters');
376+
}
377+
}
378+
```
379+
380+
**After (v2):**
381+
382+
```typescript
383+
import { ProtocolError, ProtocolErrorCode, SdkError, SdkErrorCode } from '@modelcontextprotocol/core';
384+
385+
try {
386+
await client.callTool({ name: 'test', arguments: {} });
387+
} catch (error) {
388+
// Local timeout/connection errors are now SdkError
389+
if (error instanceof SdkError && error.code === SdkErrorCode.RequestTimeout) {
390+
console.log('Request timed out');
391+
}
392+
// Protocol errors from the server are still ProtocolError
393+
if (error instanceof ProtocolError && error.code === ProtocolErrorCode.InvalidParams) {
394+
console.log('Invalid parameters');
395+
}
396+
}
397+
```
398+
399+
#### New `SdkErrorCode` enum
400+
401+
The new `SdkErrorCode` enum contains string-valued codes for local SDK errors:
402+
403+
| Code | Description |
404+
| ------------------------------------------------- | ------------------------------------------ |
405+
| `SdkErrorCode.NotConnected` | Transport is not connected |
406+
| `SdkErrorCode.AlreadyConnected` | Transport is already connected |
407+
| `SdkErrorCode.NotInitialized` | Protocol is not initialized |
408+
| `SdkErrorCode.CapabilityNotSupported` | Required capability is not supported |
409+
| `SdkErrorCode.RequestTimeout` | Request timed out waiting for response |
410+
| `SdkErrorCode.ConnectionClosed` | Connection was closed |
411+
| `SdkErrorCode.SendFailed` | Failed to send message |
412+
| `SdkErrorCode.ClientHttpNotImplemented` | HTTP POST request failed |
413+
| `SdkErrorCode.ClientHttpAuthentication` | Server returned 401 after successful auth |
414+
| `SdkErrorCode.ClientHttpForbidden` | Server returned 403 after trying upscoping |
415+
| `SdkErrorCode.ClientHttpUnexpectedContent` | Unexpected content type in HTTP response |
416+
| `SdkErrorCode.ClientHttpFailedToOpenStream` | Failed to open SSE stream |
417+
| `SdkErrorCode.ClientHttpFailedToTerminateSession` | Failed to terminate session |
418+
419+
#### `StreamableHTTPError` removed
420+
421+
The `StreamableHTTPError` class has been removed. HTTP transport errors are now thrown as `SdkError` with specific `SdkErrorCode` values that provide more granular error information:
422+
423+
**Before (v1):**
424+
425+
```typescript
426+
import { StreamableHTTPError } from '@modelcontextprotocol/sdk/client/streamableHttp.js';
427+
428+
try {
429+
await transport.send(message);
430+
} catch (error) {
431+
if (error instanceof StreamableHTTPError) {
432+
console.log('HTTP error:', error.code); // HTTP status code
433+
}
434+
}
435+
```
436+
437+
**After (v2):**
438+
439+
```typescript
440+
import { SdkError, SdkErrorCode } from '@modelcontextprotocol/core';
441+
442+
try {
443+
await transport.send(message);
444+
} catch (error) {
445+
if (error instanceof SdkError) {
446+
switch (error.code) {
447+
case SdkErrorCode.ClientHttpAuthentication:
448+
console.log('Auth failed after completing auth flow');
449+
break;
450+
case SdkErrorCode.ClientHttpForbidden:
451+
console.log('Forbidden after upscoping attempt');
452+
break;
453+
case SdkErrorCode.ClientHttpFailedToOpenStream:
454+
console.log('Failed to open SSE stream');
455+
break;
456+
case SdkErrorCode.ClientHttpNotImplemented:
457+
console.log('HTTP request failed');
458+
break;
459+
}
460+
// Access HTTP status code from error.data if needed
461+
const httpStatus = (error.data as { status?: number })?.status;
462+
}
463+
}
464+
```
465+
466+
#### Why this change?
467+
468+
Previously, `ErrorCode.RequestTimeout` (-32001) and `ErrorCode.ConnectionClosed` (-32000) were used for local timeout/connection errors. However, these errors never cross the wire as JSON-RPC responses - they are rejected locally. Using protocol error codes for local errors was
469+
semantically inconsistent.
470+
471+
The new design:
472+
473+
- `ProtocolError` with `ProtocolErrorCode`: For errors that are serialized and sent as JSON-RPC error responses
474+
- `SdkError` with `SdkErrorCode`: For local errors that are thrown/rejected locally and never leave the SDK
475+
476+
### OAuth error refactoring
477+
478+
The OAuth error classes have been consolidated into a single `OAuthError` class with an `OAuthErrorCode` enum.
479+
480+
#### Removed classes
481+
482+
The following individual error classes have been removed in favor of `OAuthError` with the appropriate code:
483+
484+
| v1 Class | v2 Equivalent |
485+
| ------------------------------ | ----------------------------------------------------------------- |
486+
| `InvalidRequestError` | `new OAuthError(OAuthErrorCode.InvalidRequest, message)` |
487+
| `InvalidClientError` | `new OAuthError(OAuthErrorCode.InvalidClient, message)` |
488+
| `InvalidGrantError` | `new OAuthError(OAuthErrorCode.InvalidGrant, message)` |
489+
| `UnauthorizedClientError` | `new OAuthError(OAuthErrorCode.UnauthorizedClient, message)` |
490+
| `UnsupportedGrantTypeError` | `new OAuthError(OAuthErrorCode.UnsupportedGrantType, message)` |
491+
| `InvalidScopeError` | `new OAuthError(OAuthErrorCode.InvalidScope, message)` |
492+
| `AccessDeniedError` | `new OAuthError(OAuthErrorCode.AccessDenied, message)` |
493+
| `ServerError` | `new OAuthError(OAuthErrorCode.ServerError, message)` |
494+
| `TemporarilyUnavailableError` | `new OAuthError(OAuthErrorCode.TemporarilyUnavailable, message)` |
495+
| `UnsupportedResponseTypeError` | `new OAuthError(OAuthErrorCode.UnsupportedResponseType, message)` |
496+
| `UnsupportedTokenTypeError` | `new OAuthError(OAuthErrorCode.UnsupportedTokenType, message)` |
497+
| `InvalidTokenError` | `new OAuthError(OAuthErrorCode.InvalidToken, message)` |
498+
| `MethodNotAllowedError` | `new OAuthError(OAuthErrorCode.MethodNotAllowed, message)` |
499+
| `TooManyRequestsError` | `new OAuthError(OAuthErrorCode.TooManyRequests, message)` |
500+
| `InvalidClientMetadataError` | `new OAuthError(OAuthErrorCode.InvalidClientMetadata, message)` |
501+
| `InsufficientScopeError` | `new OAuthError(OAuthErrorCode.InsufficientScope, message)` |
502+
| `InvalidTargetError` | `new OAuthError(OAuthErrorCode.InvalidTarget, message)` |
503+
| `CustomOAuthError` | `new OAuthError(customCode, message)` |
504+
505+
The `OAUTH_ERRORS` constant has also been removed.
506+
507+
**Before (v1):**
508+
509+
```typescript
510+
import { InvalidClientError, InvalidGrantError, ServerError } from '@modelcontextprotocol/core';
511+
512+
try {
513+
await refreshToken();
514+
} catch (error) {
515+
if (error instanceof InvalidClientError) {
516+
// Handle invalid client
517+
} else if (error instanceof InvalidGrantError) {
518+
// Handle invalid grant
519+
} else if (error instanceof ServerError) {
520+
// Handle server error
521+
}
522+
}
523+
```
524+
525+
**After (v2):**
526+
527+
```typescript
528+
import { OAuthError, OAuthErrorCode } from '@modelcontextprotocol/core';
529+
530+
try {
531+
await refreshToken();
532+
} catch (error) {
533+
if (error instanceof OAuthError) {
534+
switch (error.code) {
535+
case OAuthErrorCode.InvalidClient:
536+
// Handle invalid client
537+
break;
538+
case OAuthErrorCode.InvalidGrant:
539+
// Handle invalid grant
540+
break;
541+
case OAuthErrorCode.ServerError:
542+
// Handle server error
543+
break;
544+
}
545+
}
546+
}
547+
```
548+
347549
## Unchanged APIs
348550

349551
The following APIs are unchanged between v1 and v2 (only the import paths changed):

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

0 commit comments

Comments
 (0)