Skip to main content
Glama
error-handling.ts16.6 kB
/** * Comprehensive Error Handling System for DevOps AI Toolkit * * Provides centralized error handling, logging, and context management * with support for MCP protocol, CLI operations, and core functionality. */ import { McpError, ErrorCode } from '@modelcontextprotocol/sdk/types.js'; /** * Error categories for systematic error classification */ export enum ErrorCategory { // Infrastructure errors KUBERNETES = 'kubernetes', NETWORK = 'network', AUTHENTICATION = 'authentication', AUTHORIZATION = 'authorization', // Application errors VALIDATION = 'validation', CONFIGURATION = 'configuration', OPERATION = 'operation', // External service errors AI_SERVICE = 'ai_service', STORAGE = 'storage', // Protocol errors MCP_PROTOCOL = 'mcp_protocol', CLI_INTERFACE = 'cli_interface', // System errors INTERNAL = 'internal', UNKNOWN = 'unknown' } /** * Error severity levels */ export enum ErrorSeverity { LOW = 'low', // Non-critical, operation can continue MEDIUM = 'medium', // Important but recoverable HIGH = 'high', // Significant impact, requires attention CRITICAL = 'critical' // System-threatening, immediate action required } /** * Error context interface for comprehensive error tracking */ export interface ErrorContext { // Operation details operation: string; component: string; // User context userId?: string; sessionId?: string; requestId?: string; // Technical context timestamp: Date; version: string; // Input context input?: any; parameters?: Record<string, any>; // Stack trace and debugging originalError?: Error; stackTrace?: string; // Recovery information suggestedActions?: string[]; isRetryable?: boolean; retryCount?: number; } /** * Structured error class that extends native Error */ export class AppError extends Error { // Core identification public readonly id: string; public readonly code: string; public readonly category: ErrorCategory; public readonly severity: ErrorSeverity; // User-facing information public readonly userMessage?: string; public readonly technicalDetails?: string; // Context and debugging public readonly context: ErrorContext; // Timing public readonly timestamp: Date; // Recovery guidance public readonly suggestedActions: string[]; public readonly isRetryable: boolean; // Chaining public readonly cause?: AppError; constructor( id: string, code: string, category: ErrorCategory, severity: ErrorSeverity, message: string, context: ErrorContext, timestamp: Date, suggestedActions: string[], isRetryable: boolean, userMessage?: string, technicalDetails?: string, cause?: AppError ) { super(message); this.name = 'AppError'; this.id = id; this.code = code; this.category = category; this.severity = severity; this.context = context; this.timestamp = timestamp; this.suggestedActions = suggestedActions; this.isRetryable = isRetryable; this.userMessage = userMessage; this.technicalDetails = technicalDetails; this.cause = cause; } } /** * Log levels for structured logging */ export enum LogLevel { DEBUG = 'debug', INFO = 'info', WARN = 'warn', ERROR = 'error', FATAL = 'fatal' } /** * Structured log entry interface */ export interface LogEntry { level: LogLevel; timestamp: Date; message: string; component: string; operation?: string; requestId?: string; sessionId?: string; data?: any; error?: AppError; duration?: number; } /** * Logger interface for dependency injection */ export interface Logger { debug(message: string, data?: any): void; info(message: string, data?: any): void; warn(message: string, data?: any): void; error(message: string, error?: Error | AppError, data?: any): void; fatal(message: string, error?: Error | AppError, data?: any): void; } /** * Default console logger implementation */ export class ConsoleLogger implements Logger { private component: string; private minLevel: LogLevel; constructor(component: string, minLevel: LogLevel = LogLevel.INFO) { this.component = component; this.minLevel = minLevel; } private shouldLog(level: LogLevel): boolean { const levels = [LogLevel.DEBUG, LogLevel.INFO, LogLevel.WARN, LogLevel.ERROR, LogLevel.FATAL]; return levels.indexOf(level) >= levels.indexOf(this.minLevel); } private formatMessage(level: LogLevel, message: string, data?: any): string { const timestamp = new Date().toISOString(); const baseMessage = `[${timestamp}] ${level.toUpperCase()} [${this.component}] ${message}`; if (data) { return `${baseMessage} ${JSON.stringify(data, null, 2)}`; } return baseMessage; } debug(message: string, data?: any): void { if (this.shouldLog(LogLevel.DEBUG)) { console.debug(this.formatMessage(LogLevel.DEBUG, message, data)); } } info(message: string, data?: any): void { if (this.shouldLog(LogLevel.INFO)) { console.info(this.formatMessage(LogLevel.INFO, message, data)); } } warn(message: string, data?: any): void { if (this.shouldLog(LogLevel.WARN)) { console.warn(this.formatMessage(LogLevel.WARN, message, data)); } } error(message: string, error?: Error | AppError, data?: any): void { if (this.shouldLog(LogLevel.ERROR)) { const errorData = error ? { error: this.serializeError(error), ...data } : data; console.error(this.formatMessage(LogLevel.ERROR, message, errorData)); } } fatal(message: string, error?: Error | AppError, data?: any): void { if (this.shouldLog(LogLevel.FATAL)) { const errorData = error ? { error: this.serializeError(error), ...data } : data; console.error(this.formatMessage(LogLevel.FATAL, message, errorData)); } } private serializeError(error: Error | AppError): any { if ('category' in error) { // AppError return { id: error.id, code: error.code, category: error.category, severity: error.severity, message: error.message, context: error.context }; } else { // Native Error return { name: error.name, message: error.message, stack: error.stack }; } } } /** * Error handler factory and utilities */ export class ErrorHandler { private static requestIdCounter = 0; private static logger: Logger = new ConsoleLogger('ErrorHandler'); /** * Set custom logger implementation */ static setLogger(logger: Logger): void { this.logger = logger; } /** * Generate unique request ID */ static generateRequestId(): string { return `req_${Date.now()}_${++this.requestIdCounter}`; } /** * Create comprehensive AppError from various error sources */ static createError( category: ErrorCategory, severity: ErrorSeverity, message: string, context: Partial<ErrorContext>, originalError?: Error ): AppError { const errorId = `err_${Date.now()}_${Math.random().toString(36).substr(2, 9)}`; const timestamp = new Date(); const fullContext: ErrorContext = { operation: context.operation || 'unknown', component: context.component || 'unknown', timestamp, version: process.env.npm_package_version || '0.1.0', originalError, stackTrace: originalError?.stack || new Error().stack, isRetryable: context.isRetryable || false, retryCount: context.retryCount || 0, ...context }; const suggestedActions = context.suggestedActions || this.getDefaultSuggestedActions(category); const appError = new AppError( errorId, this.generateErrorCode(category, severity), category, severity, message, fullContext, timestamp, suggestedActions, fullContext.isRetryable || false, this.getUserFriendlyMessage(category), originalError?.message, undefined // Don't wrap the original error to prevent circular references ); // Log the error this.logger.error(`Error created: ${message}`, appError, { category, severity, operation: fullContext.operation, component: fullContext.component }); return appError; } /** * Convert AppError to McpError for MCP protocol */ static toMcpError(appError: AppError): McpError { const errorCode = this.mapToMcpErrorCode(appError.category); const message = `${appError.message}${appError.technicalDetails ? ` - ${appError.technicalDetails}` : ''}`; return new McpError(errorCode, message); } /** * Handle error with automatic logging and context enhancement */ static handleError( error: Error | AppError, context: Partial<ErrorContext>, options: { rethrow?: boolean; convertToMcp?: boolean; logLevel?: LogLevel; } = {} ): AppError | McpError { let appError: AppError; if ('category' in error) { // Already an AppError appError = error; } else { // Convert native Error to AppError appError = this.createError( this.categorizeError(error), this.assessSeverity(error), error.message, context, error ); } // Log the handled error const logLevel = options.logLevel || LogLevel.ERROR; this.logger[logLevel](`Error handled in ${context.component || 'unknown'}`, appError); if (options.convertToMcp) { const mcpError = this.toMcpError(appError); if (options.rethrow) { throw mcpError; } return mcpError; } if (options.rethrow) { throw appError; } return appError; } /** * Wrap operation with error handling */ static async withErrorHandling<T>( operation: () => Promise<T>, context: Partial<ErrorContext>, options: { retryCount?: number; convertToMcp?: boolean; } = {} ): Promise<T> { const maxRetries = options.retryCount || 0; let lastError: Error; for (let attempt = 0; attempt <= maxRetries; attempt++) { try { this.logger.debug(`Executing operation: ${context.operation}`, { attempt: attempt + 1, maxRetries: maxRetries + 1 }); return await operation(); } catch (error) { lastError = error as Error; const enhancedContext = { ...context, retryCount: attempt, isRetryable: attempt < maxRetries }; const appError = this.handleError(lastError, enhancedContext, { logLevel: attempt < maxRetries ? LogLevel.WARN : LogLevel.ERROR }) as AppError; // Retry if we haven't exceeded max retries and the error is retryable // For retry logic, we consider errors retryable by default unless explicitly marked as not retryable const shouldRetry = attempt < maxRetries && (appError.isRetryable || enhancedContext.isRetryable); if (shouldRetry) { this.logger.info(`Retrying operation: ${context.operation}`, { attempt: attempt + 1, maxRetries: maxRetries + 1, reason: appError.message }); continue; } // Final attempt failed or not retryable if (options.convertToMcp) { throw this.toMcpError(appError); } throw appError; } } // This should never be reached, but TypeScript requires it throw lastError!; } private static generateErrorCode(category: ErrorCategory, severity: ErrorSeverity): string { const categoryCode = category.toUpperCase().replace('_', ''); const severityCode = severity.charAt(0).toUpperCase(); const timestamp = Date.now().toString().slice(-6); const random = Math.random().toString(36).substring(2, 5); return `${categoryCode}_${severityCode}_${timestamp}_${random}`; } private static mapToMcpErrorCode(category: ErrorCategory): ErrorCode { switch (category) { case ErrorCategory.VALIDATION: return ErrorCode.InvalidParams; case ErrorCategory.AUTHENTICATION: case ErrorCategory.AUTHORIZATION: return ErrorCode.InvalidParams; case ErrorCategory.MCP_PROTOCOL: return ErrorCode.MethodNotFound; case ErrorCategory.OPERATION: case ErrorCategory.CLI_INTERFACE: return ErrorCode.InvalidRequest; default: return ErrorCode.InternalError; } } private static categorizeError(error: Error): ErrorCategory { const message = error.message.toLowerCase(); if (message.includes('kubeconfig') || message.includes('kubernetes')) { return ErrorCategory.KUBERNETES; } if (message.includes('network') || message.includes('connection')) { return ErrorCategory.NETWORK; } if (message.includes('authentication') || message.includes('unauthorized')) { return ErrorCategory.AUTHENTICATION; } if (message.includes('ai') || message.includes('api key invalid') || message.includes('model')) { return ErrorCategory.AI_SERVICE; } if (message.includes('validation') || message.includes('invalid')) { return ErrorCategory.VALIDATION; } return ErrorCategory.UNKNOWN; } private static assessSeverity(error: Error): ErrorSeverity { const message = error.message.toLowerCase(); if (message.includes('critical') || message.includes('fatal')) { return ErrorSeverity.CRITICAL; } if (message.includes('authentication') || message.includes('authorization')) { return ErrorSeverity.HIGH; } if (message.includes('validation') || message.includes('invalid')) { return ErrorSeverity.MEDIUM; } return ErrorSeverity.LOW; } private static getUserFriendlyMessage(category: ErrorCategory): string { switch (category) { case ErrorCategory.KUBERNETES: return 'Unable to connect to Kubernetes cluster. Please check your kubeconfig and cluster connectivity.'; case ErrorCategory.AUTHENTICATION: return 'Authentication failed. Please verify your credentials.'; case ErrorCategory.VALIDATION: return 'Input validation failed. Please check your parameters and try again.'; case ErrorCategory.AI_SERVICE: return 'AI service is temporarily unavailable. Please try again later.'; default: return 'An unexpected error occurred. Please try again or contact support.'; } } private static getDefaultSuggestedActions(category: ErrorCategory): string[] { switch (category) { case ErrorCategory.KUBERNETES: return [ 'Verify kubeconfig file exists and is valid', 'Check cluster connectivity with kubectl cluster-info', 'Ensure proper authentication credentials' ]; case ErrorCategory.VALIDATION: return [ 'Review input parameters for correct format', 'Check required fields are provided', 'Verify data types match expected schema' ]; case ErrorCategory.AI_SERVICE: return [ 'Check AI provider API key environment variable is set', 'Verify API key is valid and has sufficient credits', 'Try again after a short delay' ]; default: return [ 'Try the operation again', 'Check system logs for more details', 'Contact support if problem persists' ]; } } private static wrapNativeError(error: Error): AppError { const errorId = `err_${Date.now()}_${Math.random().toString(36).substr(2, 9)}`; const timestamp = new Date(); const category = this.categorizeError(error); const severity = this.assessSeverity(error); const context: ErrorContext = { operation: 'error_wrapping', component: 'ErrorHandler', timestamp, version: process.env.npm_package_version || '0.1.0', originalError: error, stackTrace: error.stack, isRetryable: false, retryCount: 0 }; const appError = new AppError( errorId, this.generateErrorCode(category, severity), category, severity, error.message, context, timestamp, this.getDefaultSuggestedActions(category), false, this.getUserFriendlyMessage(category), error.message, undefined // No cause to prevent circular reference ); return appError; } }

Latest Blog Posts

MCP directory API

We provide all the information about MCP servers via our MCP API.

curl -X GET 'https://glama.ai/api/mcp/v1/servers/vfarcic/dot-ai'

If you have feedback or need assistance with the MCP directory API, please join our Discord server