UNPKG

mcp-workflow-server-enhanced

Version:

Enhanced MCP Workflow Server with smart problem routing, comprehensive validation, guide compliance, and robust error handling. Intelligently routes to appropriate AI functions based on problem type.

331 lines (288 loc) 9.75 kB
import { WorkflowError, ValidationError, DomainError, GuideComplianceError, WorkflowContext } from './types.js'; import { rollbackToLastValidState } from './validation.js'; import { logWorkflowProgress } from './utils.js'; /** * Enhanced Error Handling and Recovery Framework * * This module provides comprehensive error handling, boundary checks, * and recovery mechanisms for the MCP workflow server. */ export interface ErrorRecoveryStrategy { name: string; description: string; canHandle: (error: Error) => boolean; recover: (error: Error, context: WorkflowContext) => Promise<WorkflowContext>; priority: number; // Higher number = higher priority } /** * Built-in recovery strategies */ export const RECOVERY_STRATEGIES: ErrorRecoveryStrategy[] = [ { name: 'validation-rollback', description: 'Rollback to last valid state for validation errors', canHandle: (error) => error instanceof ValidationError && error.recoverable, recover: async (error: ValidationError, context: WorkflowContext) => { const steps = ['improve-prompt', 'research', 'cognitive', 'planner', 'task-generation', 'implementation']; const currentIndex = steps.indexOf(context.currentStep); if (currentIndex > 0) { const previousStep = steps[currentIndex - 1]; return await rollbackToLastValidState(context, previousStep); } throw new Error('Cannot rollback from first step'); }, priority: 8, }, { name: 'guide-compliance-retry', description: 'Retry with enhanced guide compliance focus', canHandle: (error) => error instanceof GuideComplianceError && error.complianceScore > 30, recover: async (error: GuideComplianceError, context: WorkflowContext) => { // Add guide compliance metadata to force better compliance const enhancedContext = { ...context, metadata: { ...context.metadata, forceGuideCompliance: true, targetComplianceScore: 80, failedMaxims: error.failedMaxims, retryReason: 'guide-compliance-improvement', }, }; return enhancedContext; }, priority: 7, }, { name: 'domain-detection-fallback', description: 'Fallback to generic analysis for domain detection failures', canHandle: (error) => error instanceof DomainError && error.errorType === 'detection', recover: async (error: DomainError, context: WorkflowContext) => { const fallbackContext = { ...context, metadata: { ...context.metadata, forcedDomain: 'generic', domainDetectionFailed: true, originalDomain: error.domain, fallbackReason: 'domain-detection-failure', }, }; return fallbackContext; }, priority: 6, }, { name: 'input-sanitization', description: 'Sanitize and retry with cleaned input', canHandle: (error) => error instanceof ValidationError && error.step !== undefined, recover: async (error: ValidationError, context: WorkflowContext) => { // Sanitize the input that caused the validation error const sanitizedContext = { ...context, metadata: { ...context.metadata, inputSanitized: true, sanitizationReason: error.message, }, }; return sanitizedContext; }, priority: 5, }, { name: 'step-skip-recovery', description: 'Skip problematic step and continue with degraded functionality', canHandle: (error) => error instanceof WorkflowError && error.recoverable, recover: async (error: WorkflowError, context: WorkflowContext) => { const steps = ['improve-prompt', 'research', 'cognitive', 'planner', 'task-generation', 'implementation']; const currentIndex = steps.indexOf(error.step); if (currentIndex < steps.length - 1) { const nextStep = steps[currentIndex + 1]; const skippedContext = { ...context, currentStep: nextStep as any, metadata: { ...context.metadata, skippedSteps: [...(context.metadata?.skippedSteps || []), error.step], skipReason: error.message, degradedMode: true, }, }; return skippedContext; } throw new Error('Cannot skip final step'); }, priority: 3, }, ]; /** * Enhanced error handler with recovery mechanisms */ export class EnhancedErrorHandler { private recoveryStrategies: ErrorRecoveryStrategy[]; constructor(customStrategies: ErrorRecoveryStrategy[] = []) { this.recoveryStrategies = [...RECOVERY_STRATEGIES, ...customStrategies] .sort((a, b) => b.priority - a.priority); } /** * Handle error with automatic recovery attempt */ async handleError( error: Error, context: WorkflowContext, stepName: string ): Promise<{ recovered: boolean; context?: WorkflowContext; strategy?: string }> { logWorkflowProgress(context, stepName, `Error occurred: ${error.message}`); // Find applicable recovery strategy for (const strategy of this.recoveryStrategies) { if (strategy.canHandle(error)) { try { logWorkflowProgress(context, stepName, `Attempting recovery with strategy: ${strategy.name}` ); const recoveredContext = await strategy.recover(error, context); logWorkflowProgress(recoveredContext, stepName, `Recovery successful with strategy: ${strategy.name}` ); return { recovered: true, context: recoveredContext, strategy: strategy.name, }; } catch (recoveryError) { logWorkflowProgress(context, stepName, `Recovery strategy ${strategy.name} failed: ${recoveryError.message}` ); continue; } } } logWorkflowProgress(context, stepName, 'No recovery strategy available'); return { recovered: false }; } /** * Validate input with boundary checks */ validateInput(input: any, stepName: string): { valid: boolean; errors: string[] } { const errors: string[] = []; // Basic boundary checks if (input === null || input === undefined) { errors.push(`${stepName}: Input cannot be null or undefined`); } // Check for common injection patterns if (typeof input === 'string') { const dangerousPatterns = [ /<script/i, /javascript:/i, /on\w+\s*=/i, /eval\s*\(/i, ]; for (const pattern of dangerousPatterns) { if (pattern.test(input)) { errors.push(`${stepName}: Potentially dangerous input detected`); break; } } } // Check object depth to prevent stack overflow if (typeof input === 'object' && input !== null) { const maxDepth = 10; if (this.getObjectDepth(input) > maxDepth) { errors.push(`${stepName}: Input object too deeply nested (max depth: ${maxDepth})`); } } return { valid: errors.length === 0, errors, }; } /** * Sanitize input to prevent common issues */ sanitizeInput(input: any, stepName: string): any { if (typeof input === 'string') { // Remove potentially dangerous content return input .replace(/<script[^>]*>.*?<\/script>/gi, '') .replace(/javascript:/gi, '') .replace(/on\w+\s*=/gi, '') .trim(); } if (typeof input === 'object' && input !== null) { // Limit object depth return this.limitObjectDepth(input, 5); } return input; } /** * Get object nesting depth */ private getObjectDepth(obj: any, depth = 0): number { if (depth > 20) return depth; // Prevent infinite recursion if (typeof obj !== 'object' || obj === null) { return depth; } let maxDepth = depth; for (const key in obj) { if (obj.hasOwnProperty(key)) { const childDepth = this.getObjectDepth(obj[key], depth + 1); maxDepth = Math.max(maxDepth, childDepth); } } return maxDepth; } /** * Limit object nesting depth */ private limitObjectDepth(obj: any, maxDepth: number, currentDepth = 0): any { if (currentDepth >= maxDepth) { return '[Object: max depth exceeded]'; } if (typeof obj !== 'object' || obj === null) { return obj; } if (Array.isArray(obj)) { return obj.map(item => this.limitObjectDepth(item, maxDepth, currentDepth + 1)); } const result: any = {}; for (const key in obj) { if (obj.hasOwnProperty(key)) { result[key] = this.limitObjectDepth(obj[key], maxDepth, currentDepth + 1); } } return result; } } /** * Global error handler instance */ export const globalErrorHandler = new EnhancedErrorHandler(); /** * Utility function to wrap async operations with error handling */ export async function withErrorHandling<T>( operation: () => Promise<T>, context: WorkflowContext, stepName: string, errorHandler: EnhancedErrorHandler = globalErrorHandler ): Promise<T> { try { return await operation(); } catch (error) { const recovery = await errorHandler.handleError(error as Error, context, stepName); if (recovery.recovered && recovery.context) { // Retry the operation with recovered context logWorkflowProgress(recovery.context, stepName, 'Retrying operation after recovery'); return await operation(); } // Re-throw if recovery failed throw error; } }