This document provides comprehensive documentation of error handling features and patterns in the Model Context Protocol (MCP) TypeScript SDK. The SDK implements a robust error handling system that covers protocol-level errors, OAuth authentication errors, transport errors, and application-level exceptions.
Table of Contents
- Core Error Classes
- JSON-RPC Error Codes
- OAuth Error Handling
- Protocol Error Patterns
- Transport Error Handling
- Client Error Handling
- Server Error Handling
- Request Lifecycle Error Management
- Timeout and Cancellation
- Error Propagation and Recovery
- Best Practices
- Examples
Core Error Classes
McpError
The McpError
class is the primary error type for MCP protocol-level errors. It extends the standard JavaScript Error
class with MCP-specific information.
Location: src/types.ts:1461-1470
export class McpError extends Error { constructor( public readonly code: number, message: string, public readonly data?: unknown, ) { super(`MCP error ${code}: ${message}`); this.name = "McpError"; } }
Properties:
-
code
: Numeric error code (fromErrorCode
enum) -
message
: Human-readable error description -
data
: Optional additional error information -
name
: Always set to "McpError"
Usage:
import { McpError, ErrorCode } from "@modelcontextprotocol/sdk"; throw new McpError( ErrorCode.InvalidRequest, "Missing required parameter", { parameter: "name" } );
OAuthError Hierarchy
A comprehensive set of OAuth-specific error classes that implement RFC 6749 OAuth 2.0 error responses.
Location: src/server/auth/errors.ts
Base Class: OAuthError
export class OAuthError extends Error { static errorCode: string; constructor( message: string, public readonly errorUri?: string ) { super(message); this.name = this.constructor.name; } toResponseObject(): OAuthErrorResponse { const response: OAuthErrorResponse = { error: this.errorCode, error_description: this.message }; if (this.errorUri) { response.error_uri = this.errorUri; } return response; } get errorCode(): string { return (this.constructor as typeof OAuthError).errorCode } }
Standard OAuth Error Types
-
InvalidRequestError (
invalid_request
)- Malformed request, missing parameters, or invalid parameter values
-
InvalidClientError (
invalid_client
)- Client authentication failed
-
InvalidGrantError (
invalid_grant
)- Authorization grant is invalid, expired, or revoked
-
UnauthorizedClientError (
unauthorized_client
)- Client not authorized for this grant type
-
UnsupportedGrantTypeError (
unsupported_grant_type
)- Grant type not supported by authorization server
-
InvalidScopeError (
invalid_scope
)- Requested scope is invalid or exceeds granted scope
-
AccessDeniedError (
access_denied
)- Resource owner or authorization server denied the request
-
ServerError (
server_error
)- Unexpected server condition
-
TemporarilyUnavailableError (
temporarily_unavailable
)- Server temporarily overloaded or under maintenance
-
UnsupportedResponseTypeError (
unsupported_response_type
)- Authorization server doesn't support this response type
-
UnsupportedTokenTypeError (
unsupported_token_type
)- Token type not supported
-
InvalidTokenError (
invalid_token
)- Access token expired, revoked, or malformed
-
MethodNotAllowedError (
method_not_allowed
)- HTTP method not allowed (custom extension)
-
TooManyRequestsError (
too_many_requests
)- Rate limit exceeded (RFC 6585)
-
InvalidClientMetadataError (
invalid_client_metadata
)- Invalid client metadata (RFC 7591)
-
InsufficientScopeError (
insufficient_scope
)- Request requires higher privileges
CustomOAuthError
For defining custom error codes:
export class CustomOAuthError extends OAuthError { constructor( private readonly customErrorCode: string, message: string, errorUri?: string ) { super(message, errorUri); } get errorCode(): string { return this.customErrorCode; } }
Error Registry
All OAuth errors are registered in the OAUTH_ERRORS
constant for runtime error parsing:
export const OAUTH_ERRORS = { [InvalidRequestError.errorCode]: InvalidRequestError, [InvalidClientError.errorCode]: InvalidClientError, // ... all other error types } as const;
JSON-RPC Error Codes
The SDK defines standard JSON-RPC error codes in the ErrorCode
enum:
Location: src/types.ts:122-133
export enum ErrorCode { // SDK error codes ConnectionClosed = -32000, RequestTimeout = -32001, // Standard JSON-RPC error codes ParseError = -32700, InvalidRequest = -32600, MethodNotFound = -32601, InvalidParams = -32602, InternalError = -32603, }
Error Code Categories
-
SDK-Specific Errors (-32000 to -32099)
-
ConnectionClosed (-32000)
: Transport connection was closed -
RequestTimeout (-32001)
: Request exceeded timeout duration
-
-
Standard JSON-RPC Errors (-32600 to -32699)
-
ParseError (-32700)
: Invalid JSON received -
InvalidRequest (-32600)
: JSON-RPC request is invalid -
MethodNotFound (-32601)
: Requested method doesn't exist -
InvalidParams (-32602)
: Invalid method parameters -
InternalError (-32603)
: Internal server error
-
OAuth Error Handling
Client-Side OAuth Errors
Location: src/client/auth.ts
The SDK provides utilities for handling OAuth errors in client applications:
import { OAuthError, InvalidClientError, InvalidGrantError } from "@modelcontextprotocol/sdk"; // Error handling in OAuth flow try { const token = await exchangeAuthorization(authCode, codeVerifier, metadata); } catch (error) { if (error instanceof InvalidGrantError) { // Handle invalid authorization code console.error("Authorization code is invalid or expired"); } else if (error instanceof InvalidClientError) { // Handle client authentication failure console.error("Client credentials are invalid"); } else if (error instanceof OAuthError) { // Handle other OAuth errors console.error(`OAuth error: ${error.errorCode} - ${error.message}`); } }
Server-Side OAuth Error Responses
OAuth errors are automatically converted to proper HTTP responses:
try { // OAuth operation } catch (error) { if (error instanceof OAuthError) { const errorResponse = error.toResponseObject(); return new Response(JSON.stringify(errorResponse), { status: 400, headers: { 'Content-Type': 'application/json' } }); } }
UnauthorizedError
A special client-side error for handling unauthorized access:
export class UnauthorizedError extends Error { constructor(message: string = "Unauthorized") { super(message); this.name = "UnauthorizedError"; } }
Protocol Error Patterns
Request Handler Error Handling
Location: src/shared/protocol.ts:411-449
The Protocol class implements comprehensive error handling for request processing:
Promise.resolve() .then(() => handler(request, fullExtra)) .then( (result) => { if (abortController.signal.aborted) { return; } return capturedTransport?.send({ result, jsonrpc: "2.0", id: request.id, }); }, (error) => { if (abortController.signal.aborted) { return; } return capturedTransport?.send({ jsonrpc: "2.0", id: request.id, error: { code: Number.isSafeInteger(error["code"]) ? error["code"] : ErrorCode.InternalError, message: error.message ?? "Internal error", }, }); }, ) .catch((error) => this._onerror(new Error(`Failed to send response: ${error}`)), );
Key Features:
- Automatic error code detection and fallback
- Abort signal handling
- Transport error isolation
- Structured error responses
Notification Handler Error Handling
Location: src/shared/protocol.ts:359-367
Promise.resolve() .then(() => handler(notification)) .catch((error) => this._onerror( new Error(`Uncaught error in notification handler: ${error}`), ), );
Features:
- Async error containment
- Error reporting through
onerror
callback - Non-blocking error handling
Transport Error Handling
Connection Management
Location: src/shared/protocol.ts:331-343
private _onclose(): void { const responseHandlers = this._responseHandlers; this._responseHandlers = new Map(); this._progressHandlers.clear(); this._pendingDebouncedNotifications.clear(); this._transport = undefined; this.onclose?.(); const error = new McpError(ErrorCode.ConnectionClosed, "Connection closed"); for (const handler of responseHandlers.values()) { handler(error); } }
Connection Closure Handling:
- All pending requests receive
ConnectionClosed
error - Clean resource cleanup
- Handler deregistration
- Optional close callback invocation
WebSocket Error Handling
Location: src/client/websocket.ts
this._ws.onerror = (event) => { const error = event.error ? (event.error as Error) : new Error(`WebSocket error: ${JSON.stringify(event)}`); this.onerror?.(error); }; this._ws.onmessage = (event) => { try { const message = JSON.parse(event.data); this.onmessage?.(message); } catch (error) { this.onerror?.(error as Error); } };
Features:
- WebSocket event error transformation
- JSON parsing error handling
- Error callback propagation
Client Error Handling
Initialization Error Handling
Location: src/client/index.ts:135-183
try { const result = await this.request( { method: "initialize", params: { protocolVersion: LATEST_PROTOCOL_VERSION, capabilities: this._capabilities, clientInfo: this._clientInfo, }, }, InitializeResultSchema, options ); if (result === undefined) { throw new Error(`Server sent invalid initialize result: ${result}`); } if (!SUPPORTED_PROTOCOL_VERSIONS.includes(result.protocolVersion)) { throw new Error( `Server's protocol version is not supported: ${result.protocolVersion}`, ); } // ... initialization logic } catch (error) { // Disconnect if initialization fails void this.close(); throw error; }
Initialization Error Patterns:
- Automatic disconnection on failure
- Protocol version validation
- Result validation
- Error propagation with cleanup
Capability Validation
protected assertCapability( capability: keyof ServerCapabilities, method: string, ): void { if (!this._serverCapabilities?.[capability]) { throw new Error( `Server does not support ${capability} (required for ${method})`, ); } }
Tool Call Validation
Location: src/client/index.ts:429-478
async callTool(params: CallToolRequest["params"]) { const result = await this.request( { method: "tools/call", params }, resultSchema, options, ); // Validate tool output schema const validator = this.getToolOutputValidator(params.name); if (validator) { if (!result.structuredContent && !result.isError) { throw new McpError( ErrorCode.InvalidRequest, `Tool ${params.name} has an output schema but did not return structured content` ); } if (result.structuredContent) { try { const isValid = validator(result.structuredContent); if (!isValid) { throw new McpError( ErrorCode.InvalidParams, `Structured content does not match the tool's output schema: ${this._ajv.errorsText(validator.errors)}` ); } } catch (error) { if (error instanceof McpError) { throw error; } throw new McpError( ErrorCode.InvalidParams, `Failed to validate structured content: ${error instanceof Error ? error.message : String(error)}` ); } } } return result; }
Tool Validation Features:
- Output schema validation
- Structured content requirements
- Error classification and re-throwing
- AJV integration for JSON Schema validation
Server Error Handling
Elicitation Input Validation
Location: src/server/index.ts:313-349
async elicitInput( params: ElicitRequest["params"], options?: RequestOptions, ): Promise<ElicitResult> { const result = await this.request( { method: "elicitation/create", params }, ElicitResultSchema, options, ); if (result.action === "accept" && result.content) { try { const ajv = new Ajv(); const validate = ajv.compile(params.requestedSchema); const isValid = validate(result.content); if (!isValid) { throw new McpError( ErrorCode.InvalidParams, `Elicitation response content does not match requested schema: ${ajv.errorsText(validate.errors)}`, ); } } catch (error) { if (error instanceof McpError) { throw error; } throw new McpError( ErrorCode.InternalError, `Error validating elicitation response: ${error}`, ); } } return result; }
Validation Features:
- Schema-based content validation
- Error classification and wrapping
- JSON Schema integration
- Detailed error messages
Request Lifecycle Error Management
Request Timeout Management
Location: src/shared/protocol.ts:249-292
private _setupTimeout( messageId: number, timeout: number, maxTotalTimeout: number | undefined, onTimeout: () => void, resetTimeoutOnProgress: boolean = false ) { this._timeoutInfo.set(messageId, { timeoutId: setTimeout(onTimeout, timeout), startTime: Date.now(), timeout, maxTotalTimeout, resetTimeoutOnProgress, onTimeout }); } private _resetTimeout(messageId: number): boolean { const info = this._timeoutInfo.get(messageId); if (!info) return false; const totalElapsed = Date.now() - info.startTime; if (info.maxTotalTimeout && totalElapsed >= info.maxTotalTimeout) { this._timeoutInfo.delete(messageId); throw new McpError( ErrorCode.RequestTimeout, "Maximum total timeout exceeded", { maxTotalTimeout: info.maxTotalTimeout, totalElapsed } ); } clearTimeout(info.timeoutId); info.timeoutId = setTimeout(info.onTimeout, info.timeout); return true; }
Timeout Features:
- Per-request timeout tracking
- Progress-based timeout reset
- Maximum total timeout enforcement
- Automatic cleanup on completion/error
Request Cancellation
Location: src/shared/protocol.ts:582-601
const cancel = (reason: unknown) => { this._responseHandlers.delete(messageId); this._progressHandlers.delete(messageId); this._cleanupTimeout(messageId); this._transport ?.send({ jsonrpc: "2.0", method: "notifications/cancelled", params: { requestId: messageId, reason: String(reason), }, }) .catch((error) => this._onerror(new Error(`Failed to send cancellation: ${error}`)), ); reject(reason); };
Cancellation Features:
- Clean handler removal
- Timeout cleanup
- Cancellation notification to remote peer
- Promise rejection with reason
AbortSignal Integration
options?.signal?.addEventListener("abort", () => { cancel(options?.signal?.reason); });
Timeout and Cancellation
RequestOptions Timeout Configuration
export type RequestOptions = { timeout?: number; // Individual request timeout maxTotalTimeout?: number; // Maximum total time regardless of progress resetTimeoutOnProgress?: boolean; // Reset timeout on progress notifications signal?: AbortSignal; // External cancellation signal onprogress?: ProgressCallback; // Progress notification handler };
Progress-Based Timeout Reset
Location: src/shared/protocol.ts:451-474
private _onprogress(notification: ProgressNotification): void { const { progressToken, ...params } = notification.params; const messageId = Number(progressToken); const handler = this._progressHandlers.get(messageId); if (!handler) { this._onerror(new Error(`Received a progress notification for an unknown token: ${JSON.stringify(notification)}`)); return; } const responseHandler = this._responseHandlers.get(messageId); const timeoutInfo = this._timeoutInfo.get(messageId); if (timeoutInfo && responseHandler && timeoutInfo.resetTimeoutOnProgress) { try { this._resetTimeout(messageId); } catch (error) { responseHandler(error as Error); return; } } handler(params); }
Error Propagation and Recovery
Response Handler Error Processing
Location: src/shared/protocol.ts:476-502
private _onresponse(response: JSONRPCResponse | JSONRPCError): void { const messageId = Number(response.id); const handler = this._responseHandlers.get(messageId); if (handler === undefined) { this._onerror( new Error( `Received a response for an unknown message ID: ${JSON.stringify(response)}`, ), ); return; } this._responseHandlers.delete(messageId); this._progressHandlers.delete(messageId); this._cleanupTimeout(messageId); if (isJSONRPCResponse(response)) { handler(response); } else { const error = new McpError( response.error.code, response.error.message, response.error.data, ); handler(error); } }
Error Processing Features:
- Unknown message ID handling
- Automatic resource cleanup
- McpError construction from JSON-RPC errors
- Handler deregistration
Fallback Error Handlers
/** * A handler to invoke for any request types that do not have their own handler installed. */ fallbackRequestHandler?: ( request: JSONRPCRequest, extra: RequestHandlerExtra<SendRequestT, SendNotificationT> ) => Promise<SendResultT>; /** * A handler to invoke for any notification types that do not have their own handler installed. */ fallbackNotificationHandler?: (notification: Notification) => Promise<void>;
Best Practices
1. Use Specific Error Types
// Good: Specific error types throw new McpError(ErrorCode.InvalidParams, "Missing required field: name"); throw new InvalidClientError("Client authentication failed"); // Avoid: Generic errors throw new Error("Something went wrong");
2. Provide Meaningful Error Messages
// Good: Descriptive messages with context throw new McpError( ErrorCode.InvalidRequest, `Tool ${toolName} requires parameter '${paramName}' of type ${expectedType}`, { toolName, paramName, expectedType, receivedValue } ); // Avoid: Vague messages throw new McpError(ErrorCode.InvalidRequest, "Bad request");
3. Handle Errors at Appropriate Levels
// Application level - handle business logic errors try { const result = await client.callTool({ name: "calculator", arguments: { a: 1, b: 2 } }); } catch (error) { if (error instanceof McpError && error.code === ErrorCode.MethodNotFound) { console.log("Calculator tool not available, using fallback"); return fallbackCalculation(1, 2); } throw error; // Re-throw unexpected errors } // Transport level - handle connection errors client.onerror = (error) => { console.error("Transport error:", error); // Implement reconnection logic }; client.onclose = () => { console.log("Connection closed, attempting reconnect..."); // Implement reconnection strategy };
4. Implement Proper Cleanup
class MyClient { private client: Client; async connect() { try { await this.client.connect(transport); } catch (error) { // Cleanup on connection failure await this.client.close(); throw error; } } async shutdown() { // Always cleanup resources try { await this.client.close(); } catch (error) { console.error("Error during shutdown:", error); } } }
5. Use AbortSignal for Cancellation
const controller = new AbortController(); // Set timeout for user cancellation setTimeout(() => controller.abort("User cancelled"), 30000); try { const result = await client.callTool( { name: "longRunningTool", arguments: {} }, CallToolResultSchema, { signal: controller.signal } ); } catch (error) { if (error.name === 'AbortError') { console.log("Operation was cancelled"); } else { console.error("Operation failed:", error); } }
Examples
Complete Error Handling in Client Application
import { Client, McpError, ErrorCode, InvalidClientError, UnauthorizedError } from "@modelcontextprotocol/sdk"; class MCPClientWrapper { private client: Client; private reconnectAttempts = 0; private maxReconnectAttempts = 3; constructor() { this.client = new Client({ name: "MyApp", version: "1.0.0" }); this.setupErrorHandlers(); } private setupErrorHandlers() { this.client.onerror = (error) => { console.error("MCP Client Error:", error); if (error instanceof UnauthorizedError) { this.handleAuthenticationError(); } else if (error instanceof McpError) { this.handleMCPError(error); } else { this.handleGenericError(error); } }; this.client.onclose = () => { console.log("Connection closed"); this.attemptReconnect(); }; } private handleMCPError(error: McpError) { switch (error.code) { case ErrorCode.ConnectionClosed: console.log("Connection lost, will attempt to reconnect"); break; case ErrorCode.RequestTimeout: console.log("Request timed out, consider retrying"); break; case ErrorCode.MethodNotFound: console.log("Method not supported by server"); break; default: console.error(`MCP Error ${error.code}: ${error.message}`); } } private async handleAuthenticationError() { try { await this.refreshAuthentication(); } catch (error) { console.error("Failed to refresh authentication:", error); // Redirect to login or request new credentials } } private handleGenericError(error: Error) { console.error("Generic error:", error.message); // Log to error reporting service } private async attemptReconnect() { if (this.reconnectAttempts >= this.maxReconnectAttempts) { console.error("Max reconnection attempts reached"); return; } this.reconnectAttempts++; const delay = Math.pow(2, this.reconnectAttempts) * 1000; // Exponential backoff console.log(`Attempting reconnection ${this.reconnectAttempts}/${this.maxReconnectAttempts} in ${delay}ms`); setTimeout(async () => { try { await this.connect(); this.reconnectAttempts = 0; // Reset on successful connection } catch (error) { console.error("Reconnection failed:", error); this.attemptReconnect(); } }, delay); } async callToolSafely(toolName: string, args: any) { const maxRetries = 3; let attempt = 0; while (attempt < maxRetries) { try { const result = await this.client.callTool( { name: toolName, arguments: args }, CallToolResultSchema, { timeout: 30000, resetTimeoutOnProgress: true } ); if (result.isError) { throw new Error(`Tool error: ${result.content?.[0]?.text || 'Unknown error'}`); } return result; } catch (error) { attempt++; if (error instanceof McpError) { if (error.code === ErrorCode.RequestTimeout && attempt < maxRetries) { console.log(`Request timed out, retrying (${attempt}/${maxRetries})`); continue; } else if (error.code === ErrorCode.InvalidParams) { // Don't retry parameter errors throw error; } } if (attempt >= maxRetries) { throw error; } // Wait before retry await new Promise(resolve => setTimeout(resolve, 1000 * attempt)); } } } }
Server Error Handling with OAuth
import { Server, McpError, ErrorCode, OAuthError, InvalidTokenError, InsufficientScopeError } from "@modelcontextprotocol/sdk"; class MCPServerWrapper { private server: Server; constructor() { this.server = new Server( { name: "MyServer", version: "1.0.0" }, { capabilities: { tools: {}, resources: {} }, instructions: "A secure MCP server with OAuth" } ); this.setupRequestHandlers(); } private setupRequestHandlers() { // Tool call handler with authentication this.server.setRequestHandler(CallToolRequestSchema, async (request, extra) => { try { // Verify authentication if (!extra.authInfo?.valid) { throw new InvalidTokenError("Invalid or expired access token"); } // Check authorization scope const requiredScope = this.getRequiredScopeForTool(request.params.name); if (!this.hasScope(extra.authInfo.scopes, requiredScope)) { throw new InsufficientScopeError( `Tool '${request.params.name}' requires scope '${requiredScope}'` ); } // Execute tool const result = await this.executeTool(request.params.name, request.params.arguments); return { content: [{ type: "text", text: JSON.stringify(result) }], isError: false }; } catch (error) { // Convert OAuth errors to tool results if (error instanceof OAuthError) { return { content: [{ type: "text", text: `Authentication error: ${error.message}` }], isError: true }; } // Handle other errors if (error instanceof McpError) { throw error; // Re-throw MCP errors } // Wrap unknown errors throw new McpError( ErrorCode.InternalError, `Tool execution failed: ${error.message}`, { toolName: request.params.name, originalError: error.message } ); } }); // Resource handler with error handling this.server.setRequestHandler(ReadResourceRequestSchema, async (request, extra) => { try { const resource = await this.loadResource(request.params.uri); if (!resource) { throw new McpError( ErrorCode.InvalidParams, `Resource not found: ${request.params.uri}`, { uri: request.params.uri } ); } return { contents: [{ uri: request.params.uri, mimeType: resource.mimeType, text: resource.content }] }; } catch (error) { if (error instanceof McpError) { throw error; } throw new McpError( ErrorCode.InternalError, `Failed to read resource: ${error.message}`, { uri: request.params.uri } ); } }); } private getRequiredScopeForTool(toolName: string): string { const scopeMap = { 'read_files': 'files:read', 'write_files': 'files:write', 'execute_command': 'system:execute' }; return scopeMap[toolName] || 'basic'; } private hasScope(userScopes: string[], requiredScope: string): boolean { return userScopes.includes(requiredScope) || userScopes.includes('admin'); } private async executeTool(name: string, args: any) { // Tool implementation with proper error handling switch (name) { case 'calculator': if (typeof args.a !== 'number' || typeof args.b !== 'number') { throw new McpError( ErrorCode.InvalidParams, "Calculator requires numeric parameters 'a' and 'b'", { providedArgs: args } ); } return { result: args.a + args.b }; default: throw new McpError( ErrorCode.MethodNotFound, `Tool '${name}' not found`, { availableTools: ['calculator'] } ); } } private async loadResource(uri: string) { // Resource loading with error handling try { // Implementation would load actual resource return { content: "Resource content", mimeType: "text/plain" }; } catch (error) { // Re-throw as MCP error with context throw new McpError( ErrorCode.InternalError, `Failed to load resource from ${uri}: ${error.message}`, { uri, error: error.message } ); } } }
This comprehensive error handling documentation covers all major error scenarios in the MCP TypeScript SDK, from low-level transport errors to high-level application errors, providing developers with the knowledge and patterns needed to build robust MCP applications.
Top comments (0)