v2: Errors refactor (ProtocolError, SdkError, OAuthError) (#1454)

This commit is contained in:
Konstantin Konstantinov 2026-02-03 18:41:22 +02:00 • committed by GitHub
commit 0f0a4eb7c8
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
33 changed files with 1086 additions and 705 deletions

View file

@ -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

View file

@ -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.

View file

@ -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?

View file

@ -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}`);
}
}

View file

@ -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}`);

View file

@ -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);
});

View file

@ -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

View file

@ -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)}`
);
}

View file

@ -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:

View file

@ -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 {

View file

@ -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);

View file

@ -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;

View file

@ -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)}`
)
};

View file

@ -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 () => {

View file

@ -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');
});
});

View file

@ -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({

View file

@ -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;

View 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';
}
}

View file

@ -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';

View file

@ -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