UNPKG

@boundless-oss/atlas

Version:

Atlas - MCP Server for comprehensive startup project management

494 lines (455 loc) • 13.8 kB
/** * Enhanced Error Handling Utility * Implements MCP Design Guide Section 5.1 principles for error visibility and self-correction */ export interface DetailedError { message: string; code: string; category: 'validation' | 'execution' | 'external' | 'system'; details: Record<string, any>; context?: Record<string, any>; recoverable: boolean; suggestedActions?: string[]; originalError?: Error; } export interface RetryConfig { maxAttempts: number; baseDelay: number; // milliseconds maxDelay: number; backoffMultiplier: number; retryableErrors: string[]; } export class MCPError extends Error { public readonly code: string; public readonly category: string; public readonly details: Record<string, any>; public readonly context?: Record<string, any>; public readonly recoverable: boolean; public readonly suggestedActions?: string[]; public readonly originalError?: Error; constructor(error: DetailedError) { super(error.message); this.name = 'MCPError'; this.code = error.code; this.category = error.category; this.details = error.details; this.context = error.context; this.recoverable = error.recoverable; this.suggestedActions = error.suggestedActions; this.originalError = error.originalError; } toMCPResponse() { return { content: [ { type: 'text', text: this.formatErrorMessage(), }, ], structured: { error: true, code: this.code, category: this.category, recoverable: this.recoverable, details: this.details, context: this.context, suggestedActions: this.suggestedActions, }, }; } private formatErrorMessage(): string { let message = `āŒ **Error**: ${this.message}\n\n`; message += `šŸ” **Code**: ${this.code}\n`; message += `šŸ“‹ **Category**: ${this.category}\n`; if (Object.keys(this.details).length > 0) { message += `šŸ“ **Details**:\n`; for (const [key, value] of Object.entries(this.details)) { message += ` • ${key}: ${JSON.stringify(value)}\n`; } } if (this.context && Object.keys(this.context).length > 0) { message += `šŸŽÆ **Context**:\n`; for (const [key, value] of Object.entries(this.context)) { message += ` • ${key}: ${JSON.stringify(value)}\n`; } } if (this.suggestedActions && this.suggestedActions.length > 0) { message += `\nšŸ’” **Suggested Actions**:\n`; this.suggestedActions.forEach((action, index) => { message += `${index + 1}. ${action}\n`; }); } if (this.recoverable) { message += `\nšŸ”„ This error is recoverable. You can retry the operation.`; } return message; } } export class ErrorHandler { private static readonly DEFAULT_RETRY_CONFIG: RetryConfig = { maxAttempts: 3, baseDelay: 1000, maxDelay: 10000, backoffMultiplier: 2, retryableErrors: ['NETWORK_ERROR', 'TIMEOUT', 'RATE_LIMIT', 'TEMPORARY_UNAVAILABLE'], }; /** * Wraps a function with comprehensive error handling and retry logic */ static async withErrorHandling<T>( operation: () => Promise<T>, context: { tool: string; module: string; params?: any }, retryConfig: Partial<RetryConfig> = {} ): Promise<T> { const config = { ...this.DEFAULT_RETRY_CONFIG, ...retryConfig }; let lastError: Error | undefined; for (let attempt = 1; attempt <= config.maxAttempts; attempt++) { try { return await operation(); } catch (error) { lastError = error as Error; const mcpError = this.transformError(error as Error, context); // Don't retry non-recoverable errors if (!mcpError.recoverable || attempt === config.maxAttempts) { throw mcpError; } // Don't retry errors that aren't in the retryable list if (!config.retryableErrors.includes(mcpError.code)) { throw mcpError; } // Calculate exponential backoff delay const delay = Math.min( config.baseDelay * Math.pow(config.backoffMultiplier, attempt - 1), config.maxDelay ); console.error(`[${context.module}:${context.tool}] Attempt ${attempt} failed, retrying in ${delay}ms:`, mcpError.message); await this.sleep(delay); } } // This should never be reached, but TypeScript requires it throw this.transformError(lastError!, context); } /** * Transform generic errors into detailed MCP errors with full visibility */ static transformError(error: Error, context: { tool: string; module: string; params?: any }): MCPError { // Network/HTTP errors if (error.message.includes('fetch') || error.message.includes('network')) { return new MCPError({ message: `Network connection failed: ${error.message}`, code: 'NETWORK_ERROR', category: 'external', details: { originalMessage: error.message, stack: error.stack, }, context, recoverable: true, suggestedActions: [ 'Check your internet connection', 'Verify the service endpoint is accessible', 'Try again in a few moments', ], originalError: error, }); } // Validation errors if (error.message.includes('required') || error.message.includes('invalid')) { return new MCPError({ message: `Input validation failed: ${error.message}`, code: 'VALIDATION_ERROR', category: 'validation', details: { originalMessage: error.message, providedParams: context.params, }, context, recoverable: true, suggestedActions: [ 'Check all required parameters are provided', 'Verify parameter types match the schema', 'Review the tool documentation for correct usage', ], originalError: error, }); } // File system errors if (error.message.includes('ENOENT') || error.message.includes('EACCES')) { return new MCPError({ message: `File system operation failed: ${error.message}`, code: 'FILE_SYSTEM_ERROR', category: 'system', details: { originalMessage: error.message, operation: context.tool, }, context, recoverable: false, suggestedActions: [ 'Check file paths are correct and accessible', 'Verify you have necessary permissions', 'Ensure the target directory exists', ], originalError: error, }); } // Rate limiting if (error.message.includes('rate limit') || error.message.includes('429')) { return new MCPError({ message: `Rate limit exceeded: ${error.message}`, code: 'RATE_LIMIT', category: 'external', details: { originalMessage: error.message, }, context, recoverable: true, suggestedActions: [ 'Wait before retrying the operation', 'Reduce the frequency of requests', 'Check API quota and limits', ], originalError: error, }); } // Authentication errors if (error.message.includes('unauthorized') || error.message.includes('401')) { return new MCPError({ message: `Authentication failed: ${error.message}`, code: 'AUTH_ERROR', category: 'external', details: { originalMessage: error.message, }, context, recoverable: false, suggestedActions: [ 'Check your credentials are correct', 'Verify API keys or tokens are valid', 'Ensure you have necessary permissions', ], originalError: error, }); } // Generic error fallback return new MCPError({ message: `Unexpected error in ${context.module}.${context.tool}: ${error.message}`, code: 'UNKNOWN_ERROR', category: 'system', details: { originalMessage: error.message, stack: error.stack, errorType: error.constructor.name, }, context, recoverable: false, suggestedActions: [ 'Review the error details for specific issues', 'Check system logs for additional information', 'Report this error if it persists', ], originalError: error, }); } /** * Create a validation error for schema violations */ static createValidationError( field: string, value: any, constraint: string, context: { tool: string; module: string } ): MCPError { return new MCPError({ message: `Parameter '${field}' violates constraint: ${constraint}`, code: 'PARAMETER_VALIDATION_ERROR', category: 'validation', details: { field, value, constraint, valueType: typeof value, }, context, recoverable: true, suggestedActions: [ `Ensure '${field}' meets the requirement: ${constraint}`, 'Check the tool documentation for valid parameter formats', 'Verify the parameter type matches expectations', ], }); } /** * Create a resource not found error */ static createNotFoundError( resourceType: string, identifier: string, context: { tool: string; module: string } ): MCPError { return new MCPError({ message: `${resourceType} not found: ${identifier}`, code: 'RESOURCE_NOT_FOUND', category: 'validation', details: { resourceType, identifier, }, context, recoverable: false, suggestedActions: [ `Verify the ${resourceType} ID '${identifier}' exists`, `Use list commands to find available ${resourceType}s`, 'Check for typos in the identifier', ], }); } /** * Create a dependency error for missing prerequisites */ static createDependencyError( dependency: string, context: { tool: string; module: string } ): MCPError { return new MCPError({ message: `Missing dependency: ${dependency}`, code: 'DEPENDENCY_ERROR', category: 'validation', details: { dependency, tool: context.tool, }, context, recoverable: true, suggestedActions: [ `Ensure ${dependency} is properly configured`, 'Check prerequisite setup steps', 'Initialize required components first', ], }); } private static sleep(ms: number): Promise<void> { return new Promise(resolve => setTimeout(resolve, ms)); } /** * Get detailed information about a specific error */ async getErrorDetails(errorId: string): Promise<any> { // This would normally fetch from a database return { errorId, type: 'validation', severity: 'medium', message: 'Sample error for development', timestamp: new Date().toISOString(), tool: 'unknown', context: {}, stackTrace: null, rootCause: null, suggestions: [], relatedErrors: [] }; } /** * Analyze patterns in errors */ async analyzeErrorPatterns(options: { timeRange?: string; errorTypes?: string[]; minOccurrences?: number; groupBy?: string; }): Promise<any> { return { totalErrors: 0, patternsFound: [], recommendations: [], insights: [] }; } /** * Get timeline of errors */ async getErrorTimeline(options: { timeRange?: string; toolName?: string; severity?: string; includeContext?: boolean; }): Promise<any[]> { return []; } /** * Generate error report */ async generateErrorReport(options: { reportType?: string; timeRange?: string; includeRecommendations?: boolean; outputFormat?: string; saveToFile?: boolean; }): Promise<any> { const content = options.outputFormat === 'markdown' ? '# Error Report\n\nNo errors to report.' : { errors: [], summary: 'No errors' }; return { content, summary: { totalIssues: 0, critical: 0, high: 0, medium: 0, low: 0 }, filePath: options.saveToFile ? '.atlas/reports/error-report.md' : undefined }; } /** * Track error resolution */ async resolveErrorSuggestion(options: { errorId: string; suggestionId: string; implementation?: string; effectiveness?: number; notes?: string; }): Promise<any> { return { errorId: options.errorId, suggestionId: options.suggestionId, implementation: options.implementation, effectiveness: options.effectiveness, notes: options.notes, outcome: { success: true, message: 'Tracked' } }; } /** * Simulate error recovery */ async simulateErrorRecovery(options: { errorType: string; severity?: string; context?: any; dryRun?: boolean; }): Promise<any> { return { errorType: options.errorType, severity: options.severity || 'medium', steps: [], results: { success: true, recoverable: true }, recoveryTime: '0ms', effectiveness: '100%' }; } } /** * Decorator for automatic error handling in tool functions */ export function withMCPErrorHandling(target: any, propertyKey: string, descriptor: PropertyDescriptor) { const originalMethod = descriptor.value; descriptor.value = async function (...args: any[]) { try { return await originalMethod.apply(this, args); } catch (error) { const context = { tool: propertyKey, module: target.constructor.name, params: args[0], }; throw ErrorHandler.transformError(error as Error, context); } }; return descriptor; }