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
text/typescript
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;
}
}