@boundless-oss/atlas
Version:
Atlas - MCP Server for comprehensive startup project management
494 lines (455 loc) ⢠13.8 kB
text/typescript
/**
* 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;
}