v2: Errors refactor (ProtocolError, SdkError, OAuthError) (#1454)
This commit is contained in:
parent
160902eb2e
commit
0f0a4eb7c8
33 changed files with 1086 additions and 705 deletions
|
|
@ -29,8 +29,7 @@ When making breaking changes, document them in **both**:
|
|||
- `docs/migration.md` — human-readable guide with before/after code examples
|
||||
- `docs/migration-SKILL.md` — LLM-optimized mapping tables for mechanical migration
|
||||
|
||||
Include what changed, why, and how to migrate. Search for related sections and group
|
||||
related changes together rather than adding new standalone sections.
|
||||
Include what changed, why, and how to migrate. Search for related sections and group related changes together rather than adding new standalone sections.
|
||||
|
||||
## Code Style Guidelines
|
||||
|
||||
|
|
|
|||
|
|
@ -20,13 +20,13 @@ Remove the old package and install only what you need:
|
|||
npm uninstall @modelcontextprotocol/sdk
|
||||
```
|
||||
|
||||
| You need | Install |
|
||||
|----------|---------|
|
||||
| Client only | `npm install @modelcontextprotocol/client` |
|
||||
| Server only | `npm install @modelcontextprotocol/server` |
|
||||
| Server + Node.js HTTP | `npm install @modelcontextprotocol/server @modelcontextprotocol/node` |
|
||||
| Server + Express | `npm install @modelcontextprotocol/server @modelcontextprotocol/express` |
|
||||
| Server + Hono | `npm install @modelcontextprotocol/server @modelcontextprotocol/hono` |
|
||||
| You need | Install |
|
||||
| --------------------- | ------------------------------------------------------------------------ |
|
||||
| Client only | `npm install @modelcontextprotocol/client` |
|
||||
| Server only | `npm install @modelcontextprotocol/server` |
|
||||
| Server + Node.js HTTP | `npm install @modelcontextprotocol/server @modelcontextprotocol/node` |
|
||||
| Server + Express | `npm install @modelcontextprotocol/server @modelcontextprotocol/express` |
|
||||
| Server + Hono | `npm install @modelcontextprotocol/server @modelcontextprotocol/hono` |
|
||||
|
||||
`@modelcontextprotocol/core` is installed automatically as a dependency.
|
||||
|
||||
|
|
@ -36,65 +36,175 @@ Replace all `@modelcontextprotocol/sdk/...` imports using this table.
|
|||
|
||||
### Client imports
|
||||
|
||||
| v1 import path | v2 package |
|
||||
|----------------|------------|
|
||||
| `@modelcontextprotocol/sdk/client/index.js` | `@modelcontextprotocol/client` |
|
||||
| `@modelcontextprotocol/sdk/client/auth.js` | `@modelcontextprotocol/client` |
|
||||
| v1 import path | v2 package |
|
||||
| ---------------------------------------------------- | ------------------------------ |
|
||||
| `@modelcontextprotocol/sdk/client/index.js` | `@modelcontextprotocol/client` |
|
||||
| `@modelcontextprotocol/sdk/client/auth.js` | `@modelcontextprotocol/client` |
|
||||
| `@modelcontextprotocol/sdk/client/streamableHttp.js` | `@modelcontextprotocol/client` |
|
||||
| `@modelcontextprotocol/sdk/client/sse.js` | `@modelcontextprotocol/client` |
|
||||
| `@modelcontextprotocol/sdk/client/stdio.js` | `@modelcontextprotocol/client` |
|
||||
| `@modelcontextprotocol/sdk/client/websocket.js` | `@modelcontextprotocol/client` |
|
||||
| `@modelcontextprotocol/sdk/client/sse.js` | `@modelcontextprotocol/client` |
|
||||
| `@modelcontextprotocol/sdk/client/stdio.js` | `@modelcontextprotocol/client` |
|
||||
| `@modelcontextprotocol/sdk/client/websocket.js` | `@modelcontextprotocol/client` |
|
||||
|
||||
### Server imports
|
||||
|
||||
| v1 import path | v2 package |
|
||||
|----------------|------------|
|
||||
| `@modelcontextprotocol/sdk/server/mcp.js` | `@modelcontextprotocol/server` |
|
||||
| `@modelcontextprotocol/sdk/server/index.js` | `@modelcontextprotocol/server` |
|
||||
| `@modelcontextprotocol/sdk/server/stdio.js` | `@modelcontextprotocol/server` |
|
||||
| v1 import path | v2 package |
|
||||
| ---------------------------------------------------- | ----------------------------------------------------------------------------------- |
|
||||
| `@modelcontextprotocol/sdk/server/mcp.js` | `@modelcontextprotocol/server` |
|
||||
| `@modelcontextprotocol/sdk/server/index.js` | `@modelcontextprotocol/server` |
|
||||
| `@modelcontextprotocol/sdk/server/stdio.js` | `@modelcontextprotocol/server` |
|
||||
| `@modelcontextprotocol/sdk/server/streamableHttp.js` | `@modelcontextprotocol/node` (class renamed to `NodeStreamableHTTPServerTransport`) |
|
||||
| `@modelcontextprotocol/sdk/server/sse.js` | REMOVED (migrate to Streamable HTTP) |
|
||||
| `@modelcontextprotocol/sdk/server/auth/*` | REMOVED (use external auth library) |
|
||||
| `@modelcontextprotocol/sdk/server/middleware.js` | `@modelcontextprotocol/express` (signature changed, see section 8) |
|
||||
| `@modelcontextprotocol/sdk/server/sse.js` | REMOVED (migrate to Streamable HTTP) |
|
||||
| `@modelcontextprotocol/sdk/server/auth/*` | REMOVED (use external auth library) |
|
||||
| `@modelcontextprotocol/sdk/server/middleware.js` | `@modelcontextprotocol/express` (signature changed, see section 8) |
|
||||
|
||||
### Types / shared imports
|
||||
|
||||
| v1 import path | v2 package |
|
||||
|----------------|------------|
|
||||
| `@modelcontextprotocol/sdk/types.js` | `@modelcontextprotocol/core` |
|
||||
| `@modelcontextprotocol/sdk/shared/protocol.js` | `@modelcontextprotocol/core` |
|
||||
| `@modelcontextprotocol/sdk/shared/transport.js` | `@modelcontextprotocol/core` |
|
||||
| `@modelcontextprotocol/sdk/shared/stdio.js` | `@modelcontextprotocol/core` |
|
||||
| v1 import path | v2 package |
|
||||
| ------------------------------------------------- | ---------------------------- |
|
||||
| `@modelcontextprotocol/sdk/types.js` | `@modelcontextprotocol/core` |
|
||||
| `@modelcontextprotocol/sdk/shared/protocol.js` | `@modelcontextprotocol/core` |
|
||||
| `@modelcontextprotocol/sdk/shared/transport.js` | `@modelcontextprotocol/core` |
|
||||
| `@modelcontextprotocol/sdk/shared/stdio.js` | `@modelcontextprotocol/core` |
|
||||
| `@modelcontextprotocol/sdk/shared/uriTemplate.js` | `@modelcontextprotocol/core` |
|
||||
| `@modelcontextprotocol/sdk/shared/auth.js` | `@modelcontextprotocol/core` |
|
||||
| `@modelcontextprotocol/sdk/shared/auth.js` | `@modelcontextprotocol/core` |
|
||||
|
||||
Notes:
|
||||
|
||||
- `@modelcontextprotocol/client` and `@modelcontextprotocol/server` both re-export everything from `@modelcontextprotocol/core`, so you can import types from whichever package you already depend on.
|
||||
- When multiple v1 imports map to the same v2 package, consolidate them into a single import statement.
|
||||
- If code imports from `sdk/client/...`, install `@modelcontextprotocol/client`. If from `sdk/server/...`, install `@modelcontextprotocol/server`. If from `sdk/types.js` or `sdk/shared/...` only, install `@modelcontextprotocol/core`.
|
||||
|
||||
## 4. Renamed Symbols
|
||||
|
||||
| v1 symbol | v2 symbol | v2 package |
|
||||
|-----------|-----------|------------|
|
||||
| v1 symbol | v2 symbol | v2 package |
|
||||
| ------------------------------- | ----------------------------------- | ---------------------------- |
|
||||
| `StreamableHTTPServerTransport` | `NodeStreamableHTTPServerTransport` | `@modelcontextprotocol/node` |
|
||||
|
||||
## 5. Removed / Renamed Type Aliases and Symbols
|
||||
|
||||
| v1 (removed) | v2 (replacement) |
|
||||
|--------------|------------------|
|
||||
| `JSONRPCError` | `JSONRPCErrorResponse` |
|
||||
| `JSONRPCErrorSchema` | `JSONRPCErrorResponseSchema` |
|
||||
| `isJSONRPCError` | `isJSONRPCErrorResponse` |
|
||||
| `isJSONRPCResponse` | `isJSONRPCResultResponse` |
|
||||
| `ResourceReference` | `ResourceTemplateReference` |
|
||||
| `ResourceReferenceSchema` | `ResourceTemplateReferenceSchema` |
|
||||
| `IsomorphicHeaders` | REMOVED (use Web Standard `Headers`) |
|
||||
| `AuthInfo` (from `server/auth/types.js`) | `AuthInfo` (now in `@modelcontextprotocol/core`) |
|
||||
| v1 (removed) | v2 (replacement) |
|
||||
| ---------------------------------------- | -------------------------------------------------------- |
|
||||
| `JSONRPCError` | `JSONRPCErrorResponse` |
|
||||
| `JSONRPCErrorSchema` | `JSONRPCErrorResponseSchema` |
|
||||
| `isJSONRPCError` | `isJSONRPCErrorResponse` |
|
||||
| `isJSONRPCResponse` | `isJSONRPCResultResponse` |
|
||||
| `ResourceReference` | `ResourceTemplateReference` |
|
||||
| `ResourceReferenceSchema` | `ResourceTemplateReferenceSchema` |
|
||||
| `IsomorphicHeaders` | REMOVED (use Web Standard `Headers`) |
|
||||
| `AuthInfo` (from `server/auth/types.js`) | `AuthInfo` (now in `@modelcontextprotocol/core`) |
|
||||
| `McpError` | `ProtocolError` |
|
||||
| `ErrorCode` | `ProtocolErrorCode` |
|
||||
| `ErrorCode.RequestTimeout` | `SdkErrorCode.RequestTimeout` |
|
||||
| `ErrorCode.ConnectionClosed` | `SdkErrorCode.ConnectionClosed` |
|
||||
| `StreamableHTTPError` | REMOVED (use `SdkError` with `SdkErrorCode.ClientHttp*`) |
|
||||
|
||||
All other symbols from `@modelcontextprotocol/sdk/types.js` retain their original names (e.g., `CallToolResultSchema`, `ListToolsResultSchema`, etc.).
|
||||
|
||||
**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 schemas, all callback return types.
|
||||
### Error class changes
|
||||
|
||||
Two error classes now exist:
|
||||
|
||||
- **`ProtocolError`** (renamed from `McpError`): Protocol errors that cross the wire as JSON-RPC responses
|
||||
- **`SdkError`** (new): Local SDK errors that never cross the wire
|
||||
|
||||
| Error scenario | v1 type | v2 type |
|
||||
| -------------------------------- | -------------------------------------------- | ----------------------------------------------------------------- |
|
||||
| Request timeout | `McpError` with `ErrorCode.RequestTimeout` | `SdkError` with `SdkErrorCode.RequestTimeout` |
|
||||
| Connection closed | `McpError` with `ErrorCode.ConnectionClosed` | `SdkError` with `SdkErrorCode.ConnectionClosed` |
|
||||
| Capability not supported | `new Error(...)` | `SdkError` with `SdkErrorCode.CapabilityNotSupported` |
|
||||
| Not connected | `new Error('Not connected')` | `SdkError` with `SdkErrorCode.NotConnected` |
|
||||
| Invalid params (server response) | `McpError` with `ErrorCode.InvalidParams` | `ProtocolError` with `ProtocolErrorCode.InvalidParams` |
|
||||
| HTTP transport error | `StreamableHTTPError` | `SdkError` with `SdkErrorCode.ClientHttp*` |
|
||||
| Failed to open SSE stream | `StreamableHTTPError` | `SdkError` with `SdkErrorCode.ClientHttpFailedToOpenStream` |
|
||||
| 401 after auth flow | `StreamableHTTPError` | `SdkError` with `SdkErrorCode.ClientHttpAuthentication` |
|
||||
| 403 after upscoping | `StreamableHTTPError` | `SdkError` with `SdkErrorCode.ClientHttpForbidden` |
|
||||
| Unexpected content type | `StreamableHTTPError` | `SdkError` with `SdkErrorCode.ClientHttpUnexpectedContent` |
|
||||
| Session termination failed | `StreamableHTTPError` | `SdkError` with `SdkErrorCode.ClientHttpFailedToTerminateSession` |
|
||||
|
||||
New `SdkErrorCode` enum values:
|
||||
|
||||
- `SdkErrorCode.NotConnected` = `'NOT_CONNECTED'`
|
||||
- `SdkErrorCode.AlreadyConnected` = `'ALREADY_CONNECTED'`
|
||||
- `SdkErrorCode.NotInitialized` = `'NOT_INITIALIZED'`
|
||||
- `SdkErrorCode.CapabilityNotSupported` = `'CAPABILITY_NOT_SUPPORTED'`
|
||||
- `SdkErrorCode.RequestTimeout` = `'REQUEST_TIMEOUT'`
|
||||
- `SdkErrorCode.ConnectionClosed` = `'CONNECTION_CLOSED'`
|
||||
- `SdkErrorCode.SendFailed` = `'SEND_FAILED'`
|
||||
- `SdkErrorCode.ClientHttpNotImplemented` = `'CLIENT_HTTP_NOT_IMPLEMENTED'`
|
||||
- `SdkErrorCode.ClientHttpAuthentication` = `'CLIENT_HTTP_AUTHENTICATION'`
|
||||
- `SdkErrorCode.ClientHttpForbidden` = `'CLIENT_HTTP_FORBIDDEN'`
|
||||
- `SdkErrorCode.ClientHttpUnexpectedContent` = `'CLIENT_HTTP_UNEXPECTED_CONTENT'`
|
||||
- `SdkErrorCode.ClientHttpFailedToOpenStream` = `'CLIENT_HTTP_FAILED_TO_OPEN_STREAM'`
|
||||
- `SdkErrorCode.ClientHttpFailedToTerminateSession` = `'CLIENT_HTTP_FAILED_TO_TERMINATE_SESSION'`
|
||||
|
||||
Update error handling:
|
||||
|
||||
```typescript
|
||||
// v1
|
||||
if (error instanceof McpError && error.code === ErrorCode.RequestTimeout) { ... }
|
||||
|
||||
// v2
|
||||
import { SdkError, SdkErrorCode } from '@modelcontextprotocol/core';
|
||||
if (error instanceof SdkError && error.code === SdkErrorCode.RequestTimeout) { ... }
|
||||
```
|
||||
|
||||
Update HTTP transport error handling:
|
||||
|
||||
```typescript
|
||||
// v1
|
||||
import { StreamableHTTPError } from '@modelcontextprotocol/sdk/client/streamableHttp.js';
|
||||
if (error instanceof StreamableHTTPError) {
|
||||
console.log('HTTP status:', error.code);
|
||||
}
|
||||
|
||||
// v2
|
||||
import { SdkError, SdkErrorCode } from '@modelcontextprotocol/core';
|
||||
if (error instanceof SdkError && error.code === SdkErrorCode.ClientHttpFailedToOpenStream) {
|
||||
const status = (error.data as { status?: number })?.status;
|
||||
}
|
||||
```
|
||||
|
||||
### OAuth error consolidation
|
||||
|
||||
Individual OAuth error classes replaced with single `OAuthError` class and `OAuthErrorCode` enum:
|
||||
|
||||
| v1 Class | v2 Equivalent |
|
||||
| ------------------------------ | ---------------------------------------------------------- |
|
||||
| `InvalidRequestError` | `OAuthError` with `OAuthErrorCode.InvalidRequest` |
|
||||
| `InvalidClientError` | `OAuthError` with `OAuthErrorCode.InvalidClient` |
|
||||
| `InvalidGrantError` | `OAuthError` with `OAuthErrorCode.InvalidGrant` |
|
||||
| `UnauthorizedClientError` | `OAuthError` with `OAuthErrorCode.UnauthorizedClient` |
|
||||
| `UnsupportedGrantTypeError` | `OAuthError` with `OAuthErrorCode.UnsupportedGrantType` |
|
||||
| `InvalidScopeError` | `OAuthError` with `OAuthErrorCode.InvalidScope` |
|
||||
| `AccessDeniedError` | `OAuthError` with `OAuthErrorCode.AccessDenied` |
|
||||
| `ServerError` | `OAuthError` with `OAuthErrorCode.ServerError` |
|
||||
| `TemporarilyUnavailableError` | `OAuthError` with `OAuthErrorCode.TemporarilyUnavailable` |
|
||||
| `UnsupportedResponseTypeError` | `OAuthError` with `OAuthErrorCode.UnsupportedResponseType` |
|
||||
| `UnsupportedTokenTypeError` | `OAuthError` with `OAuthErrorCode.UnsupportedTokenType` |
|
||||
| `InvalidTokenError` | `OAuthError` with `OAuthErrorCode.InvalidToken` |
|
||||
| `MethodNotAllowedError` | `OAuthError` with `OAuthErrorCode.MethodNotAllowed` |
|
||||
| `TooManyRequestsError` | `OAuthError` with `OAuthErrorCode.TooManyRequests` |
|
||||
| `InvalidClientMetadataError` | `OAuthError` with `OAuthErrorCode.InvalidClientMetadata` |
|
||||
| `InsufficientScopeError` | `OAuthError` with `OAuthErrorCode.InsufficientScope` |
|
||||
| `InvalidTargetError` | `OAuthError` with `OAuthErrorCode.InvalidTarget` |
|
||||
| `CustomOAuthError` | `new OAuthError(customCode, message)` |
|
||||
|
||||
Removed: `OAUTH_ERRORS` constant.
|
||||
|
||||
Update OAuth error handling:
|
||||
|
||||
```typescript
|
||||
// v1
|
||||
import { InvalidClientError, InvalidGrantError } from '@modelcontextprotocol/core';
|
||||
if (error instanceof InvalidClientError) { ... }
|
||||
|
||||
// v2
|
||||
import { OAuthError, OAuthErrorCode } from '@modelcontextprotocol/core';
|
||||
if (error instanceof OAuthError && error.code === OAuthErrorCode.InvalidClient) { ... }
|
||||
```
|
||||
|
||||
**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
|
||||
schemas, all callback return types.
|
||||
|
||||
## 6. McpServer API Changes
|
||||
|
||||
|
|
@ -105,21 +215,25 @@ The variadic `.tool()`, `.prompt()`, `.resource()` methods are removed. Use the
|
|||
```typescript
|
||||
// v1: server.tool(name, schema, callback)
|
||||
server.tool('greet', { name: z.string() }, async ({ name }) => {
|
||||
return { content: [{ type: 'text', text: `Hello, ${name}!` }] };
|
||||
return { content: [{ type: 'text', text: `Hello, ${name}!` }] };
|
||||
});
|
||||
|
||||
// v1: server.tool(name, description, schema, callback)
|
||||
server.tool('greet', 'Greet a user', { name: z.string() }, async ({ name }) => {
|
||||
return { content: [{ type: 'text', text: `Hello, ${name}!` }] };
|
||||
return { content: [{ type: 'text', text: `Hello, ${name}!` }] };
|
||||
});
|
||||
|
||||
// v2: server.registerTool(name, config, callback)
|
||||
server.registerTool('greet', {
|
||||
description: 'Greet a user',
|
||||
inputSchema: { name: z.string() },
|
||||
}, async ({ name }) => {
|
||||
return { content: [{ type: 'text', text: `Hello, ${name}!` }] };
|
||||
});
|
||||
server.registerTool(
|
||||
'greet',
|
||||
{
|
||||
description: 'Greet a user',
|
||||
inputSchema: { name: z.string() }
|
||||
},
|
||||
async ({ name }) => {
|
||||
return { content: [{ type: 'text', text: `Hello, ${name}!` }] };
|
||||
}
|
||||
);
|
||||
```
|
||||
|
||||
Config object fields: `title?`, `description?`, `inputSchema?`, `outputSchema?`, `annotations?`, `_meta?`
|
||||
|
|
@ -129,15 +243,19 @@ Config object fields: `title?`, `description?`, `inputSchema?`, `outputSchema?`,
|
|||
```typescript
|
||||
// v1: server.prompt(name, schema, callback)
|
||||
server.prompt('summarize', { text: z.string() }, async ({ text }) => {
|
||||
return { messages: [{ role: 'user', content: { type: 'text', text } }] };
|
||||
return { messages: [{ role: 'user', content: { type: 'text', text } }] };
|
||||
});
|
||||
|
||||
// v2: server.registerPrompt(name, config, callback)
|
||||
server.registerPrompt('summarize', {
|
||||
argsSchema: { text: z.string() },
|
||||
}, async ({ text }) => {
|
||||
return { messages: [{ role: 'user', content: { type: 'text', text } }] };
|
||||
});
|
||||
server.registerPrompt(
|
||||
'summarize',
|
||||
{
|
||||
argsSchema: { text: z.string() }
|
||||
},
|
||||
async ({ text }) => {
|
||||
return { messages: [{ role: 'user', content: { type: 'text', text } }] };
|
||||
}
|
||||
);
|
||||
```
|
||||
|
||||
Config object fields: `title?`, `description?`, `argsSchema?`
|
||||
|
|
@ -146,13 +264,13 @@ Config object fields: `title?`, `description?`, `argsSchema?`
|
|||
|
||||
```typescript
|
||||
// v1: server.resource(name, uri, callback)
|
||||
server.resource('config', 'config://app', async (uri) => {
|
||||
return { contents: [{ uri: uri.href, text: '{}' }] };
|
||||
server.resource('config', 'config://app', async uri => {
|
||||
return { contents: [{ uri: uri.href, text: '{}' }] };
|
||||
});
|
||||
|
||||
// v2: server.registerResource(name, uri, metadata, callback)
|
||||
server.registerResource('config', 'config://app', {}, async (uri) => {
|
||||
return { contents: [{ uri: uri.href, text: '{}' }] };
|
||||
server.registerResource('config', 'config://app', {}, async uri => {
|
||||
return { contents: [{ uri: uri.href, text: '{}' }] };
|
||||
});
|
||||
```
|
||||
|
||||
|
|
@ -180,7 +298,8 @@ extra.requestInfo?.headers.get('mcp-session-id')
|
|||
|
||||
### Server-side auth
|
||||
|
||||
All server OAuth exports removed: `mcpAuthRouter`, `OAuthServerProvider`, `OAuthTokenVerifier`, `requireBearerAuth`, `authenticateClient`, `ProxyOAuthServerProvider`, `allowedMethods`, and associated types. Use an external auth library (e.g., `better-auth`). See `examples/server/src/` for demos.
|
||||
All server OAuth exports removed: `mcpAuthRouter`, `OAuthServerProvider`, `OAuthTokenVerifier`, `requireBearerAuth`, `authenticateClient`, `ProxyOAuthServerProvider`, `allowedMethods`, and associated types. Use an external auth library (e.g., `better-auth`). See
|
||||
`examples/server/src/` for demos.
|
||||
|
||||
### Host header validation (Express)
|
||||
|
||||
|
|
@ -214,26 +333,26 @@ server.setNotificationHandler('notifications/message', (notification) => { ... }
|
|||
|
||||
Schema to method string mapping:
|
||||
|
||||
| v1 Schema | v2 Method String |
|
||||
|-----------|-----------------|
|
||||
| `InitializeRequestSchema` | `'initialize'` |
|
||||
| `CallToolRequestSchema` | `'tools/call'` |
|
||||
| `ListToolsRequestSchema` | `'tools/list'` |
|
||||
| `ListPromptsRequestSchema` | `'prompts/list'` |
|
||||
| `GetPromptRequestSchema` | `'prompts/get'` |
|
||||
| `ListResourcesRequestSchema` | `'resources/list'` |
|
||||
| `ReadResourceRequestSchema` | `'resources/read'` |
|
||||
| `CreateMessageRequestSchema` | `'sampling/createMessage'` |
|
||||
| `ElicitRequestSchema` | `'elicitation/create'` |
|
||||
| `SetLevelRequestSchema` | `'logging/setLevel'` |
|
||||
| `PingRequestSchema` | `'ping'` |
|
||||
| `LoggingMessageNotificationSchema` | `'notifications/message'` |
|
||||
| `ToolListChangedNotificationSchema` | `'notifications/tools/list_changed'` |
|
||||
| v1 Schema | v2 Method String |
|
||||
| --------------------------------------- | ---------------------------------------- |
|
||||
| `InitializeRequestSchema` | `'initialize'` |
|
||||
| `CallToolRequestSchema` | `'tools/call'` |
|
||||
| `ListToolsRequestSchema` | `'tools/list'` |
|
||||
| `ListPromptsRequestSchema` | `'prompts/list'` |
|
||||
| `GetPromptRequestSchema` | `'prompts/get'` |
|
||||
| `ListResourcesRequestSchema` | `'resources/list'` |
|
||||
| `ReadResourceRequestSchema` | `'resources/read'` |
|
||||
| `CreateMessageRequestSchema` | `'sampling/createMessage'` |
|
||||
| `ElicitRequestSchema` | `'elicitation/create'` |
|
||||
| `SetLevelRequestSchema` | `'logging/setLevel'` |
|
||||
| `PingRequestSchema` | `'ping'` |
|
||||
| `LoggingMessageNotificationSchema` | `'notifications/message'` |
|
||||
| `ToolListChangedNotificationSchema` | `'notifications/tools/list_changed'` |
|
||||
| `ResourceListChangedNotificationSchema` | `'notifications/resources/list_changed'` |
|
||||
| `PromptListChangedNotificationSchema` | `'notifications/prompts/list_changed'` |
|
||||
| `ProgressNotificationSchema` | `'notifications/progress'` |
|
||||
| `CancelledNotificationSchema` | `'notifications/cancelled'` |
|
||||
| `InitializedNotificationSchema` | `'notifications/initialized'` |
|
||||
| `PromptListChangedNotificationSchema` | `'notifications/prompts/list_changed'` |
|
||||
| `ProgressNotificationSchema` | `'notifications/progress'` |
|
||||
| `CancelledNotificationSchema` | `'notifications/cancelled'` |
|
||||
| `InitializedNotificationSchema` | `'notifications/initialized'` |
|
||||
|
||||
Request/notification params remain fully typed. Remove unused schema imports after migration.
|
||||
|
||||
|
|
|
|||
|
|
@ -4,7 +4,8 @@ This guide covers the breaking changes introduced in v2 of the MCP TypeScript SD
|
|||
|
||||
## Overview
|
||||
|
||||
Version 2 of the MCP TypeScript SDK introduces several breaking changes to improve modularity, reduce dependency bloat, and provide a cleaner API surface. The biggest change is the split from a single `@modelcontextprotocol/sdk` package into separate `@modelcontextprotocol/core`, `@modelcontextprotocol/client`, and `@modelcontextprotocol/server` packages.
|
||||
Version 2 of the MCP TypeScript SDK introduces several breaking changes to improve modularity, reduce dependency bloat, and provide a cleaner API surface. The biggest change is the split from a single `@modelcontextprotocol/sdk` package into separate `@modelcontextprotocol/core`,
|
||||
`@modelcontextprotocol/client`, and `@modelcontextprotocol/server` packages.
|
||||
|
||||
## Breaking Changes
|
||||
|
||||
|
|
@ -12,11 +13,11 @@ Version 2 of the MCP TypeScript SDK introduces several breaking changes to impro
|
|||
|
||||
The single `@modelcontextprotocol/sdk` package has been split into three packages:
|
||||
|
||||
| v1 | v2 |
|
||||
|----|-----|
|
||||
| v1 | v2 |
|
||||
| --------------------------- | ---------------------------------------------------------- |
|
||||
| `@modelcontextprotocol/sdk` | `@modelcontextprotocol/core` (types, protocol, transports) |
|
||||
| | `@modelcontextprotocol/client` (client implementation) |
|
||||
| | `@modelcontextprotocol/server` (server implementation) |
|
||||
| | `@modelcontextprotocol/client` (client implementation) |
|
||||
| | `@modelcontextprotocol/server` (server implementation) |
|
||||
|
||||
Remove the old package and install only the packages you need:
|
||||
|
||||
|
|
@ -64,6 +65,7 @@ Note: `@modelcontextprotocol/client` and `@modelcontextprotocol/server` both re-
|
|||
v2 requires **Node.js 20+** and ships **ESM only** (no more CommonJS builds).
|
||||
|
||||
If your project uses CommonJS (`require()`), you will need to either:
|
||||
|
||||
- Migrate to ESM (`import`/`export`)
|
||||
- Use dynamic `import()` to load the SDK
|
||||
|
||||
|
|
@ -71,11 +73,11 @@ If your project uses CommonJS (`require()`), you will need to either:
|
|||
|
||||
The server package no longer depends on Express or Hono. HTTP framework integrations are now separate middleware packages:
|
||||
|
||||
| v1 | v2 |
|
||||
|----|-----|
|
||||
| v1 | v2 |
|
||||
| -------------------------------------- | ------------------------------------------- |
|
||||
| Built into `@modelcontextprotocol/sdk` | `@modelcontextprotocol/node` (Node.js HTTP) |
|
||||
| | `@modelcontextprotocol/express` (Express) |
|
||||
| | `@modelcontextprotocol/hono` (Hono) |
|
||||
| | `@modelcontextprotocol/express` (Express) |
|
||||
| | `@modelcontextprotocol/hono` (Hono) |
|
||||
|
||||
Install the middleware package for your framework:
|
||||
|
||||
|
|
@ -128,12 +130,12 @@ This affects both transport constructors and request handler code that reads hea
|
|||
```typescript
|
||||
// Transport headers
|
||||
const transport = new StreamableHTTPClientTransport(url, {
|
||||
requestInit: {
|
||||
headers: {
|
||||
'Authorization': 'Bearer token',
|
||||
'X-Custom': 'value',
|
||||
},
|
||||
},
|
||||
requestInit: {
|
||||
headers: {
|
||||
Authorization: 'Bearer token',
|
||||
'X-Custom': 'value'
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
// Reading headers in a request handler
|
||||
|
|
@ -145,12 +147,12 @@ const sessionId = extra.requestInfo?.headers['mcp-session-id'];
|
|||
```typescript
|
||||
// Transport headers
|
||||
const transport = new StreamableHTTPClientTransport(url, {
|
||||
requestInit: {
|
||||
headers: new Headers({
|
||||
'Authorization': 'Bearer token',
|
||||
'X-Custom': 'value',
|
||||
}),
|
||||
},
|
||||
requestInit: {
|
||||
headers: new Headers({
|
||||
Authorization: 'Bearer token',
|
||||
'X-Custom': 'value'
|
||||
})
|
||||
}
|
||||
});
|
||||
|
||||
// Reading headers in a request handler
|
||||
|
|
@ -170,22 +172,22 @@ const server = new McpServer({ name: 'demo', version: '1.0.0' });
|
|||
|
||||
// Tool with schema
|
||||
server.tool('greet', { name: z.string() }, async ({ name }) => {
|
||||
return { content: [{ type: 'text', text: `Hello, ${name}!` }] };
|
||||
return { content: [{ type: 'text', text: `Hello, ${name}!` }] };
|
||||
});
|
||||
|
||||
// Tool with description
|
||||
server.tool('greet', 'Greet a user', { name: z.string() }, async ({ name }) => {
|
||||
return { content: [{ type: 'text', text: `Hello, ${name}!` }] };
|
||||
return { content: [{ type: 'text', text: `Hello, ${name}!` }] };
|
||||
});
|
||||
|
||||
// Prompt
|
||||
server.prompt('summarize', { text: z.string() }, async ({ text }) => {
|
||||
return { messages: [{ role: 'user', content: { type: 'text', text: `Summarize: ${text}` } }] };
|
||||
return { messages: [{ role: 'user', content: { type: 'text', text: `Summarize: ${text}` } }] };
|
||||
});
|
||||
|
||||
// Resource
|
||||
server.resource('config', 'config://app', async (uri) => {
|
||||
return { contents: [{ uri: uri.href, text: '{}' }] };
|
||||
server.resource('config', 'config://app', async uri => {
|
||||
return { contents: [{ uri: uri.href, text: '{}' }] };
|
||||
});
|
||||
```
|
||||
|
||||
|
|
@ -198,28 +200,29 @@ const server = new McpServer({ name: 'demo', version: '1.0.0' });
|
|||
|
||||
// Tool with schema
|
||||
server.registerTool('greet', { inputSchema: { name: z.string() } }, async ({ name }) => {
|
||||
return { content: [{ type: 'text', text: `Hello, ${name}!` }] };
|
||||
return { content: [{ type: 'text', text: `Hello, ${name}!` }] };
|
||||
});
|
||||
|
||||
// Tool with description
|
||||
server.registerTool('greet', { description: 'Greet a user', inputSchema: { name: z.string() } }, async ({ name }) => {
|
||||
return { content: [{ type: 'text', text: `Hello, ${name}!` }] };
|
||||
return { content: [{ type: 'text', text: `Hello, ${name}!` }] };
|
||||
});
|
||||
|
||||
// Prompt
|
||||
server.registerPrompt('summarize', { argsSchema: { text: z.string() } }, async ({ text }) => {
|
||||
return { messages: [{ role: 'user', content: { type: 'text', text: `Summarize: ${text}` } }] };
|
||||
return { messages: [{ role: 'user', content: { type: 'text', text: `Summarize: ${text}` } }] };
|
||||
});
|
||||
|
||||
// Resource
|
||||
server.registerResource('config', 'config://app', {}, async (uri) => {
|
||||
return { contents: [{ uri: uri.href, text: '{}' }] };
|
||||
server.registerResource('config', 'config://app', {}, async uri => {
|
||||
return { contents: [{ uri: uri.href, text: '{}' }] };
|
||||
});
|
||||
```
|
||||
|
||||
### Host header validation moved
|
||||
|
||||
Express-specific middleware (`hostHeaderValidation()`, `localhostHostValidation()`) moved from the server package to `@modelcontextprotocol/express`. The server package now exports framework-agnostic functions instead: `validateHostHeader()`, `localhostAllowedHostnames()`, `hostHeaderValidationResponse()`.
|
||||
Express-specific middleware (`hostHeaderValidation()`, `localhostHostValidation()`) moved from the server package to `@modelcontextprotocol/express`. The server package now exports framework-agnostic functions instead: `validateHostHeader()`, `localhostAllowedHostnames()`,
|
||||
`hostHeaderValidationResponse()`.
|
||||
|
||||
**Before (v1):**
|
||||
|
||||
|
|
@ -249,13 +252,13 @@ import { Server, InitializeRequestSchema, LoggingMessageNotificationSchema } fro
|
|||
const server = new Server({ name: 'my-server', version: '1.0.0' });
|
||||
|
||||
// Request handler with schema
|
||||
server.setRequestHandler(InitializeRequestSchema, async (request) => {
|
||||
return { protocolVersion: '...', capabilities: {}, serverInfo: { name: '...', version: '...' } };
|
||||
server.setRequestHandler(InitializeRequestSchema, async request => {
|
||||
return { protocolVersion: '...', capabilities: {}, serverInfo: { name: '...', version: '...' } };
|
||||
});
|
||||
|
||||
// Notification handler with schema
|
||||
server.setNotificationHandler(LoggingMessageNotificationSchema, (notification) => {
|
||||
console.log(notification.params.data);
|
||||
server.setNotificationHandler(LoggingMessageNotificationSchema, notification => {
|
||||
console.log(notification.params.data);
|
||||
});
|
||||
```
|
||||
|
||||
|
|
@ -267,13 +270,13 @@ import { Server } from '@modelcontextprotocol/server';
|
|||
const server = new Server({ name: 'my-server', version: '1.0.0' });
|
||||
|
||||
// Request handler with method string
|
||||
server.setRequestHandler('initialize', async (request) => {
|
||||
return { protocolVersion: '...', capabilities: {}, serverInfo: { name: '...', version: '...' } };
|
||||
server.setRequestHandler('initialize', async request => {
|
||||
return { protocolVersion: '...', capabilities: {}, serverInfo: { name: '...', version: '...' } };
|
||||
});
|
||||
|
||||
// Notification handler with method string
|
||||
server.setNotificationHandler('notifications/message', (notification) => {
|
||||
console.log(notification.params.data);
|
||||
server.setNotificationHandler('notifications/message', notification => {
|
||||
console.log(notification.params.data);
|
||||
});
|
||||
```
|
||||
|
||||
|
|
@ -281,21 +284,21 @@ The request and notification parameters remain fully typed via `RequestTypeMap`
|
|||
|
||||
Common method string replacements:
|
||||
|
||||
| Schema (v1) | Method string (v2) |
|
||||
|-------------|-------------------|
|
||||
| `InitializeRequestSchema` | `'initialize'` |
|
||||
| `CallToolRequestSchema` | `'tools/call'` |
|
||||
| `ListToolsRequestSchema` | `'tools/list'` |
|
||||
| `ListPromptsRequestSchema` | `'prompts/list'` |
|
||||
| `GetPromptRequestSchema` | `'prompts/get'` |
|
||||
| `ListResourcesRequestSchema` | `'resources/list'` |
|
||||
| `ReadResourceRequestSchema` | `'resources/read'` |
|
||||
| `CreateMessageRequestSchema` | `'sampling/createMessage'` |
|
||||
| `ElicitRequestSchema` | `'elicitation/create'` |
|
||||
| `LoggingMessageNotificationSchema` | `'notifications/message'` |
|
||||
| `ToolListChangedNotificationSchema` | `'notifications/tools/list_changed'` |
|
||||
| Schema (v1) | Method string (v2) |
|
||||
| --------------------------------------- | ---------------------------------------- |
|
||||
| `InitializeRequestSchema` | `'initialize'` |
|
||||
| `CallToolRequestSchema` | `'tools/call'` |
|
||||
| `ListToolsRequestSchema` | `'tools/list'` |
|
||||
| `ListPromptsRequestSchema` | `'prompts/list'` |
|
||||
| `GetPromptRequestSchema` | `'prompts/get'` |
|
||||
| `ListResourcesRequestSchema` | `'resources/list'` |
|
||||
| `ReadResourceRequestSchema` | `'resources/read'` |
|
||||
| `CreateMessageRequestSchema` | `'sampling/createMessage'` |
|
||||
| `ElicitRequestSchema` | `'elicitation/create'` |
|
||||
| `LoggingMessageNotificationSchema` | `'notifications/message'` |
|
||||
| `ToolListChangedNotificationSchema` | `'notifications/tools/list_changed'` |
|
||||
| `ResourceListChangedNotificationSchema` | `'notifications/resources/list_changed'` |
|
||||
| `PromptListChangedNotificationSchema` | `'notifications/prompts/list_changed'` |
|
||||
| `PromptListChangedNotificationSchema` | `'notifications/prompts/list_changed'` |
|
||||
|
||||
### Client list methods return empty results for missing capabilities
|
||||
|
||||
|
|
@ -304,24 +307,27 @@ Common method string replacements:
|
|||
To restore v1 behavior (throw an error when capabilities are missing), set `enforceStrictCapabilities: true`:
|
||||
|
||||
```typescript
|
||||
const client = new Client({ name: 'my-client', version: '1.0.0' }, {
|
||||
enforceStrictCapabilities: true,
|
||||
});
|
||||
const client = new Client(
|
||||
{ name: 'my-client', version: '1.0.0' },
|
||||
{
|
||||
enforceStrictCapabilities: true
|
||||
}
|
||||
);
|
||||
```
|
||||
|
||||
### Removed type aliases and deprecated exports
|
||||
|
||||
The following deprecated type aliases have been removed from `@modelcontextprotocol/core`:
|
||||
|
||||
| Removed | Replacement |
|
||||
|---------|-------------|
|
||||
| `JSONRPCError` | `JSONRPCErrorResponse` |
|
||||
| `JSONRPCErrorSchema` | `JSONRPCErrorResponseSchema` |
|
||||
| `isJSONRPCError` | `isJSONRPCErrorResponse` |
|
||||
| `isJSONRPCResponse` | `isJSONRPCResultResponse` |
|
||||
| `ResourceReferenceSchema` | `ResourceTemplateReferenceSchema` |
|
||||
| `ResourceReference` | `ResourceTemplateReference` |
|
||||
| `IsomorphicHeaders` | Use Web Standard `Headers` |
|
||||
| Removed | Replacement |
|
||||
| ---------------------------------------- | ------------------------------------------------ |
|
||||
| `JSONRPCError` | `JSONRPCErrorResponse` |
|
||||
| `JSONRPCErrorSchema` | `JSONRPCErrorResponseSchema` |
|
||||
| `isJSONRPCError` | `isJSONRPCErrorResponse` |
|
||||
| `isJSONRPCResponse` | `isJSONRPCResultResponse` |
|
||||
| `ResourceReferenceSchema` | `ResourceTemplateReferenceSchema` |
|
||||
| `ResourceReference` | `ResourceTemplateReference` |
|
||||
| `IsomorphicHeaders` | Use Web Standard `Headers` |
|
||||
| `AuthInfo` (from `server/auth/types.js`) | `AuthInfo` (now in `@modelcontextprotocol/core`) |
|
||||
|
||||
All other types and schemas exported from `@modelcontextprotocol/sdk/types.js` retain their original names in `@modelcontextprotocol/core`.
|
||||
|
|
@ -338,6 +344,208 @@ import { JSONRPCError, ResourceReference, isJSONRPCError } from '@modelcontextpr
|
|||
import { JSONRPCErrorResponse, ResourceTemplateReference, isJSONRPCErrorResponse } from '@modelcontextprotocol/core';
|
||||
```
|
||||
|
||||
### Error hierarchy refactoring
|
||||
|
||||
The SDK now distinguishes between two types of errors:
|
||||
|
||||
1. **`ProtocolError`** (renamed from `McpError`): Protocol errors that cross the wire as JSON-RPC error responses
|
||||
2. **`SdkError`**: Local SDK errors that never cross the wire (timeouts, connection issues, capability checks)
|
||||
|
||||
#### Renamed exports
|
||||
|
||||
| v1 | v2 |
|
||||
| ---------------------------- | ------------------------------- |
|
||||
| `McpError` | `ProtocolError` |
|
||||
| `ErrorCode` | `ProtocolErrorCode` |
|
||||
| `ErrorCode.RequestTimeout` | `SdkErrorCode.RequestTimeout` |
|
||||
| `ErrorCode.ConnectionClosed` | `SdkErrorCode.ConnectionClosed` |
|
||||
|
||||
**Before (v1):**
|
||||
|
||||
```typescript
|
||||
import { McpError, ErrorCode } from '@modelcontextprotocol/sdk/types.js';
|
||||
|
||||
try {
|
||||
await client.callTool({ name: 'test', arguments: {} });
|
||||
} catch (error) {
|
||||
if (error instanceof McpError && error.code === ErrorCode.RequestTimeout) {
|
||||
console.log('Request timed out');
|
||||
}
|
||||
if (error instanceof McpError && error.code === ErrorCode.InvalidParams) {
|
||||
console.log('Invalid parameters');
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
**After (v2):**
|
||||
|
||||
```typescript
|
||||
import { ProtocolError, ProtocolErrorCode, SdkError, SdkErrorCode } from '@modelcontextprotocol/core';
|
||||
|
||||
try {
|
||||
await client.callTool({ name: 'test', arguments: {} });
|
||||
} catch (error) {
|
||||
// Local timeout/connection errors are now SdkError
|
||||
if (error instanceof SdkError && error.code === SdkErrorCode.RequestTimeout) {
|
||||
console.log('Request timed out');
|
||||
}
|
||||
// Protocol errors from the server are still ProtocolError
|
||||
if (error instanceof ProtocolError && error.code === ProtocolErrorCode.InvalidParams) {
|
||||
console.log('Invalid parameters');
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
#### New `SdkErrorCode` enum
|
||||
|
||||
The new `SdkErrorCode` enum contains string-valued codes for local SDK errors:
|
||||
|
||||
| Code | Description |
|
||||
| ------------------------------------------------- | ------------------------------------------ |
|
||||
| `SdkErrorCode.NotConnected` | Transport is not connected |
|
||||
| `SdkErrorCode.AlreadyConnected` | Transport is already connected |
|
||||
| `SdkErrorCode.NotInitialized` | Protocol is not initialized |
|
||||
| `SdkErrorCode.CapabilityNotSupported` | Required capability is not supported |
|
||||
| `SdkErrorCode.RequestTimeout` | Request timed out waiting for response |
|
||||
| `SdkErrorCode.ConnectionClosed` | Connection was closed |
|
||||
| `SdkErrorCode.SendFailed` | Failed to send message |
|
||||
| `SdkErrorCode.ClientHttpNotImplemented` | HTTP POST request failed |
|
||||
| `SdkErrorCode.ClientHttpAuthentication` | Server returned 401 after successful auth |
|
||||
| `SdkErrorCode.ClientHttpForbidden` | Server returned 403 after trying upscoping |
|
||||
| `SdkErrorCode.ClientHttpUnexpectedContent` | Unexpected content type in HTTP response |
|
||||
| `SdkErrorCode.ClientHttpFailedToOpenStream` | Failed to open SSE stream |
|
||||
| `SdkErrorCode.ClientHttpFailedToTerminateSession` | Failed to terminate session |
|
||||
|
||||
#### `StreamableHTTPError` removed
|
||||
|
||||
The `StreamableHTTPError` class has been removed. HTTP transport errors are now thrown as `SdkError` with specific `SdkErrorCode` values that provide more granular error information:
|
||||
|
||||
**Before (v1):**
|
||||
|
||||
```typescript
|
||||
import { StreamableHTTPError } from '@modelcontextprotocol/sdk/client/streamableHttp.js';
|
||||
|
||||
try {
|
||||
await transport.send(message);
|
||||
} catch (error) {
|
||||
if (error instanceof StreamableHTTPError) {
|
||||
console.log('HTTP error:', error.code); // HTTP status code
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
**After (v2):**
|
||||
|
||||
```typescript
|
||||
import { SdkError, SdkErrorCode } from '@modelcontextprotocol/core';
|
||||
|
||||
try {
|
||||
await transport.send(message);
|
||||
} catch (error) {
|
||||
if (error instanceof SdkError) {
|
||||
switch (error.code) {
|
||||
case SdkErrorCode.ClientHttpAuthentication:
|
||||
console.log('Auth failed after completing auth flow');
|
||||
break;
|
||||
case SdkErrorCode.ClientHttpForbidden:
|
||||
console.log('Forbidden after upscoping attempt');
|
||||
break;
|
||||
case SdkErrorCode.ClientHttpFailedToOpenStream:
|
||||
console.log('Failed to open SSE stream');
|
||||
break;
|
||||
case SdkErrorCode.ClientHttpNotImplemented:
|
||||
console.log('HTTP request failed');
|
||||
break;
|
||||
}
|
||||
// Access HTTP status code from error.data if needed
|
||||
const httpStatus = (error.data as { status?: number })?.status;
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
#### Why this change?
|
||||
|
||||
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
|
||||
semantically inconsistent.
|
||||
|
||||
The new design:
|
||||
|
||||
- `ProtocolError` with `ProtocolErrorCode`: For errors that are serialized and sent as JSON-RPC error responses
|
||||
- `SdkError` with `SdkErrorCode`: For local errors that are thrown/rejected locally and never leave the SDK
|
||||
|
||||
### OAuth error refactoring
|
||||
|
||||
The OAuth error classes have been consolidated into a single `OAuthError` class with an `OAuthErrorCode` enum.
|
||||
|
||||
#### Removed classes
|
||||
|
||||
The following individual error classes have been removed in favor of `OAuthError` with the appropriate code:
|
||||
|
||||
| v1 Class | v2 Equivalent |
|
||||
| ------------------------------ | ----------------------------------------------------------------- |
|
||||
| `InvalidRequestError` | `new OAuthError(OAuthErrorCode.InvalidRequest, message)` |
|
||||
| `InvalidClientError` | `new OAuthError(OAuthErrorCode.InvalidClient, message)` |
|
||||
| `InvalidGrantError` | `new OAuthError(OAuthErrorCode.InvalidGrant, message)` |
|
||||
| `UnauthorizedClientError` | `new OAuthError(OAuthErrorCode.UnauthorizedClient, message)` |
|
||||
| `UnsupportedGrantTypeError` | `new OAuthError(OAuthErrorCode.UnsupportedGrantType, message)` |
|
||||
| `InvalidScopeError` | `new OAuthError(OAuthErrorCode.InvalidScope, message)` |
|
||||
| `AccessDeniedError` | `new OAuthError(OAuthErrorCode.AccessDenied, message)` |
|
||||
| `ServerError` | `new OAuthError(OAuthErrorCode.ServerError, message)` |
|
||||
| `TemporarilyUnavailableError` | `new OAuthError(OAuthErrorCode.TemporarilyUnavailable, message)` |
|
||||
| `UnsupportedResponseTypeError` | `new OAuthError(OAuthErrorCode.UnsupportedResponseType, message)` |
|
||||
| `UnsupportedTokenTypeError` | `new OAuthError(OAuthErrorCode.UnsupportedTokenType, message)` |
|
||||
| `InvalidTokenError` | `new OAuthError(OAuthErrorCode.InvalidToken, message)` |
|
||||
| `MethodNotAllowedError` | `new OAuthError(OAuthErrorCode.MethodNotAllowed, message)` |
|
||||
| `TooManyRequestsError` | `new OAuthError(OAuthErrorCode.TooManyRequests, message)` |
|
||||
| `InvalidClientMetadataError` | `new OAuthError(OAuthErrorCode.InvalidClientMetadata, message)` |
|
||||
| `InsufficientScopeError` | `new OAuthError(OAuthErrorCode.InsufficientScope, message)` |
|
||||
| `InvalidTargetError` | `new OAuthError(OAuthErrorCode.InvalidTarget, message)` |
|
||||
| `CustomOAuthError` | `new OAuthError(customCode, message)` |
|
||||
|
||||
The `OAUTH_ERRORS` constant has also been removed.
|
||||
|
||||
**Before (v1):**
|
||||
|
||||
```typescript
|
||||
import { InvalidClientError, InvalidGrantError, ServerError } from '@modelcontextprotocol/core';
|
||||
|
||||
try {
|
||||
await refreshToken();
|
||||
} catch (error) {
|
||||
if (error instanceof InvalidClientError) {
|
||||
// Handle invalid client
|
||||
} else if (error instanceof InvalidGrantError) {
|
||||
// Handle invalid grant
|
||||
} else if (error instanceof ServerError) {
|
||||
// Handle server error
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
**After (v2):**
|
||||
|
||||
```typescript
|
||||
import { OAuthError, OAuthErrorCode } from '@modelcontextprotocol/core';
|
||||
|
||||
try {
|
||||
await refreshToken();
|
||||
} catch (error) {
|
||||
if (error instanceof OAuthError) {
|
||||
switch (error.code) {
|
||||
case OAuthErrorCode.InvalidClient:
|
||||
// Handle invalid client
|
||||
break;
|
||||
case OAuthErrorCode.InvalidGrant:
|
||||
// Handle invalid grant
|
||||
break;
|
||||
case OAuthErrorCode.ServerError:
|
||||
// Handle server error
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## Unchanged APIs
|
||||
|
||||
The following APIs are unchanged between v1 and v2 (only the import paths changed):
|
||||
|
|
@ -352,7 +560,8 @@ The following APIs are unchanged between v1 and v2 (only the import paths change
|
|||
|
||||
## Using an LLM to migrate your code
|
||||
|
||||
An LLM-optimized version of this guide is available at [`docs/migration-SKILL.md`](migration-SKILL.md). It contains dense mapping tables designed for tools like Claude Code to mechanically apply all the changes described above. You can paste it into your LLM context or load it as a skill.
|
||||
An LLM-optimized version of this guide is available at [`docs/migration-SKILL.md`](migration-SKILL.md). It contains dense mapping tables designed for tools like Claude Code to mechanically apply all the changes described above. You can paste it into your LLM context or load it as
|
||||
a skill.
|
||||
|
||||
## Need Help?
|
||||
|
||||
|
|
|
|||
|
|
@ -21,10 +21,10 @@ import type {
|
|||
import {
|
||||
CallToolResultSchema,
|
||||
Client,
|
||||
ErrorCode,
|
||||
getDisplayName,
|
||||
ListToolsResultSchema,
|
||||
McpError,
|
||||
ProtocolError,
|
||||
ProtocolErrorCode,
|
||||
StreamableHTTPClientTransport,
|
||||
UnauthorizedError,
|
||||
UrlElicitationRequiredError
|
||||
|
|
@ -337,7 +337,7 @@ async function handleElicitationRequest(request: ElicitRequest): Promise<ElicitR
|
|||
} else {
|
||||
// Should not happen because the client declares its capabilities to the server,
|
||||
// but being defensive is a good practice:
|
||||
throw new McpError(ErrorCode.InvalidParams, `Unsupported elicitation mode: ${mode}`);
|
||||
throw new ProtocolError(ProtocolErrorCode.InvalidParams, `Unsupported elicitation mode: ${mode}`);
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -12,13 +12,13 @@ import type {
|
|||
import {
|
||||
CallToolResultSchema,
|
||||
Client,
|
||||
ErrorCode,
|
||||
getDisplayName,
|
||||
GetPromptResultSchema,
|
||||
ListPromptsResultSchema,
|
||||
ListResourcesResultSchema,
|
||||
ListToolsResultSchema,
|
||||
McpError,
|
||||
ProtocolError,
|
||||
ProtocolErrorCode,
|
||||
ReadResourceResultSchema,
|
||||
RELATED_TASK_META_KEY,
|
||||
StreamableHTTPClientTransport
|
||||
|
|
@ -270,7 +270,7 @@ async function connect(url?: string): Promise<void> {
|
|||
// Set up elicitation request handler with proper validation
|
||||
client.setRequestHandler('elicitation/create', async request => {
|
||||
if (request.params.mode !== 'form') {
|
||||
throw new McpError(ErrorCode.InvalidParams, `Unsupported elicitation mode: ${request.params.mode}`);
|
||||
throw new ProtocolError(ProtocolErrorCode.InvalidParams, `Unsupported elicitation mode: ${request.params.mode}`);
|
||||
}
|
||||
console.log('\n🔔 Elicitation (form) Request Received:');
|
||||
console.log(`Message: ${request.params.message}`);
|
||||
|
|
|
|||
|
|
@ -10,7 +10,13 @@
|
|||
import { createInterface } from 'node:readline';
|
||||
|
||||
import type { CreateMessageRequest, CreateMessageResult, TextContent } from '@modelcontextprotocol/client';
|
||||
import { CallToolResultSchema, Client, ErrorCode, McpError, StreamableHTTPClientTransport } from '@modelcontextprotocol/client';
|
||||
import {
|
||||
CallToolResultSchema,
|
||||
Client,
|
||||
ProtocolError,
|
||||
ProtocolErrorCode,
|
||||
StreamableHTTPClientTransport
|
||||
} from '@modelcontextprotocol/client';
|
||||
|
||||
// Create readline interface for user input
|
||||
const readline = createInterface({
|
||||
|
|
@ -96,7 +102,7 @@ async function run(url: string): Promise<void> {
|
|||
// Set up elicitation request handler
|
||||
client.setRequestHandler('elicitation/create', async request => {
|
||||
if (request.params.mode && request.params.mode !== 'form') {
|
||||
throw new McpError(ErrorCode.InvalidParams, `Unsupported elicitation mode: ${request.params.mode}`);
|
||||
throw new ProtocolError(ProtocolErrorCode.InvalidParams, `Unsupported elicitation mode: ${request.params.mode}`);
|
||||
}
|
||||
return elicitationCallback(request.params);
|
||||
});
|
||||
|
|
|
|||
|
|
@ -11,21 +11,16 @@ import type {
|
|||
} from '@modelcontextprotocol/core';
|
||||
import {
|
||||
checkResourceAllowed,
|
||||
InvalidClientError,
|
||||
InvalidClientMetadataError,
|
||||
InvalidGrantError,
|
||||
LATEST_PROTOCOL_VERSION,
|
||||
OAUTH_ERRORS,
|
||||
OAuthClientInformationFullSchema,
|
||||
OAuthError,
|
||||
OAuthErrorCode,
|
||||
OAuthErrorResponseSchema,
|
||||
OAuthMetadataSchema,
|
||||
OAuthProtectedResourceMetadataSchema,
|
||||
OAuthTokensSchema,
|
||||
OpenIdProviderDiscoveryMetadataSchema,
|
||||
resourceUrlFromServerUrl,
|
||||
ServerError,
|
||||
UnauthorizedClientError
|
||||
resourceUrlFromServerUrl
|
||||
} from '@modelcontextprotocol/core';
|
||||
import pkceChallenge from 'pkce-challenge';
|
||||
|
||||
|
|
@ -328,7 +323,7 @@ function applyPublicAuth(clientId: string, params: URLSearchParams): void {
|
|||
* Parses an OAuth error response from a string or Response object.
|
||||
*
|
||||
* If the input is a standard OAuth2.0 error response, it will be parsed according to the spec
|
||||
* and an instance of the appropriate OAuthError subclass will be returned.
|
||||
* and an OAuthError will be returned with the appropriate error code.
|
||||
* If parsing fails, it falls back to a generic ServerError that includes
|
||||
* the response status (if available) and original content.
|
||||
*
|
||||
|
|
@ -341,13 +336,11 @@ export async function parseErrorResponse(input: Response | string): Promise<OAut
|
|||
|
||||
try {
|
||||
const result = OAuthErrorResponseSchema.parse(JSON.parse(body));
|
||||
const { error, error_description, error_uri } = result;
|
||||
const errorClass = OAUTH_ERRORS[error] || ServerError;
|
||||
return new errorClass(error_description || '', error_uri);
|
||||
return OAuthError.fromResponse(result);
|
||||
} catch (error) {
|
||||
// Not a valid OAuth error response, but try to inform the user of the raw data anyway
|
||||
const errorMessage = `${statusCode ? `HTTP ${statusCode}: ` : ''}Invalid OAuth error response: ${error}. Raw body: ${body}`;
|
||||
return new ServerError(errorMessage);
|
||||
return new OAuthError(OAuthErrorCode.ServerError, errorMessage);
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -371,12 +364,14 @@ export async function auth(
|
|||
return await authInternal(provider, options);
|
||||
} catch (error) {
|
||||
// Handle recoverable error types by invalidating credentials and retrying
|
||||
if (error instanceof InvalidClientError || error instanceof UnauthorizedClientError) {
|
||||
await provider.invalidateCredentials?.('all');
|
||||
return await authInternal(provider, options);
|
||||
} else if (error instanceof InvalidGrantError) {
|
||||
await provider.invalidateCredentials?.('tokens');
|
||||
return await authInternal(provider, options);
|
||||
if (error instanceof OAuthError) {
|
||||
if (error.code === OAuthErrorCode.InvalidClient || error.code === OAuthErrorCode.UnauthorizedClient) {
|
||||
await provider.invalidateCredentials?.('all');
|
||||
return await authInternal(provider, options);
|
||||
} else if (error.code === OAuthErrorCode.InvalidGrant) {
|
||||
await provider.invalidateCredentials?.('tokens');
|
||||
return await authInternal(provider, options);
|
||||
}
|
||||
}
|
||||
|
||||
// Throw otherwise
|
||||
|
|
@ -437,7 +432,8 @@ async function authInternal(
|
|||
const clientMetadataUrl = provider.clientMetadataUrl;
|
||||
|
||||
if (clientMetadataUrl && !isHttpsUrl(clientMetadataUrl)) {
|
||||
throw new InvalidClientMetadataError(
|
||||
throw new OAuthError(
|
||||
OAuthErrorCode.InvalidClientMetadata,
|
||||
`clientMetadataUrl must be a valid HTTPS URL with a non-root pathname, got: ${clientMetadataUrl}`
|
||||
);
|
||||
}
|
||||
|
|
@ -502,7 +498,7 @@ async function authInternal(
|
|||
return 'AUTHORIZED';
|
||||
} catch (error) {
|
||||
// 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.
|
||||
if (!(error instanceof OAuthError) || error instanceof ServerError) {
|
||||
if (!(error instanceof OAuthError) || error.code === OAuthErrorCode.ServerError) {
|
||||
// Could not refresh OAuth tokens
|
||||
} else {
|
||||
// Refresh failed for another reason, re-throw
|
||||
|
|
|
|||
|
|
@ -47,7 +47,6 @@ import {
|
|||
ElicitRequestSchema,
|
||||
ElicitResultSchema,
|
||||
EmptyResultSchema,
|
||||
ErrorCode,
|
||||
GetPromptResultSchema,
|
||||
InitializeResultSchema,
|
||||
LATEST_PROTOCOL_VERSION,
|
||||
|
|
@ -56,11 +55,14 @@ import {
|
|||
ListResourcesResultSchema,
|
||||
ListResourceTemplatesResultSchema,
|
||||
ListToolsResultSchema,
|
||||
McpError,
|
||||
mergeCapabilities,
|
||||
Protocol,
|
||||
ProtocolError,
|
||||
ProtocolErrorCode,
|
||||
ReadResourceResultSchema,
|
||||
safeParse
|
||||
safeParse,
|
||||
SdkError,
|
||||
SdkErrorCode
|
||||
} from '@modelcontextprotocol/core';
|
||||
|
||||
import { ExperimentalClientTasks } from '../experimental/tasks/client.js';
|
||||
|
|
@ -344,7 +346,7 @@ export class Client<
|
|||
// Type guard: if success is false, error is guaranteed to exist
|
||||
const errorMessage =
|
||||
validatedRequest.error instanceof Error ? validatedRequest.error.message : String(validatedRequest.error);
|
||||
throw new McpError(ErrorCode.InvalidParams, `Invalid elicitation request: ${errorMessage}`);
|
||||
throw new ProtocolError(ProtocolErrorCode.InvalidParams, `Invalid elicitation request: ${errorMessage}`);
|
||||
}
|
||||
|
||||
const { params } = validatedRequest.data;
|
||||
|
|
@ -352,11 +354,11 @@ export class Client<
|
|||
const { supportsFormMode, supportsUrlMode } = getSupportedElicitationModes(this._capabilities.elicitation);
|
||||
|
||||
if (params.mode === 'form' && !supportsFormMode) {
|
||||
throw new McpError(ErrorCode.InvalidParams, 'Client does not support form-mode elicitation requests');
|
||||
throw new ProtocolError(ProtocolErrorCode.InvalidParams, 'Client does not support form-mode elicitation requests');
|
||||
}
|
||||
|
||||
if (params.mode === 'url' && !supportsUrlMode) {
|
||||
throw new McpError(ErrorCode.InvalidParams, 'Client does not support URL-mode elicitation requests');
|
||||
throw new ProtocolError(ProtocolErrorCode.InvalidParams, 'Client does not support URL-mode elicitation requests');
|
||||
}
|
||||
|
||||
const result = await Promise.resolve(handler(request, extra));
|
||||
|
|
@ -369,7 +371,7 @@ export class Client<
|
|||
taskValidationResult.error instanceof Error
|
||||
? taskValidationResult.error.message
|
||||
: String(taskValidationResult.error);
|
||||
throw new McpError(ErrorCode.InvalidParams, `Invalid task creation result: ${errorMessage}`);
|
||||
throw new ProtocolError(ProtocolErrorCode.InvalidParams, `Invalid task creation result: ${errorMessage}`);
|
||||
}
|
||||
return taskValidationResult.data;
|
||||
}
|
||||
|
|
@ -380,7 +382,7 @@ export class Client<
|
|||
// Type guard: if success is false, error is guaranteed to exist
|
||||
const errorMessage =
|
||||
validationResult.error instanceof Error ? validationResult.error.message : String(validationResult.error);
|
||||
throw new McpError(ErrorCode.InvalidParams, `Invalid elicitation result: ${errorMessage}`);
|
||||
throw new ProtocolError(ProtocolErrorCode.InvalidParams, `Invalid elicitation result: ${errorMessage}`);
|
||||
}
|
||||
|
||||
const validatedResult = validationResult.data;
|
||||
|
|
@ -416,7 +418,7 @@ export class Client<
|
|||
if (!validatedRequest.success) {
|
||||
const errorMessage =
|
||||
validatedRequest.error instanceof Error ? validatedRequest.error.message : String(validatedRequest.error);
|
||||
throw new McpError(ErrorCode.InvalidParams, `Invalid sampling request: ${errorMessage}`);
|
||||
throw new ProtocolError(ProtocolErrorCode.InvalidParams, `Invalid sampling request: ${errorMessage}`);
|
||||
}
|
||||
|
||||
const { params } = validatedRequest.data;
|
||||
|
|
@ -431,7 +433,7 @@ export class Client<
|
|||
taskValidationResult.error instanceof Error
|
||||
? taskValidationResult.error.message
|
||||
: String(taskValidationResult.error);
|
||||
throw new McpError(ErrorCode.InvalidParams, `Invalid task creation result: ${errorMessage}`);
|
||||
throw new ProtocolError(ProtocolErrorCode.InvalidParams, `Invalid task creation result: ${errorMessage}`);
|
||||
}
|
||||
return taskValidationResult.data;
|
||||
}
|
||||
|
|
@ -443,7 +445,7 @@ export class Client<
|
|||
if (!validationResult.success) {
|
||||
const errorMessage =
|
||||
validationResult.error instanceof Error ? validationResult.error.message : String(validationResult.error);
|
||||
throw new McpError(ErrorCode.InvalidParams, `Invalid sampling result: ${errorMessage}`);
|
||||
throw new ProtocolError(ProtocolErrorCode.InvalidParams, `Invalid sampling result: ${errorMessage}`);
|
||||
}
|
||||
|
||||
return validationResult.data;
|
||||
|
|
@ -542,7 +544,7 @@ export class Client<
|
|||
switch (method as ClientRequest['method']) {
|
||||
case 'logging/setLevel': {
|
||||
if (!this._serverCapabilities?.logging) {
|
||||
throw new Error(`Server does not support logging (required for ${method})`);
|
||||
throw new SdkError(SdkErrorCode.CapabilityNotSupported, `Server does not support logging (required for ${method})`);
|
||||
}
|
||||
break;
|
||||
}
|
||||
|
|
@ -550,7 +552,7 @@ export class Client<
|
|||
case 'prompts/get':
|
||||
case 'prompts/list': {
|
||||
if (!this._serverCapabilities?.prompts) {
|
||||
throw new Error(`Server does not support prompts (required for ${method})`);
|
||||
throw new SdkError(SdkErrorCode.CapabilityNotSupported, `Server does not support prompts (required for ${method})`);
|
||||
}
|
||||
break;
|
||||
}
|
||||
|
|
@ -561,11 +563,14 @@ export class Client<
|
|||
case 'resources/subscribe':
|
||||
case 'resources/unsubscribe': {
|
||||
if (!this._serverCapabilities?.resources) {
|
||||
throw new Error(`Server does not support resources (required for ${method})`);
|
||||
throw new SdkError(SdkErrorCode.CapabilityNotSupported, `Server does not support resources (required for ${method})`);
|
||||
}
|
||||
|
||||
if (method === 'resources/subscribe' && !this._serverCapabilities.resources.subscribe) {
|
||||
throw new Error(`Server does not support resource subscriptions (required for ${method})`);
|
||||
throw new SdkError(
|
||||
SdkErrorCode.CapabilityNotSupported,
|
||||
`Server does not support resource subscriptions (required for ${method})`
|
||||
);
|
||||
}
|
||||
|
||||
break;
|
||||
|
|
@ -574,14 +579,14 @@ export class Client<
|
|||
case 'tools/call':
|
||||
case 'tools/list': {
|
||||
if (!this._serverCapabilities?.tools) {
|
||||
throw new Error(`Server does not support tools (required for ${method})`);
|
||||
throw new SdkError(SdkErrorCode.CapabilityNotSupported, `Server does not support tools (required for ${method})`);
|
||||
}
|
||||
break;
|
||||
}
|
||||
|
||||
case 'completion/complete': {
|
||||
if (!this._serverCapabilities?.completions) {
|
||||
throw new Error(`Server does not support completions (required for ${method})`);
|
||||
throw new SdkError(SdkErrorCode.CapabilityNotSupported, `Server does not support completions (required for ${method})`);
|
||||
}
|
||||
break;
|
||||
}
|
||||
|
|
@ -602,7 +607,10 @@ export class Client<
|
|||
switch (method as ClientNotification['method']) {
|
||||
case 'notifications/roots/list_changed': {
|
||||
if (!this._capabilities.roots?.listChanged) {
|
||||
throw new Error(`Client does not support roots list changed notifications (required for ${method})`);
|
||||
throw new SdkError(
|
||||
SdkErrorCode.CapabilityNotSupported,
|
||||
`Client does not support roots list changed notifications (required for ${method})`
|
||||
);
|
||||
}
|
||||
break;
|
||||
}
|
||||
|
|
@ -634,21 +642,30 @@ export class Client<
|
|||
switch (method) {
|
||||
case 'sampling/createMessage': {
|
||||
if (!this._capabilities.sampling) {
|
||||
throw new Error(`Client does not support sampling capability (required for ${method})`);
|
||||
throw new SdkError(
|
||||
SdkErrorCode.CapabilityNotSupported,
|
||||
`Client does not support sampling capability (required for ${method})`
|
||||
);
|
||||
}
|
||||
break;
|
||||
}
|
||||
|
||||
case 'elicitation/create': {
|
||||
if (!this._capabilities.elicitation) {
|
||||
throw new Error(`Client does not support elicitation capability (required for ${method})`);
|
||||
throw new SdkError(
|
||||
SdkErrorCode.CapabilityNotSupported,
|
||||
`Client does not support elicitation capability (required for ${method})`
|
||||
);
|
||||
}
|
||||
break;
|
||||
}
|
||||
|
||||
case 'roots/list': {
|
||||
if (!this._capabilities.roots) {
|
||||
throw new Error(`Client does not support roots capability (required for ${method})`);
|
||||
throw new SdkError(
|
||||
SdkErrorCode.CapabilityNotSupported,
|
||||
`Client does not support roots capability (required for ${method})`
|
||||
);
|
||||
}
|
||||
break;
|
||||
}
|
||||
|
|
@ -658,7 +675,10 @@ export class Client<
|
|||
case 'tasks/result':
|
||||
case 'tasks/cancel': {
|
||||
if (!this._capabilities.tasks) {
|
||||
throw new Error(`Client does not support tasks capability (required for ${method})`);
|
||||
throw new SdkError(
|
||||
SdkErrorCode.CapabilityNotSupported,
|
||||
`Client does not support tasks capability (required for ${method})`
|
||||
);
|
||||
}
|
||||
break;
|
||||
}
|
||||
|
|
@ -753,8 +773,8 @@ export class Client<
|
|||
) {
|
||||
// Guard: required-task tools need experimental API
|
||||
if (this.isToolTaskRequired(params.name)) {
|
||||
throw new McpError(
|
||||
ErrorCode.InvalidRequest,
|
||||
throw new ProtocolError(
|
||||
ProtocolErrorCode.InvalidRequest,
|
||||
`Tool "${params.name}" requires task-based execution. Use client.experimental.tasks.callToolStream() instead.`
|
||||
);
|
||||
}
|
||||
|
|
@ -766,8 +786,8 @@ export class Client<
|
|||
if (validator) {
|
||||
// If tool has outputSchema, it MUST return structuredContent (unless it's an error)
|
||||
if (!result.structuredContent && !result.isError) {
|
||||
throw new McpError(
|
||||
ErrorCode.InvalidRequest,
|
||||
throw new ProtocolError(
|
||||
ProtocolErrorCode.InvalidRequest,
|
||||
`Tool ${params.name} has an output schema but did not return structured content`
|
||||
);
|
||||
}
|
||||
|
|
@ -779,17 +799,17 @@ export class Client<
|
|||
const validationResult = validator(result.structuredContent);
|
||||
|
||||
if (!validationResult.valid) {
|
||||
throw new McpError(
|
||||
ErrorCode.InvalidParams,
|
||||
throw new ProtocolError(
|
||||
ProtocolErrorCode.InvalidParams,
|
||||
`Structured content does not match the tool's output schema: ${validationResult.errorMessage}`
|
||||
);
|
||||
}
|
||||
} catch (error) {
|
||||
if (error instanceof McpError) {
|
||||
if (error instanceof ProtocolError) {
|
||||
throw error;
|
||||
}
|
||||
throw new McpError(
|
||||
ErrorCode.InvalidParams,
|
||||
throw new ProtocolError(
|
||||
ProtocolErrorCode.InvalidParams,
|
||||
`Failed to validate structured content: ${error instanceof Error ? error.message : String(error)}`
|
||||
);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -16,7 +16,7 @@ export type Middleware = (next: FetchLike) => FetchLike;
|
|||
* - Add Authorization headers with access tokens
|
||||
* - Handle 401 responses by attempting re-authentication
|
||||
* - Retry the original request after successful auth
|
||||
* - Handle OAuth errors appropriately (InvalidClientError, etc.)
|
||||
* - Handle OAuth errors appropriately (OAuthErrorCode.InvalidClient, etc.)
|
||||
*
|
||||
* The baseUrl parameter is optional and defaults to using the domain from the request URL.
|
||||
* However, you should explicitly provide baseUrl when:
|
||||
|
|
|
|||
|
|
@ -1,5 +1,5 @@
|
|||
import type { FetchLike, JSONRPCMessage, Transport } from '@modelcontextprotocol/core';
|
||||
import { createFetchWithInit, JSONRPCMessageSchema, normalizeHeaders } from '@modelcontextprotocol/core';
|
||||
import { createFetchWithInit, JSONRPCMessageSchema, normalizeHeaders, SdkError, SdkErrorCode } from '@modelcontextprotocol/core';
|
||||
import type { ErrorEvent, EventSourceInit } from 'eventsource';
|
||||
import { EventSource } from 'eventsource';
|
||||
|
||||
|
|
@ -245,7 +245,7 @@ export class SSEClientTransport implements Transport {
|
|||
|
||||
async send(message: JSONRPCMessage): Promise<void> {
|
||||
if (!this._endpoint) {
|
||||
throw new Error('Not connected');
|
||||
throw new SdkError(SdkErrorCode.NotConnected, 'Not connected');
|
||||
}
|
||||
|
||||
try {
|
||||
|
|
|
|||
|
|
@ -4,7 +4,7 @@ import type { Stream } from 'node:stream';
|
|||
import { PassThrough } from 'node:stream';
|
||||
|
||||
import type { JSONRPCMessage, Transport } from '@modelcontextprotocol/core';
|
||||
import { ReadBuffer, serializeMessage } from '@modelcontextprotocol/core';
|
||||
import { ReadBuffer, SdkError, SdkErrorCode, serializeMessage } from '@modelcontextprotocol/core';
|
||||
import spawn from 'cross-spawn';
|
||||
|
||||
export type StdioServerParameters = {
|
||||
|
|
@ -246,7 +246,7 @@ export class StdioClientTransport implements Transport {
|
|||
send(message: JSONRPCMessage): Promise<void> {
|
||||
return new Promise(resolve => {
|
||||
if (!this._process?.stdin) {
|
||||
throw new Error('Not connected');
|
||||
throw new SdkError(SdkErrorCode.NotConnected, 'Not connected');
|
||||
}
|
||||
|
||||
const json = serializeMessage(message);
|
||||
|
|
|
|||
|
|
@ -7,7 +7,9 @@ import {
|
|||
isJSONRPCRequest,
|
||||
isJSONRPCResultResponse,
|
||||
JSONRPCMessageSchema,
|
||||
normalizeHeaders
|
||||
normalizeHeaders,
|
||||
SdkError,
|
||||
SdkErrorCode
|
||||
} from '@modelcontextprotocol/core';
|
||||
import { EventSourceParserStream } from 'eventsource-parser/stream';
|
||||
|
||||
|
|
@ -22,15 +24,6 @@ const DEFAULT_STREAMABLE_HTTP_RECONNECTION_OPTIONS: StreamableHTTPReconnectionOp
|
|||
maxRetries: 2
|
||||
};
|
||||
|
||||
export class StreamableHTTPError extends Error {
|
||||
constructor(
|
||||
public readonly code: number | undefined,
|
||||
message: string | undefined
|
||||
) {
|
||||
super(`Streamable HTTP error: ${message}`);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Options for starting or authenticating an SSE connection
|
||||
*/
|
||||
|
|
@ -248,7 +241,10 @@ export class StreamableHTTPClientTransport implements Transport {
|
|||
return;
|
||||
}
|
||||
|
||||
throw new StreamableHTTPError(response.status, `Failed to open SSE stream: ${response.statusText}`);
|
||||
throw new SdkError(SdkErrorCode.ClientHttpFailedToOpenStream, `Failed to open SSE stream: ${response.statusText}`, {
|
||||
status: response.status,
|
||||
statusText: response.statusText
|
||||
});
|
||||
}
|
||||
|
||||
this._handleSseStream(response.body, options, true);
|
||||
|
|
@ -500,7 +496,10 @@ export class StreamableHTTPClientTransport implements Transport {
|
|||
if (response.status === 401 && this._authProvider) {
|
||||
// Prevent infinite recursion when server returns 401 after successful auth
|
||||
if (this._hasCompletedAuthFlow) {
|
||||
throw new StreamableHTTPError(401, 'Server returned 401 after successful authentication');
|
||||
throw new SdkError(SdkErrorCode.ClientHttpAuthentication, 'Server returned 401 after successful authentication', {
|
||||
status: 401,
|
||||
text
|
||||
});
|
||||
}
|
||||
|
||||
const { resourceMetadataUrl, scope } = extractWWWAuthenticateParams(response);
|
||||
|
|
@ -531,7 +530,10 @@ 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 StreamableHTTPError(403, 'Server returned 403 after trying upscoping');
|
||||
throw new SdkError(SdkErrorCode.ClientHttpForbidden, 'Server returned 403 after trying upscoping', {
|
||||
status: 403,
|
||||
text
|
||||
});
|
||||
}
|
||||
|
||||
if (scope) {
|
||||
|
|
@ -559,7 +561,10 @@ export class StreamableHTTPClientTransport implements Transport {
|
|||
}
|
||||
}
|
||||
|
||||
throw new StreamableHTTPError(response.status, `Error POSTing to endpoint: ${text}`);
|
||||
throw new SdkError(SdkErrorCode.ClientHttpNotImplemented, `Error POSTing to endpoint: ${text}`, {
|
||||
status: response.status,
|
||||
text
|
||||
});
|
||||
}
|
||||
|
||||
// Reset auth loop flag on successful response
|
||||
|
|
@ -604,7 +609,9 @@ export class StreamableHTTPClientTransport implements Transport {
|
|||
}
|
||||
} else {
|
||||
await response.text?.().catch(() => {});
|
||||
throw new StreamableHTTPError(-1, `Unexpected content type: ${contentType}`);
|
||||
throw new SdkError(SdkErrorCode.ClientHttpUnexpectedContent, `Unexpected content type: ${contentType}`, {
|
||||
contentType
|
||||
});
|
||||
}
|
||||
} else {
|
||||
// No requests in message but got 200 OK - still need to release connection
|
||||
|
|
@ -652,7 +659,10 @@ 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 StreamableHTTPError(response.status, `Failed to terminate session: ${response.statusText}`);
|
||||
throw new SdkError(SdkErrorCode.ClientHttpFailedToTerminateSession, `Failed to terminate session: ${response.statusText}`, {
|
||||
status: response.status,
|
||||
statusText: response.statusText
|
||||
});
|
||||
}
|
||||
|
||||
this._sessionId = undefined;
|
||||
|
|
|
|||
|
|
@ -20,7 +20,7 @@ import type {
|
|||
Result,
|
||||
SchemaOutput
|
||||
} from '@modelcontextprotocol/core';
|
||||
import { CallToolResultSchema, ErrorCode, McpError } from '@modelcontextprotocol/core';
|
||||
import { CallToolResultSchema, ProtocolError, ProtocolErrorCode } from '@modelcontextprotocol/core';
|
||||
|
||||
import type { Client } from '../../client/client.js';
|
||||
|
||||
|
|
@ -123,8 +123,8 @@ export class ExperimentalClientTasks<
|
|||
if (!result.structuredContent && !result.isError) {
|
||||
yield {
|
||||
type: 'error',
|
||||
error: new McpError(
|
||||
ErrorCode.InvalidRequest,
|
||||
error: new ProtocolError(
|
||||
ProtocolErrorCode.InvalidRequest,
|
||||
`Tool ${params.name} has an output schema but did not return structured content`
|
||||
)
|
||||
};
|
||||
|
|
@ -140,22 +140,22 @@ export class ExperimentalClientTasks<
|
|||
if (!validationResult.valid) {
|
||||
yield {
|
||||
type: 'error',
|
||||
error: new McpError(
|
||||
ErrorCode.InvalidParams,
|
||||
error: new ProtocolError(
|
||||
ProtocolErrorCode.InvalidParams,
|
||||
`Structured content does not match the tool's output schema: ${validationResult.errorMessage}`
|
||||
)
|
||||
};
|
||||
return;
|
||||
}
|
||||
} catch (error) {
|
||||
if (error instanceof McpError) {
|
||||
if (error instanceof ProtocolError) {
|
||||
yield { type: 'error', error };
|
||||
return;
|
||||
}
|
||||
yield {
|
||||
type: 'error',
|
||||
error: new McpError(
|
||||
ErrorCode.InvalidParams,
|
||||
error: new ProtocolError(
|
||||
ProtocolErrorCode.InvalidParams,
|
||||
`Failed to validate structured content: ${error instanceof Error ? error.message : String(error)}`
|
||||
)
|
||||
};
|
||||
|
|
|
|||
|
|
@ -1,7 +1,9 @@
|
|||
import type { AuthorizationServerMetadata, OAuthTokens } from '@modelcontextprotocol/core';
|
||||
import { InvalidClientMetadataError, LATEST_PROTOCOL_VERSION, ServerError } from '@modelcontextprotocol/core';
|
||||
import { expect, type Mock, vi } from 'vitest';
|
||||
import { LATEST_PROTOCOL_VERSION, OAuthError, OAuthErrorCode } from '@modelcontextprotocol/core';
|
||||
import type { Mock } from 'vitest';
|
||||
import { expect, vi } from 'vitest';
|
||||
|
||||
import type { OAuthClientProvider } from '../../src/client/auth.js';
|
||||
import {
|
||||
auth,
|
||||
buildDiscoveryUrls,
|
||||
|
|
@ -11,7 +13,6 @@ import {
|
|||
exchangeAuthorization,
|
||||
extractWWWAuthenticateParams,
|
||||
isHttpsUrl,
|
||||
type OAuthClientProvider,
|
||||
refreshAuthorization,
|
||||
registerClient,
|
||||
selectClientAuthMethod,
|
||||
|
|
@ -29,7 +30,7 @@ vi.mock('pkce-challenge', () => ({
|
|||
|
||||
// Mock fetch globally
|
||||
const mockFetch = vi.fn();
|
||||
global.fetch = mockFetch;
|
||||
globalThis.fetch = mockFetch;
|
||||
|
||||
describe('OAuth Authorization', () => {
|
||||
beforeEach(() => {
|
||||
|
|
@ -137,18 +138,13 @@ describe('OAuth Authorization', () => {
|
|||
mockFetch.mockImplementation((_url, _options) => {
|
||||
callCount++;
|
||||
|
||||
if (callCount === 1) {
|
||||
// First call with MCP header - fail with TypeError (simulating CORS error)
|
||||
// We need to use TypeError specifically because that's what the implementation checks for
|
||||
return Promise.reject(new TypeError('Network error'));
|
||||
} else {
|
||||
// Second call without header - succeed
|
||||
return Promise.resolve({
|
||||
ok: true,
|
||||
status: 200,
|
||||
json: async () => validMetadata
|
||||
});
|
||||
}
|
||||
return callCount === 1
|
||||
? Promise.reject(new TypeError('Network error'))
|
||||
: Promise.resolve({
|
||||
ok: true,
|
||||
status: 200,
|
||||
json: async () => validMetadata
|
||||
});
|
||||
});
|
||||
|
||||
// Should succeed with the second call
|
||||
|
|
@ -170,13 +166,7 @@ describe('OAuth Authorization', () => {
|
|||
mockFetch.mockImplementation((_url, _options) => {
|
||||
callCount++;
|
||||
|
||||
if (callCount === 1) {
|
||||
// First call - fail with TypeError
|
||||
return Promise.reject(new TypeError('First failure'));
|
||||
} else {
|
||||
// Second call - fail with different error
|
||||
return Promise.reject(new Error('Second failure'));
|
||||
}
|
||||
return callCount === 1 ? Promise.reject(new TypeError('First failure')) : Promise.reject(new Error('Second failure'));
|
||||
});
|
||||
|
||||
// Should fail with the second error
|
||||
|
|
@ -608,18 +598,13 @@ describe('OAuth Authorization', () => {
|
|||
mockFetch.mockImplementation((_url, _options) => {
|
||||
callCount++;
|
||||
|
||||
if (callCount === 1) {
|
||||
// First call with MCP header - fail with TypeError (simulating CORS error)
|
||||
// We need to use TypeError specifically because that's what the implementation checks for
|
||||
return Promise.reject(new TypeError('Network error'));
|
||||
} else {
|
||||
// Second call without header - succeed
|
||||
return Promise.resolve({
|
||||
ok: true,
|
||||
status: 200,
|
||||
json: async () => validMetadata
|
||||
});
|
||||
}
|
||||
return callCount === 1
|
||||
? Promise.reject(new TypeError('Network error'))
|
||||
: Promise.resolve({
|
||||
ok: true,
|
||||
status: 200,
|
||||
json: async () => validMetadata
|
||||
});
|
||||
});
|
||||
|
||||
// Should succeed with the second call
|
||||
|
|
@ -641,13 +626,7 @@ describe('OAuth Authorization', () => {
|
|||
mockFetch.mockImplementation((_url, _options) => {
|
||||
callCount++;
|
||||
|
||||
if (callCount === 1) {
|
||||
// First call - fail with TypeError
|
||||
return Promise.reject(new TypeError('First failure'));
|
||||
} else {
|
||||
// Second call - fail with different error
|
||||
return Promise.reject(new Error('Second failure'));
|
||||
}
|
||||
return callCount === 1 ? Promise.reject(new TypeError('First failure')) : Promise.reject(new Error('Second failure'));
|
||||
});
|
||||
|
||||
// Should fail with the second error
|
||||
|
|
@ -1265,7 +1244,9 @@ describe('OAuth Authorization', () => {
|
|||
});
|
||||
|
||||
it('throws on error response', async () => {
|
||||
mockFetch.mockResolvedValueOnce(Response.json(new ServerError('Token exchange failed').toResponseObject(), { status: 400 }));
|
||||
mockFetch.mockResolvedValueOnce(
|
||||
Response.json(new OAuthError(OAuthErrorCode.ServerError, 'Token exchange failed').toResponseObject(), { status: 400 })
|
||||
);
|
||||
|
||||
await expect(
|
||||
exchangeAuthorization('https://auth.example.com', {
|
||||
|
|
@ -1458,7 +1439,9 @@ describe('OAuth Authorization', () => {
|
|||
});
|
||||
|
||||
it('throws on error response', async () => {
|
||||
mockFetch.mockResolvedValueOnce(Response.json(new ServerError('Token refresh failed').toResponseObject(), { status: 400 }));
|
||||
mockFetch.mockResolvedValueOnce(
|
||||
Response.json(new OAuthError(OAuthErrorCode.ServerError, 'Token refresh failed').toResponseObject(), { status: 400 })
|
||||
);
|
||||
|
||||
await expect(
|
||||
refreshAuthorization('https://auth.example.com', {
|
||||
|
|
@ -1478,8 +1461,8 @@ describe('OAuth Authorization', () => {
|
|||
const validClientInfo = {
|
||||
client_id: 'client123',
|
||||
client_secret: 'secret123',
|
||||
client_id_issued_at: 1612137600,
|
||||
client_secret_expires_at: 1612224000,
|
||||
client_id_issued_at: 1_612_137_600,
|
||||
client_secret_expires_at: 1_612_224_000,
|
||||
...validClientMetadata
|
||||
};
|
||||
|
||||
|
|
@ -1544,7 +1527,9 @@ describe('OAuth Authorization', () => {
|
|||
|
||||
it('throws on error response', async () => {
|
||||
mockFetch.mockResolvedValueOnce(
|
||||
Response.json(new ServerError('Dynamic client registration failed').toResponseObject(), { status: 400 })
|
||||
Response.json(new OAuthError(OAuthErrorCode.ServerError, 'Dynamic client registration failed').toResponseObject(), {
|
||||
status: 400
|
||||
})
|
||||
);
|
||||
|
||||
await expect(
|
||||
|
|
@ -1627,6 +1612,7 @@ describe('OAuth Authorization', () => {
|
|||
// redirectUrl returns undefined to indicate non-interactive flow
|
||||
const ccProvider: OAuthClientProvider = {
|
||||
get redirectUrl() {
|
||||
// eslint-disable-next-line unicorn/no-useless-undefined
|
||||
return undefined;
|
||||
},
|
||||
get clientMetadata() {
|
||||
|
|
@ -1712,8 +1698,8 @@ describe('OAuth Authorization', () => {
|
|||
json: async () => ({
|
||||
client_id: 'test-client-id',
|
||||
client_secret: 'test-client-secret',
|
||||
client_id_issued_at: 1612137600,
|
||||
client_secret_expires_at: 1612224000,
|
||||
client_id_issued_at: 1_612_137_600,
|
||||
client_secret_expires_at: 1_612_224_000,
|
||||
redirect_uris: ['http://localhost:3000/callback'],
|
||||
client_name: 'Test Client'
|
||||
})
|
||||
|
|
@ -1784,8 +1770,8 @@ describe('OAuth Authorization', () => {
|
|||
json: async () => ({
|
||||
client_id: 'test-client-id',
|
||||
client_secret: 'test-client-secret',
|
||||
client_id_issued_at: 1612137600,
|
||||
client_secret_expires_at: 1612224000,
|
||||
client_id_issued_at: 1_612_137_600,
|
||||
client_secret_expires_at: 1_612_224_000,
|
||||
redirect_uris: ['http://localhost:3000/callback'],
|
||||
client_name: 'Test Client'
|
||||
})
|
||||
|
|
@ -3120,7 +3106,7 @@ describe('OAuth Authorization', () => {
|
|||
auth(providerWithInvalidUri, {
|
||||
serverUrl: 'https://server.example.com'
|
||||
})
|
||||
).rejects.toThrow(InvalidClientMetadataError);
|
||||
).rejects.toMatchObject({ code: OAuthErrorCode.InvalidClientMetadata });
|
||||
});
|
||||
|
||||
it('throws an error when clientMetadataUrl has root pathname', async () => {
|
||||
|
|
@ -3155,7 +3141,7 @@ describe('OAuth Authorization', () => {
|
|||
auth(providerWithRootPathname, {
|
||||
serverUrl: 'https://server.example.com'
|
||||
})
|
||||
).rejects.toThrow(InvalidClientMetadataError);
|
||||
).rejects.toMatchObject({ code: OAuthErrorCode.InvalidClientMetadata });
|
||||
});
|
||||
|
||||
it('throws an error when clientMetadataUrl is not a valid URL', async () => {
|
||||
|
|
@ -3190,7 +3176,7 @@ describe('OAuth Authorization', () => {
|
|||
auth(providerWithInvalidUrl, {
|
||||
serverUrl: 'https://server.example.com'
|
||||
})
|
||||
).rejects.toThrow(InvalidClientMetadataError);
|
||||
).rejects.toMatchObject({ code: OAuthErrorCode.InvalidClientMetadata });
|
||||
});
|
||||
|
||||
it('falls back to DCR when client_uri is missing', async () => {
|
||||
|
|
|
|||
|
|
@ -3,7 +3,7 @@ import { createServer } from 'node:http';
|
|||
import type { AddressInfo } from 'node:net';
|
||||
|
||||
import type { JSONRPCMessage, OAuthTokens } from '@modelcontextprotocol/core';
|
||||
import { InvalidClientError, InvalidGrantError, UnauthorizedClientError } from '@modelcontextprotocol/core';
|
||||
import { OAuthError, OAuthErrorCode } from '@modelcontextprotocol/core';
|
||||
import { listenOnRandomPort } from '@modelcontextprotocol/test-helpers';
|
||||
import type { Mock, Mocked, MockedFunction, MockInstance } from 'vitest';
|
||||
|
||||
|
|
@ -1001,7 +1001,7 @@ describe('SSEClientTransport', () => {
|
|||
expect(mockAuthProvider.redirectToAuthorization).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('invalidates all credentials on InvalidClientError during token refresh', async () => {
|
||||
it('invalidates all credentials on OAuthErrorCode.InvalidClient during token refresh', async () => {
|
||||
// Mock tokens() to return token with refresh token
|
||||
mockAuthProvider.tokens.mockResolvedValue({
|
||||
access_token: 'expired-token',
|
||||
|
|
@ -1009,9 +1009,10 @@ describe('SSEClientTransport', () => {
|
|||
refresh_token: 'refresh-token'
|
||||
});
|
||||
|
||||
const expectedError = new OAuthError(OAuthErrorCode.InvalidClient, 'Client authentication failed');
|
||||
let baseUrl = resourceBaseUrl;
|
||||
|
||||
// Create server that returns InvalidClientError on token refresh
|
||||
// Create server that returns OAuthErrorCode.InvalidClient on token refresh
|
||||
const server = createServer((req, res) => {
|
||||
lastServerRequest = req;
|
||||
|
||||
|
|
@ -1031,9 +1032,7 @@ describe('SSEClientTransport', () => {
|
|||
}
|
||||
|
||||
if (req.url === '/token' && req.method === 'POST') {
|
||||
// Handle token refresh request - return InvalidClientError
|
||||
const error = new InvalidClientError('Client authentication failed');
|
||||
res.writeHead(400, { 'Content-Type': 'application/json' }).end(JSON.stringify(error.toResponseObject()));
|
||||
res.writeHead(400, { 'Content-Type': 'application/json' }).end(JSON.stringify(expectedError.toResponseObject()));
|
||||
return;
|
||||
}
|
||||
|
||||
|
|
@ -1050,11 +1049,11 @@ describe('SSEClientTransport', () => {
|
|||
authProvider: mockAuthProvider
|
||||
});
|
||||
|
||||
await expect(() => transport.start()).rejects.toThrow(InvalidClientError);
|
||||
await expect(() => transport.start()).rejects.toMatchObject(expectedError);
|
||||
expect(mockAuthProvider.invalidateCredentials).toHaveBeenCalledWith('all');
|
||||
});
|
||||
|
||||
it('invalidates all credentials on UnauthorizedClientError during token refresh', async () => {
|
||||
it('invalidates all credentials on OAuthErrorCode.UnauthorizedClient during token refresh', async () => {
|
||||
// Mock tokens() to return token with refresh token
|
||||
mockAuthProvider.tokens.mockResolvedValue({
|
||||
access_token: 'expired-token',
|
||||
|
|
@ -1062,6 +1061,7 @@ describe('SSEClientTransport', () => {
|
|||
refresh_token: 'refresh-token'
|
||||
});
|
||||
|
||||
const expectedError = new OAuthError(OAuthErrorCode.UnauthorizedClient, 'Client not authorized');
|
||||
let baseUrl = resourceBaseUrl;
|
||||
|
||||
const server = createServer((req, res) => {
|
||||
|
|
@ -1083,9 +1083,7 @@ describe('SSEClientTransport', () => {
|
|||
}
|
||||
|
||||
if (req.url === '/token' && req.method === 'POST') {
|
||||
// Handle token refresh request - return UnauthorizedClientError
|
||||
const error = new UnauthorizedClientError('Client not authorized');
|
||||
res.writeHead(400, { 'Content-Type': 'application/json' }).end(JSON.stringify(error.toResponseObject()));
|
||||
res.writeHead(400, { 'Content-Type': 'application/json' }).end(JSON.stringify(expectedError.toResponseObject()));
|
||||
return;
|
||||
}
|
||||
|
||||
|
|
@ -1102,17 +1100,19 @@ describe('SSEClientTransport', () => {
|
|||
authProvider: mockAuthProvider
|
||||
});
|
||||
|
||||
await expect(() => transport.start()).rejects.toThrow(UnauthorizedClientError);
|
||||
await expect(() => transport.start()).rejects.toMatchObject(expectedError);
|
||||
expect(mockAuthProvider.invalidateCredentials).toHaveBeenCalledWith('all');
|
||||
});
|
||||
|
||||
it('invalidates tokens on InvalidGrantError during token refresh', async () => {
|
||||
it('invalidates tokens on OAuthErrorCode.InvalidGrant during token refresh', async () => {
|
||||
// Mock tokens() to return token with refresh token
|
||||
mockAuthProvider.tokens.mockResolvedValue({
|
||||
access_token: 'expired-token',
|
||||
token_type: 'Bearer',
|
||||
refresh_token: 'refresh-token'
|
||||
});
|
||||
|
||||
const expectedError = new OAuthError(OAuthErrorCode.InvalidGrant, 'Invalid refresh token');
|
||||
let baseUrl = resourceBaseUrl;
|
||||
|
||||
const server = createServer((req, res) => {
|
||||
|
|
@ -1134,9 +1134,7 @@ describe('SSEClientTransport', () => {
|
|||
}
|
||||
|
||||
if (req.url === '/token' && req.method === 'POST') {
|
||||
// Handle token refresh request - return InvalidGrantError
|
||||
const error = new InvalidGrantError('Invalid refresh token');
|
||||
res.writeHead(400, { 'Content-Type': 'application/json' }).end(JSON.stringify(error.toResponseObject()));
|
||||
res.writeHead(400, { 'Content-Type': 'application/json' }).end(JSON.stringify(expectedError.toResponseObject()));
|
||||
return;
|
||||
}
|
||||
|
||||
|
|
@ -1153,7 +1151,7 @@ describe('SSEClientTransport', () => {
|
|||
authProvider: mockAuthProvider
|
||||
});
|
||||
|
||||
await expect(() => transport.start()).rejects.toThrow(InvalidGrantError);
|
||||
await expect(() => transport.start()).rejects.toMatchObject(expectedError);
|
||||
expect(mockAuthProvider.invalidateCredentials).toHaveBeenCalledWith('tokens');
|
||||
});
|
||||
});
|
||||
|
|
|
|||
|
|
@ -1,5 +1,5 @@
|
|||
import type { JSONRPCMessage, JSONRPCRequest } from '@modelcontextprotocol/core';
|
||||
import { InvalidClientError, InvalidGrantError, UnauthorizedClientError } from '@modelcontextprotocol/core';
|
||||
import { OAuthError, OAuthErrorCode, SdkError, SdkErrorCode } from '@modelcontextprotocol/core';
|
||||
import type { Mock, Mocked } from 'vitest';
|
||||
|
||||
import type { OAuthClientProvider } from '../../src/client/auth.js';
|
||||
|
|
@ -212,7 +212,12 @@ describe('StreamableHTTPClientTransport', () => {
|
|||
const errorSpy = vi.fn();
|
||||
transport.onerror = errorSpy;
|
||||
|
||||
await expect(transport.send(message)).rejects.toThrow('Streamable HTTP error: Error POSTing to endpoint: Session not found');
|
||||
await expect(transport.send(message)).rejects.toThrow(
|
||||
new SdkError(SdkErrorCode.ClientHttpNotImplemented, 'Error POSTing to endpoint: Session not found', {
|
||||
status: 404,
|
||||
text: 'Session not found'
|
||||
})
|
||||
);
|
||||
expect(errorSpy).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
|
|
@ -1050,7 +1055,7 @@ describe('StreamableHTTPClientTransport', () => {
|
|||
});
|
||||
});
|
||||
|
||||
it('invalidates all credentials on InvalidClientError during auth', async () => {
|
||||
it('invalidates all credentials on OAuthErrorCode.InvalidClient during auth', async () => {
|
||||
const message: JSONRPCMessage = {
|
||||
jsonrpc: '2.0',
|
||||
method: 'test',
|
||||
|
|
@ -1092,9 +1097,11 @@ describe('StreamableHTTPClientTransport', () => {
|
|||
code_challenge_methods_supported: ['S256']
|
||||
})
|
||||
})
|
||||
// Token refresh fails with InvalidClientError
|
||||
// Token refresh fails with OAuthErrorCode.InvalidClient
|
||||
.mockResolvedValueOnce(
|
||||
Response.json(new InvalidClientError('Client authentication failed').toResponseObject(), { status: 400 })
|
||||
Response.json(new OAuthError(OAuthErrorCode.InvalidClient, 'Client authentication failed').toResponseObject(), {
|
||||
status: 400
|
||||
})
|
||||
)
|
||||
// Fallback should fail to complete the flow
|
||||
.mockResolvedValue({
|
||||
|
|
@ -1107,7 +1114,7 @@ describe('StreamableHTTPClientTransport', () => {
|
|||
await transport.send(message).catch(() => {});
|
||||
});
|
||||
|
||||
it('invalidates all credentials on UnauthorizedClientError during auth', async () => {
|
||||
it('invalidates all credentials on OAuthErrorCode.UnauthorizedClient during auth', async () => {
|
||||
const message: JSONRPCMessage = {
|
||||
jsonrpc: '2.0',
|
||||
method: 'test',
|
||||
|
|
@ -1149,8 +1156,12 @@ describe('StreamableHTTPClientTransport', () => {
|
|||
code_challenge_methods_supported: ['S256']
|
||||
})
|
||||
})
|
||||
// Token refresh fails with UnauthorizedClientError
|
||||
.mockResolvedValueOnce(Response.json(new UnauthorizedClientError('Client not authorized').toResponseObject(), { status: 400 }))
|
||||
// Token refresh fails with OAuthErrorCode.UnauthorizedClient
|
||||
.mockResolvedValueOnce(
|
||||
Response.json(new OAuthError(OAuthErrorCode.UnauthorizedClient, 'Client not authorized').toResponseObject(), {
|
||||
status: 400
|
||||
})
|
||||
)
|
||||
// Fallback should fail to complete the flow
|
||||
.mockResolvedValue({
|
||||
ok: false,
|
||||
|
|
@ -1165,7 +1176,7 @@ describe('StreamableHTTPClientTransport', () => {
|
|||
await transport.send(message).catch(() => {});
|
||||
});
|
||||
|
||||
it('invalidates tokens on InvalidGrantError during auth', async () => {
|
||||
it('invalidates tokens on OAuthErrorCode.InvalidGrant during auth', async () => {
|
||||
const message: JSONRPCMessage = {
|
||||
jsonrpc: '2.0',
|
||||
method: 'test',
|
||||
|
|
@ -1207,8 +1218,10 @@ describe('StreamableHTTPClientTransport', () => {
|
|||
code_challenge_methods_supported: ['S256']
|
||||
})
|
||||
})
|
||||
// Token refresh fails with InvalidGrantError
|
||||
.mockResolvedValueOnce(Response.json(new InvalidGrantError('Invalid refresh token').toResponseObject(), { status: 400 }))
|
||||
// Token refresh fails with OAuthErrorCode.InvalidGrant
|
||||
.mockResolvedValueOnce(
|
||||
Response.json(new OAuthError(OAuthErrorCode.InvalidGrant, 'Invalid refresh token').toResponseObject(), { status: 400 })
|
||||
)
|
||||
// Fallback should fail to complete the flow
|
||||
.mockResolvedValue({
|
||||
ok: false,
|
||||
|
|
@ -1218,7 +1231,7 @@ describe('StreamableHTTPClientTransport', () => {
|
|||
}
|
||||
});
|
||||
|
||||
// Behavior for InvalidGrantError during auth is covered in dedicated OAuth
|
||||
// Behavior for OAuthErrorCode.InvalidGrant during auth is covered in dedicated OAuth
|
||||
// unit tests and SSE transport tests. Here we just assert that the call
|
||||
// path completes without unhandled rejections.
|
||||
await transport.send(message).catch(() => {});
|
||||
|
|
@ -1255,9 +1268,11 @@ describe('StreamableHTTPClientTransport', () => {
|
|||
code_challenge_methods_supported: ['S256']
|
||||
})
|
||||
})
|
||||
// Token refresh fails with InvalidClientError
|
||||
// Token refresh fails with OAuthErrorCode.InvalidClient
|
||||
.mockResolvedValueOnce(
|
||||
Response.json(new InvalidClientError('Client authentication failed').toResponseObject(), { status: 400 })
|
||||
Response.json(new OAuthError(OAuthErrorCode.InvalidClient, 'Client authentication failed').toResponseObject(), {
|
||||
status: 400
|
||||
})
|
||||
)
|
||||
// Fallback should fail to complete the flow
|
||||
.mockResolvedValue({
|
||||
|
|
|
|||
|
|
@ -1,25 +1,117 @@
|
|||
import type { OAuthErrorResponse } from '../shared/auth.js';
|
||||
|
||||
/**
|
||||
* Base class for all OAuth errors
|
||||
* OAuth error codes as defined by RFC 6749 and extensions.
|
||||
*/
|
||||
export enum OAuthErrorCode {
|
||||
/**
|
||||
* The request is missing a required parameter, includes an invalid parameter value,
|
||||
* includes a parameter more than once, or is otherwise malformed.
|
||||
*/
|
||||
InvalidRequest = 'invalid_request',
|
||||
|
||||
/**
|
||||
* Client authentication failed (e.g., unknown client, no client authentication included,
|
||||
* or unsupported authentication method).
|
||||
*/
|
||||
InvalidClient = 'invalid_client',
|
||||
|
||||
/**
|
||||
* The provided authorization grant or refresh token is invalid, expired, revoked,
|
||||
* does not match the redirection URI used in the authorization request, or was issued to another client.
|
||||
*/
|
||||
InvalidGrant = 'invalid_grant',
|
||||
|
||||
/**
|
||||
* The authenticated client is not authorized to use this authorization grant type.
|
||||
*/
|
||||
UnauthorizedClient = 'unauthorized_client',
|
||||
|
||||
/**
|
||||
* The authorization grant type is not supported by the authorization server.
|
||||
*/
|
||||
UnsupportedGrantType = 'unsupported_grant_type',
|
||||
|
||||
/**
|
||||
* The requested scope is invalid, unknown, malformed, or exceeds the scope granted by the resource owner.
|
||||
*/
|
||||
InvalidScope = 'invalid_scope',
|
||||
|
||||
/**
|
||||
* The resource owner or authorization server denied the request.
|
||||
*/
|
||||
AccessDenied = 'access_denied',
|
||||
|
||||
/**
|
||||
* The authorization server encountered an unexpected condition that prevented it from fulfilling the request.
|
||||
*/
|
||||
ServerError = 'server_error',
|
||||
|
||||
/**
|
||||
* The authorization server is currently unable to handle the request due to temporary overloading or maintenance.
|
||||
*/
|
||||
TemporarilyUnavailable = 'temporarily_unavailable',
|
||||
|
||||
/**
|
||||
* The authorization server does not support obtaining an authorization code using this method.
|
||||
*/
|
||||
UnsupportedResponseType = 'unsupported_response_type',
|
||||
|
||||
/**
|
||||
* The authorization server does not support the requested token type.
|
||||
*/
|
||||
UnsupportedTokenType = 'unsupported_token_type',
|
||||
|
||||
/**
|
||||
* The access token provided is expired, revoked, malformed, or invalid for other reasons.
|
||||
*/
|
||||
InvalidToken = 'invalid_token',
|
||||
|
||||
/**
|
||||
* The HTTP method used is not allowed for this endpoint. (Custom, non-standard error)
|
||||
*/
|
||||
MethodNotAllowed = 'method_not_allowed',
|
||||
|
||||
/**
|
||||
* Rate limit exceeded. (Custom, non-standard error based on RFC 6585)
|
||||
*/
|
||||
TooManyRequests = 'too_many_requests',
|
||||
|
||||
/**
|
||||
* The client metadata is invalid. (Custom error for dynamic client registration - RFC 7591)
|
||||
*/
|
||||
InvalidClientMetadata = 'invalid_client_metadata',
|
||||
|
||||
/**
|
||||
* The request requires higher privileges than provided by the access token.
|
||||
*/
|
||||
InsufficientScope = 'insufficient_scope',
|
||||
|
||||
/**
|
||||
* The requested resource is invalid, missing, unknown, or malformed. (Custom error for resource indicators - RFC 8707)
|
||||
*/
|
||||
InvalidTarget = 'invalid_target'
|
||||
}
|
||||
|
||||
/**
|
||||
* OAuth error class for all OAuth-related errors.
|
||||
*/
|
||||
export class OAuthError extends Error {
|
||||
static errorCode: string;
|
||||
|
||||
constructor(
|
||||
public readonly code: OAuthErrorCode | string,
|
||||
message: string,
|
||||
public readonly errorUri?: string
|
||||
) {
|
||||
super(message);
|
||||
this.name = this.constructor.name;
|
||||
this.name = 'OAuthError';
|
||||
}
|
||||
|
||||
/**
|
||||
* Converts the error to a standard OAuth error response object
|
||||
* Converts the error to a standard OAuth error response object.
|
||||
*/
|
||||
toResponseObject(): OAuthErrorResponse {
|
||||
const response: OAuthErrorResponse = {
|
||||
error: this.errorCode,
|
||||
error: this.code,
|
||||
error_description: this.message
|
||||
};
|
||||
|
||||
|
|
@ -30,183 +122,10 @@ export class OAuthError extends Error {
|
|||
return response;
|
||||
}
|
||||
|
||||
get errorCode(): string {
|
||||
return (this.constructor as typeof OAuthError).errorCode;
|
||||
/**
|
||||
* Creates an OAuthError from an OAuth error response.
|
||||
*/
|
||||
static fromResponse(response: OAuthErrorResponse): OAuthError {
|
||||
return new OAuthError(response.error as OAuthErrorCode, response.error_description ?? response.error, response.error_uri);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Invalid request error - The request is missing a required parameter,
|
||||
* includes an invalid parameter value, includes a parameter more than once,
|
||||
* or is otherwise malformed.
|
||||
*/
|
||||
export class InvalidRequestError extends OAuthError {
|
||||
static override errorCode = 'invalid_request';
|
||||
}
|
||||
|
||||
/**
|
||||
* Invalid client error - Client authentication failed (e.g., unknown client, no client
|
||||
* authentication included, or unsupported authentication method).
|
||||
*/
|
||||
export class InvalidClientError extends OAuthError {
|
||||
static override errorCode = 'invalid_client';
|
||||
}
|
||||
|
||||
/**
|
||||
* Invalid grant error - The provided authorization grant or refresh token is
|
||||
* invalid, expired, revoked, does not match the redirection URI used in the
|
||||
* authorization request, or was issued to another client.
|
||||
*/
|
||||
export class InvalidGrantError extends OAuthError {
|
||||
static override errorCode = 'invalid_grant';
|
||||
}
|
||||
|
||||
/**
|
||||
* Unauthorized client error - The authenticated client is not authorized to use
|
||||
* this authorization grant type.
|
||||
*/
|
||||
export class UnauthorizedClientError extends OAuthError {
|
||||
static override errorCode = 'unauthorized_client';
|
||||
}
|
||||
|
||||
/**
|
||||
* Unsupported grant type error - The authorization grant type is not supported
|
||||
* by the authorization server.
|
||||
*/
|
||||
export class UnsupportedGrantTypeError extends OAuthError {
|
||||
static override errorCode = 'unsupported_grant_type';
|
||||
}
|
||||
|
||||
/**
|
||||
* Invalid scope error - The requested scope is invalid, unknown, malformed, or
|
||||
* exceeds the scope granted by the resource owner.
|
||||
*/
|
||||
export class InvalidScopeError extends OAuthError {
|
||||
static override errorCode = 'invalid_scope';
|
||||
}
|
||||
|
||||
/**
|
||||
* Access denied error - The resource owner or authorization server denied the request.
|
||||
*/
|
||||
export class AccessDeniedError extends OAuthError {
|
||||
static override errorCode = 'access_denied';
|
||||
}
|
||||
|
||||
/**
|
||||
* Server error - The authorization server encountered an unexpected condition
|
||||
* that prevented it from fulfilling the request.
|
||||
*/
|
||||
export class ServerError extends OAuthError {
|
||||
static override errorCode = 'server_error';
|
||||
}
|
||||
|
||||
/**
|
||||
* Temporarily unavailable error - The authorization server is currently unable to
|
||||
* handle the request due to a temporary overloading or maintenance of the server.
|
||||
*/
|
||||
export class TemporarilyUnavailableError extends OAuthError {
|
||||
static override errorCode = 'temporarily_unavailable';
|
||||
}
|
||||
|
||||
/**
|
||||
* Unsupported response type error - The authorization server does not support
|
||||
* obtaining an authorization code using this method.
|
||||
*/
|
||||
export class UnsupportedResponseTypeError extends OAuthError {
|
||||
static override errorCode = 'unsupported_response_type';
|
||||
}
|
||||
|
||||
/**
|
||||
* Unsupported token type error - The authorization server does not support
|
||||
* the requested token type.
|
||||
*/
|
||||
export class UnsupportedTokenTypeError extends OAuthError {
|
||||
static override errorCode = 'unsupported_token_type';
|
||||
}
|
||||
|
||||
/**
|
||||
* Invalid token error - The access token provided is expired, revoked, malformed,
|
||||
* or invalid for other reasons.
|
||||
*/
|
||||
export class InvalidTokenError extends OAuthError {
|
||||
static override errorCode = 'invalid_token';
|
||||
}
|
||||
|
||||
/**
|
||||
* Method not allowed error - The HTTP method used is not allowed for this endpoint.
|
||||
* (Custom, non-standard error)
|
||||
*/
|
||||
export class MethodNotAllowedError extends OAuthError {
|
||||
static override errorCode = 'method_not_allowed';
|
||||
}
|
||||
|
||||
/**
|
||||
* Too many requests error - Rate limit exceeded.
|
||||
* (Custom, non-standard error based on RFC 6585)
|
||||
*/
|
||||
export class TooManyRequestsError extends OAuthError {
|
||||
static override errorCode = 'too_many_requests';
|
||||
}
|
||||
|
||||
/**
|
||||
* Invalid client metadata error - The client metadata is invalid.
|
||||
* (Custom error for dynamic client registration - RFC 7591)
|
||||
*/
|
||||
export class InvalidClientMetadataError extends OAuthError {
|
||||
static override errorCode = 'invalid_client_metadata';
|
||||
}
|
||||
|
||||
/**
|
||||
* Insufficient scope error - The request requires higher privileges than provided by the access token.
|
||||
*/
|
||||
export class InsufficientScopeError extends OAuthError {
|
||||
static override errorCode = 'insufficient_scope';
|
||||
}
|
||||
|
||||
/**
|
||||
* Invalid target error - The requested resource is invalid, missing, unknown, or malformed.
|
||||
* (Custom error for resource indicators - RFC 8707)
|
||||
*/
|
||||
export class InvalidTargetError extends OAuthError {
|
||||
static override errorCode = 'invalid_target';
|
||||
}
|
||||
|
||||
/**
|
||||
* A utility class for defining one-off error codes
|
||||
*/
|
||||
export class CustomOAuthError extends OAuthError {
|
||||
constructor(
|
||||
private readonly customErrorCode: string,
|
||||
message: string,
|
||||
errorUri?: string
|
||||
) {
|
||||
super(message, errorUri);
|
||||
}
|
||||
|
||||
override get errorCode(): string {
|
||||
return this.customErrorCode;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* A full list of all OAuthErrors, enabling parsing from error responses
|
||||
*/
|
||||
export const OAUTH_ERRORS = {
|
||||
[InvalidRequestError.errorCode]: InvalidRequestError,
|
||||
[InvalidClientError.errorCode]: InvalidClientError,
|
||||
[InvalidGrantError.errorCode]: InvalidGrantError,
|
||||
[UnauthorizedClientError.errorCode]: UnauthorizedClientError,
|
||||
[UnsupportedGrantTypeError.errorCode]: UnsupportedGrantTypeError,
|
||||
[InvalidScopeError.errorCode]: InvalidScopeError,
|
||||
[AccessDeniedError.errorCode]: AccessDeniedError,
|
||||
[ServerError.errorCode]: ServerError,
|
||||
[TemporarilyUnavailableError.errorCode]: TemporarilyUnavailableError,
|
||||
[UnsupportedResponseTypeError.errorCode]: UnsupportedResponseTypeError,
|
||||
[UnsupportedTokenTypeError.errorCode]: UnsupportedTokenTypeError,
|
||||
[InvalidTokenError.errorCode]: InvalidTokenError,
|
||||
[MethodNotAllowedError.errorCode]: MethodNotAllowedError,
|
||||
[TooManyRequestsError.errorCode]: TooManyRequestsError,
|
||||
[InvalidClientMetadataError.errorCode]: InvalidClientMetadataError,
|
||||
[InsufficientScopeError.errorCode]: InsufficientScopeError,
|
||||
[InvalidTargetError.errorCode]: InvalidTargetError
|
||||
} as const;
|
||||
|
|
|
|||
64
packages/core/src/errors/sdkErrors.ts
Normal file
64
packages/core/src/errors/sdkErrors.ts
Normal file
|
|
@ -0,0 +1,64 @@
|
|||
/**
|
||||
* Error codes for SDK errors (local errors that never cross the wire).
|
||||
* Unlike ProtocolErrorCode which uses numeric JSON-RPC codes, SdkErrorCode uses
|
||||
* descriptive string values for better developer experience.
|
||||
*
|
||||
* These errors are thrown locally by the SDK and are never serialized as
|
||||
* JSON-RPC error responses.
|
||||
*/
|
||||
export enum SdkErrorCode {
|
||||
// State errors
|
||||
/** Transport is not connected */
|
||||
NotConnected = 'NOT_CONNECTED',
|
||||
/** Transport is already connected */
|
||||
AlreadyConnected = 'ALREADY_CONNECTED',
|
||||
/** Protocol is not initialized */
|
||||
NotInitialized = 'NOT_INITIALIZED',
|
||||
|
||||
// Capability errors
|
||||
/** Required capability is not supported by the remote side */
|
||||
CapabilityNotSupported = 'CAPABILITY_NOT_SUPPORTED',
|
||||
|
||||
// Transport errors
|
||||
/** Request timed out waiting for response */
|
||||
RequestTimeout = 'REQUEST_TIMEOUT',
|
||||
/** Connection was closed */
|
||||
ConnectionClosed = 'CONNECTION_CLOSED',
|
||||
/** Failed to send message */
|
||||
SendFailed = 'SEND_FAILED',
|
||||
|
||||
// Transport errors
|
||||
ClientHttpNotImplemented = 'CLIENT_HTTP_NOT_IMPLEMENTED',
|
||||
ClientHttpAuthentication = 'CLIENT_HTTP_AUTHENTICATION',
|
||||
ClientHttpForbidden = 'CLIENT_HTTP_FORBIDDEN',
|
||||
ClientHttpUnexpectedContent = 'CLIENT_HTTP_UNEXPECTED_CONTENT',
|
||||
ClientHttpFailedToOpenStream = 'CLIENT_HTTP_FAILED_TO_OPEN_STREAM',
|
||||
ClientHttpFailedToTerminateSession = 'CLIENT_HTTP_FAILED_TO_TERMINATE_SESSION'
|
||||
}
|
||||
|
||||
/**
|
||||
* SDK errors are local errors that never cross the wire.
|
||||
* They are distinct from ProtocolError which represents JSON-RPC protocol errors
|
||||
* that are serialized and sent as error responses.
|
||||
*
|
||||
* @example
|
||||
* ```typescript
|
||||
* // Throwing an SDK error
|
||||
* throw new SdkError(SdkErrorCode.NotConnected, 'Transport is not connected');
|
||||
*
|
||||
* // Checking error type by code
|
||||
* if (error instanceof SdkError && error.code === SdkErrorCode.RequestTimeout) {
|
||||
* // Handle timeout
|
||||
* }
|
||||
* ```
|
||||
*/
|
||||
export class SdkError extends Error {
|
||||
constructor(
|
||||
public readonly code: SdkErrorCode,
|
||||
message: string,
|
||||
public readonly data?: unknown
|
||||
) {
|
||||
super(message);
|
||||
this.name = 'SdkError';
|
||||
}
|
||||
}
|
||||
|
|
@ -1,4 +1,5 @@
|
|||
export * from './auth/errors.js';
|
||||
export * from './errors/sdkErrors.js';
|
||||
export * from './shared/auth.js';
|
||||
export * from './shared/authUtils.js';
|
||||
export * from './shared/metadataUtils.js';
|
||||
|
|
|
|||
|
|
@ -1,3 +1,4 @@
|
|||
import { SdkError, SdkErrorCode } from '../errors/sdkErrors.js';
|
||||
import type { CreateTaskOptions, QueuedMessage, TaskMessageQueue, TaskStore } from '../experimental/tasks/interfaces.js';
|
||||
import { isTerminal } from '../experimental/tasks/interfaces.js';
|
||||
import type {
|
||||
|
|
@ -34,7 +35,6 @@ import type {
|
|||
import {
|
||||
CancelTaskResultSchema,
|
||||
CreateTaskResultSchema,
|
||||
ErrorCode,
|
||||
getNotificationSchema,
|
||||
getRequestSchema,
|
||||
GetTaskResultSchema,
|
||||
|
|
@ -44,7 +44,8 @@ import {
|
|||
isJSONRPCResultResponse,
|
||||
isTaskAugmentedRequestParams,
|
||||
ListTasksResultSchema,
|
||||
McpError,
|
||||
ProtocolError,
|
||||
ProtocolErrorCode,
|
||||
RELATED_TASK_META_KEY,
|
||||
SUPPORTED_PROTOCOL_VERSIONS,
|
||||
TaskStatusNotificationSchema
|
||||
|
|
@ -134,7 +135,7 @@ export type RequestOptions = {
|
|||
signal?: AbortSignal;
|
||||
|
||||
/**
|
||||
* A timeout (in milliseconds) for this request. If exceeded, an McpError with code `RequestTimeout` will be raised from request().
|
||||
* A timeout (in milliseconds) for this request. If exceeded, an SdkError with code `SdkErrorCode.RequestTimeout` will be raised from request().
|
||||
*
|
||||
* If not specified, `DEFAULT_REQUEST_TIMEOUT_MSEC` will be used as the timeout.
|
||||
*/
|
||||
|
|
@ -149,7 +150,7 @@ export type RequestOptions = {
|
|||
|
||||
/**
|
||||
* Maximum total time (in milliseconds) to wait for a response.
|
||||
* If exceeded, an McpError with code `RequestTimeout` will be raised, regardless of progress notifications.
|
||||
* If exceeded, an SdkError with code `SdkErrorCode.RequestTimeout` will be raised, regardless of progress notifications.
|
||||
* If not specified, there is no maximum total timeout.
|
||||
*/
|
||||
maxTotalTimeout?: number;
|
||||
|
|
@ -401,7 +402,7 @@ export abstract class Protocol<SendRequestT extends Request, SendNotificationT e
|
|||
this.setRequestHandler('tasks/get', async (request, extra) => {
|
||||
const task = await this._taskStore!.getTask(request.params.taskId, extra.sessionId);
|
||||
if (!task) {
|
||||
throw new McpError(ErrorCode.InvalidParams, 'Failed to retrieve task: Task not found');
|
||||
throw new ProtocolError(ProtocolErrorCode.InvalidParams, 'Failed to retrieve task: Task not found');
|
||||
}
|
||||
|
||||
// Per spec: tasks/get responses SHALL NOT include related-task metadata
|
||||
|
|
@ -435,9 +436,9 @@ export abstract class Protocol<SendRequestT extends Request, SendNotificationT e
|
|||
if (queuedMessage.type === 'response') {
|
||||
resolver(message as JSONRPCResultResponse);
|
||||
} else {
|
||||
// Convert JSONRPCError to McpError
|
||||
// Convert JSONRPCError to ProtocolError
|
||||
const errorMessage = message as JSONRPCErrorResponse;
|
||||
const error = new McpError(
|
||||
const error = new ProtocolError(
|
||||
errorMessage.error.code,
|
||||
errorMessage.error.message,
|
||||
errorMessage.error.data
|
||||
|
|
@ -463,7 +464,7 @@ export abstract class Protocol<SendRequestT extends Request, SendNotificationT e
|
|||
// Now check task status
|
||||
const task = await this._taskStore!.getTask(taskId, extra.sessionId);
|
||||
if (!task) {
|
||||
throw new McpError(ErrorCode.InvalidParams, `Task not found: ${taskId}`);
|
||||
throw new ProtocolError(ProtocolErrorCode.InvalidParams, `Task not found: ${taskId}`);
|
||||
}
|
||||
|
||||
// Block if task is not terminal (we've already delivered all queued messages above)
|
||||
|
|
@ -507,8 +508,8 @@ export abstract class Protocol<SendRequestT extends Request, SendNotificationT e
|
|||
_meta: {}
|
||||
} as unknown as SendResultT;
|
||||
} catch (error) {
|
||||
throw new McpError(
|
||||
ErrorCode.InvalidParams,
|
||||
throw new ProtocolError(
|
||||
ProtocolErrorCode.InvalidParams,
|
||||
`Failed to list tasks: ${error instanceof Error ? error.message : String(error)}`
|
||||
);
|
||||
}
|
||||
|
|
@ -520,12 +521,12 @@ export abstract class Protocol<SendRequestT extends Request, SendNotificationT e
|
|||
const task = await this._taskStore!.getTask(request.params.taskId, extra.sessionId);
|
||||
|
||||
if (!task) {
|
||||
throw new McpError(ErrorCode.InvalidParams, `Task not found: ${request.params.taskId}`);
|
||||
throw new ProtocolError(ProtocolErrorCode.InvalidParams, `Task not found: ${request.params.taskId}`);
|
||||
}
|
||||
|
||||
// Reject cancellation of terminal tasks
|
||||
if (isTerminal(task.status)) {
|
||||
throw new McpError(ErrorCode.InvalidParams, `Cannot cancel task in terminal status: ${task.status}`);
|
||||
throw new ProtocolError(ProtocolErrorCode.InvalidParams, `Cannot cancel task in terminal status: ${task.status}`);
|
||||
}
|
||||
|
||||
await this._taskStore!.updateTaskStatus(
|
||||
|
|
@ -540,7 +541,10 @@ export abstract class Protocol<SendRequestT extends Request, SendNotificationT e
|
|||
const cancelledTask = await this._taskStore!.getTask(request.params.taskId, extra.sessionId);
|
||||
if (!cancelledTask) {
|
||||
// Task was deleted during cancellation (e.g., cleanup happened)
|
||||
throw new McpError(ErrorCode.InvalidParams, `Task not found after cancellation: ${request.params.taskId}`);
|
||||
throw new ProtocolError(
|
||||
ProtocolErrorCode.InvalidParams,
|
||||
`Task not found after cancellation: ${request.params.taskId}`
|
||||
);
|
||||
}
|
||||
|
||||
return {
|
||||
|
|
@ -548,12 +552,12 @@ export abstract class Protocol<SendRequestT extends Request, SendNotificationT e
|
|||
...cancelledTask
|
||||
} as unknown as SendResultT;
|
||||
} catch (error) {
|
||||
// Re-throw McpError as-is
|
||||
if (error instanceof McpError) {
|
||||
// Re-throw ProtocolError as-is
|
||||
if (error instanceof ProtocolError) {
|
||||
throw error;
|
||||
}
|
||||
throw new McpError(
|
||||
ErrorCode.InvalidRequest,
|
||||
throw new ProtocolError(
|
||||
ProtocolErrorCode.InvalidRequest,
|
||||
`Failed to cancel task: ${error instanceof Error ? error.message : String(error)}`
|
||||
);
|
||||
}
|
||||
|
|
@ -594,7 +598,7 @@ export abstract class Protocol<SendRequestT extends Request, SendNotificationT e
|
|||
const totalElapsed = Date.now() - info.startTime;
|
||||
if (info.maxTotalTimeout && totalElapsed >= info.maxTotalTimeout) {
|
||||
this._timeoutInfo.delete(messageId);
|
||||
throw McpError.fromError(ErrorCode.RequestTimeout, 'Maximum total timeout exceeded', {
|
||||
throw new SdkError(SdkErrorCode.RequestTimeout, 'Maximum total timeout exceeded', {
|
||||
maxTotalTimeout: info.maxTotalTimeout,
|
||||
totalElapsed
|
||||
});
|
||||
|
|
@ -659,7 +663,7 @@ export abstract class Protocol<SendRequestT extends Request, SendNotificationT e
|
|||
this._taskProgressTokens.clear();
|
||||
this._pendingDebouncedNotifications.clear();
|
||||
|
||||
const error = McpError.fromError(ErrorCode.ConnectionClosed, 'Connection closed');
|
||||
const error = new SdkError(SdkErrorCode.ConnectionClosed, 'Connection closed');
|
||||
|
||||
this._transport = undefined;
|
||||
this.onclose?.();
|
||||
|
|
@ -701,7 +705,7 @@ export abstract class Protocol<SendRequestT extends Request, SendNotificationT e
|
|||
jsonrpc: '2.0',
|
||||
id: request.id,
|
||||
error: {
|
||||
code: ErrorCode.MethodNotFound,
|
||||
code: ProtocolErrorCode.MethodNotFound,
|
||||
message: 'Method not found'
|
||||
}
|
||||
};
|
||||
|
|
@ -815,7 +819,7 @@ export abstract class Protocol<SendRequestT extends Request, SendNotificationT e
|
|||
jsonrpc: '2.0',
|
||||
id: request.id,
|
||||
error: {
|
||||
code: Number.isSafeInteger(error['code']) ? error['code'] : ErrorCode.InternalError,
|
||||
code: Number.isSafeInteger(error['code']) ? error['code'] : ProtocolErrorCode.InternalError,
|
||||
message: error.message ?? 'Internal error',
|
||||
...(error['data'] !== undefined && { data: error['data'] })
|
||||
}
|
||||
|
|
@ -880,7 +884,7 @@ export abstract class Protocol<SendRequestT extends Request, SendNotificationT e
|
|||
if (isJSONRPCResultResponse(response)) {
|
||||
resolver(response);
|
||||
} else {
|
||||
const error = new McpError(response.error.code, response.error.message, response.error.data);
|
||||
const error = new ProtocolError(response.error.code, response.error.message, response.error.data);
|
||||
resolver(error);
|
||||
}
|
||||
return;
|
||||
|
|
@ -915,7 +919,7 @@ export abstract class Protocol<SendRequestT extends Request, SendNotificationT e
|
|||
if (isJSONRPCResultResponse(response)) {
|
||||
handler(response);
|
||||
} else {
|
||||
const error = McpError.fromError(response.error.code, response.error.message, response.error.data);
|
||||
const error = ProtocolError.fromError(response.error.code, response.error.message, response.error.data);
|
||||
handler(error);
|
||||
}
|
||||
}
|
||||
|
|
@ -1008,7 +1012,7 @@ export abstract class Protocol<SendRequestT extends Request, SendNotificationT e
|
|||
} catch (error) {
|
||||
yield {
|
||||
type: 'error',
|
||||
error: error instanceof McpError ? error : new McpError(ErrorCode.InternalError, String(error))
|
||||
error: error instanceof Error ? error : new Error(String(error))
|
||||
};
|
||||
}
|
||||
return;
|
||||
|
|
@ -1026,7 +1030,7 @@ export abstract class Protocol<SendRequestT extends Request, SendNotificationT e
|
|||
taskId = createResult.task.taskId;
|
||||
yield { type: 'taskCreated', task: createResult.task };
|
||||
} else {
|
||||
throw new McpError(ErrorCode.InternalError, 'Task creation did not return a task');
|
||||
throw new ProtocolError(ProtocolErrorCode.InternalError, 'Task creation did not return a task');
|
||||
}
|
||||
|
||||
// Poll for task completion
|
||||
|
|
@ -1048,7 +1052,7 @@ export abstract class Protocol<SendRequestT extends Request, SendNotificationT e
|
|||
case 'failed': {
|
||||
yield {
|
||||
type: 'error',
|
||||
error: new McpError(ErrorCode.InternalError, `Task ${taskId} failed`)
|
||||
error: new ProtocolError(ProtocolErrorCode.InternalError, `Task ${taskId} failed`)
|
||||
};
|
||||
|
||||
break;
|
||||
|
|
@ -1056,7 +1060,7 @@ export abstract class Protocol<SendRequestT extends Request, SendNotificationT e
|
|||
case 'cancelled': {
|
||||
yield {
|
||||
type: 'error',
|
||||
error: new McpError(ErrorCode.InternalError, `Task ${taskId} was cancelled`)
|
||||
error: new ProtocolError(ProtocolErrorCode.InternalError, `Task ${taskId} was cancelled`)
|
||||
};
|
||||
|
||||
break;
|
||||
|
|
@ -1084,7 +1088,7 @@ export abstract class Protocol<SendRequestT extends Request, SendNotificationT e
|
|||
} catch (error) {
|
||||
yield {
|
||||
type: 'error',
|
||||
error: error instanceof McpError ? error : new McpError(ErrorCode.InternalError, String(error))
|
||||
error: error instanceof Error ? error : new Error(String(error))
|
||||
};
|
||||
}
|
||||
}
|
||||
|
|
@ -1180,8 +1184,8 @@ export abstract class Protocol<SendRequestT extends Request, SendNotificationT e
|
|||
)
|
||||
.catch(error => this._onerror(new Error(`Failed to send cancellation: ${error}`)));
|
||||
|
||||
// Wrap the reason in an McpError if it isn't already
|
||||
const error = reason instanceof McpError ? reason : new McpError(ErrorCode.RequestTimeout, String(reason));
|
||||
// Wrap the reason in an SdkError if it isn't already
|
||||
const error = reason instanceof SdkError ? reason : new SdkError(SdkErrorCode.RequestTimeout, String(reason));
|
||||
reject(error);
|
||||
};
|
||||
|
||||
|
|
@ -1212,7 +1216,7 @@ export abstract class Protocol<SendRequestT extends Request, SendNotificationT e
|
|||
});
|
||||
|
||||
const timeout = options?.timeout ?? DEFAULT_REQUEST_TIMEOUT_MSEC;
|
||||
const timeoutHandler = () => cancel(McpError.fromError(ErrorCode.RequestTimeout, 'Request timed out', { timeout }));
|
||||
const timeoutHandler = () => cancel(new SdkError(SdkErrorCode.RequestTimeout, 'Request timed out', { timeout }));
|
||||
|
||||
this._setupTimeout(messageId, timeout, options?.maxTotalTimeout, timeoutHandler, options?.resetTimeoutOnProgress ?? false);
|
||||
|
||||
|
|
@ -1301,7 +1305,7 @@ export abstract class Protocol<SendRequestT extends Request, SendNotificationT e
|
|||
*/
|
||||
async notification(notification: SendNotificationT, options?: NotificationOptions): Promise<void> {
|
||||
if (!this._transport) {
|
||||
throw new Error('Not connected');
|
||||
throw new SdkError(SdkErrorCode.NotConnected, 'Not connected');
|
||||
}
|
||||
|
||||
this.assertNotificationCapability(notification.method);
|
||||
|
|
@ -1518,7 +1522,7 @@ export abstract class Protocol<SendRequestT extends Request, SendNotificationT e
|
|||
const requestId = message.message.id as RequestId;
|
||||
const resolver = this._requestResolvers.get(requestId);
|
||||
if (resolver) {
|
||||
resolver(new McpError(ErrorCode.InternalError, 'Task cancelled or completed'));
|
||||
resolver(new ProtocolError(ProtocolErrorCode.InternalError, 'Task cancelled or completed'));
|
||||
this._requestResolvers.delete(requestId);
|
||||
} else {
|
||||
// Log error when resolver is missing during cleanup for better observability
|
||||
|
|
@ -1550,7 +1554,7 @@ export abstract class Protocol<SendRequestT extends Request, SendNotificationT e
|
|||
|
||||
return new Promise((resolve, reject) => {
|
||||
if (signal.aborted) {
|
||||
reject(new McpError(ErrorCode.InvalidRequest, 'Request cancelled'));
|
||||
reject(new ProtocolError(ProtocolErrorCode.InvalidRequest, 'Request cancelled'));
|
||||
return;
|
||||
}
|
||||
|
||||
|
|
@ -1562,7 +1566,7 @@ export abstract class Protocol<SendRequestT extends Request, SendNotificationT e
|
|||
'abort',
|
||||
() => {
|
||||
clearTimeout(timeoutId);
|
||||
reject(new McpError(ErrorCode.InvalidRequest, 'Request cancelled'));
|
||||
reject(new ProtocolError(ProtocolErrorCode.InvalidRequest, 'Request cancelled'));
|
||||
},
|
||||
{ once: true }
|
||||
);
|
||||
|
|
@ -1594,7 +1598,7 @@ export abstract class Protocol<SendRequestT extends Request, SendNotificationT e
|
|||
getTask: async taskId => {
|
||||
const task = await taskStore.getTask(taskId, sessionId);
|
||||
if (!task) {
|
||||
throw new McpError(ErrorCode.InvalidParams, 'Failed to retrieve task: Task not found');
|
||||
throw new ProtocolError(ProtocolErrorCode.InvalidParams, 'Failed to retrieve task: Task not found');
|
||||
}
|
||||
|
||||
return task;
|
||||
|
|
@ -1624,13 +1628,13 @@ export abstract class Protocol<SendRequestT extends Request, SendNotificationT e
|
|||
// Check if task exists
|
||||
const task = await taskStore.getTask(taskId, sessionId);
|
||||
if (!task) {
|
||||
throw new McpError(ErrorCode.InvalidParams, `Task "${taskId}" not found - it may have been cleaned up`);
|
||||
throw new ProtocolError(ProtocolErrorCode.InvalidParams, `Task "${taskId}" not found - it may have been cleaned up`);
|
||||
}
|
||||
|
||||
// Don't allow transitions from terminal states
|
||||
if (isTerminal(task.status)) {
|
||||
throw new McpError(
|
||||
ErrorCode.InvalidParams,
|
||||
throw new ProtocolError(
|
||||
ProtocolErrorCode.InvalidParams,
|
||||
`Cannot update task "${taskId}" from terminal status "${task.status}" to "${status}". Terminal states (completed, failed, cancelled) cannot transition to other states.`
|
||||
);
|
||||
}
|
||||
|
|
|
|||
Some files were not shown because too many files have changed in this diff Show more
Loading…
Reference in a new issue