UNPKG

marketing-post-generator-mcp

Version:

A powerful MCP server for AI-powered marketing blog post generation with Claude integration

384 lines 16.2 kB
import { ToolExecutionError, ContentGenerationError, ServiceUnavailableError, NetworkError, TimeoutError, AuthenticationError, AuthorizationError, ValidationError, ConfigurationError, } from './BaseError'; export class McpErrorWrapper { errorHandler; logger; config; constructor(errorHandler, logger, config) { this.errorHandler = errorHandler; this.logger = logger; this.config = { criticalTools: ['generate_content', 'save_post', 'publish_post'], criticalPrompts: ['content_generation', 'post_creation'], errorTypeMappings: { ENOTFOUND: 'NetworkError', ECONNREFUSED: 'NetworkError', ECONNRESET: 'NetworkError', ETIMEDOUT: 'TimeoutError', TIMEOUT: 'TimeoutError', UNAUTHORIZED: 'AuthenticationError', FORBIDDEN: 'AuthorizationError', VALIDATION_FAILED: 'ValidationError', INVALID_INPUT: 'ValidationError', CONFIG_ERROR: 'ConfigurationError', }, severityOverrides: {}, ...config, }; } async wrapToolExecution(toolFunction, context) { const startTime = Date.now(); try { this.logger.debug('Tool execution started', { toolName: context.toolName, arguments: context.arguments, requestId: context.requestId, timestamp: new Date().toISOString(), }); const result = await toolFunction(); const executionTime = Date.now() - startTime; this.logger.info('Tool execution completed', { toolName: context.toolName, executionTime, requestId: context.requestId, timestamp: new Date().toISOString(), }); return result; } catch (error) { const executionTime = Date.now() - startTime; // Convert to typed error first const typedError = error instanceof Error ? this.convertToTypedError(error, { ...context, executionTime }) : new Error(String(error)); // Create tool execution error const toolError = new ToolExecutionError(`Tool '${context.toolName}' execution failed: ${typedError.message}`, context.toolName, typedError); // Handle the error with context const handledError = this.errorHandler.handleError(toolError, { ...context, executionTime, operation: 'tool_execution', }, { severity: this.determineSeverity(typedError, context), shouldNotify: this.shouldNotifyForTool(context.toolName), }); throw handledError; } } async wrapPromptExecution(promptFunction, context) { const startTime = Date.now(); try { this.logger.debug('Prompt execution started', { promptName: context.promptName, arguments: context.arguments, requestId: context.requestId, timestamp: new Date().toISOString(), }); const result = await promptFunction(); const executionTime = Date.now() - startTime; this.logger.info('Prompt execution completed', { promptName: context.promptName, executionTime, requestId: context.requestId, timestamp: new Date().toISOString(), }); return result; } catch (error) { const executionTime = Date.now() - startTime; // Convert to typed error first const typedError = error instanceof Error ? this.convertToTypedError(error, { ...context, executionTime }) : new Error(String(error)); // Create content generation error for prompts const promptError = new ContentGenerationError(`Prompt '${context.promptName}' execution failed: ${typedError.message}`, { ...context, executionTime, }, typedError); // Handle the error with context const handledError = this.errorHandler.handleError(promptError, { ...context, executionTime, operation: 'prompt_execution', }, { severity: this.determineSeverity(typedError, context), shouldNotify: this.shouldNotifyForPrompt(context.promptName), }); throw handledError; } } async wrapWithRetry(operation, context, retryOptions = {}) { const { maxRetries = 3, baseDelay = 1000, maxDelay = 10000, shouldRetry = (error) => this.errorHandler.isRetryableError(error), } = retryOptions; let lastError = new Error('No attempts made'); for (let attempt = 1; attempt <= maxRetries + 1; attempt++) { try { if (attempt > 1) { this.logger.info('Retrying operation', { operationName: context.operationName, attempt, maxRetries: maxRetries + 1, requestId: context.requestId, }); } return await operation(); } catch (error) { lastError = error instanceof Error ? error : new Error(String(error)); if (attempt <= maxRetries && shouldRetry(lastError)) { const delay = Math.min(baseDelay * Math.pow(2, attempt - 1), maxDelay); this.logger.warn('Operation failed, retrying', { operationName: context.operationName, attempt, error: lastError.message, retryDelay: delay, requestId: context.requestId, }); await this.delay(delay); continue; } // Max retries reached or error is not retryable const finalError = new ServiceUnavailableError(`Operation '${context.operationName}' failed after ${attempt} attempts: ${lastError.message}`, { ...context, attempts: attempt, finalError: lastError.message, }, lastError); throw this.errorHandler.handleError(finalError, context, { severity: 'high', shouldNotify: attempt > maxRetries, }); } } throw lastError; } determineSeverity(error, context) { // Check for severity overrides based on context const contextKey = 'toolName' in context ? context.toolName : context.promptName; if (this.config.severityOverrides[contextKey]) { return this.config.severityOverrides[contextKey]; } if (error instanceof Error) { // Use proper error type checking instead of string matching const classification = this.classifyError(error); // Authentication/authorization errors are high severity if (classification.isAuthError) { return 'high'; } // Validation errors are low severity if (classification.isValidationError) { return 'low'; } // Network errors are medium severity by default if (classification.isNetworkError) { return 'medium'; } // Timeout errors are medium severity if (classification.isTimeoutError) { return 'medium'; } // Configuration errors are high severity if (classification.isConfigurationError) { return 'high'; } } // Critical tools/prompts should have higher severity if ('toolName' in context && this.config.criticalTools.includes(context.toolName)) { return 'high'; } if ('promptName' in context && this.config.criticalPrompts.includes(context.promptName)) { return 'high'; } return 'medium'; } shouldNotifyForTool(toolName) { return this.config.criticalTools.includes(toolName); } shouldNotifyForPrompt(promptName) { return this.config.criticalPrompts.includes(promptName); } classifyError(error) { // Check if error is already a specific BaseError type if (error instanceof NetworkError) { return { isNetworkError: true, isTimeoutError: false, isAuthError: false, isValidationError: false, isConfigurationError: false, originalError: error, }; } if (error instanceof TimeoutError) { return { isNetworkError: false, isTimeoutError: true, isAuthError: false, isValidationError: false, isConfigurationError: false, originalError: error, }; } if (error instanceof AuthenticationError || error instanceof AuthorizationError) { return { isNetworkError: false, isTimeoutError: false, isAuthError: true, isValidationError: false, isConfigurationError: false, originalError: error, }; } if (error instanceof ValidationError) { return { isNetworkError: false, isTimeoutError: false, isAuthError: false, isValidationError: true, isConfigurationError: false, originalError: error, }; } if (error instanceof ConfigurationError) { return { isNetworkError: false, isTimeoutError: false, isAuthError: false, isValidationError: false, isConfigurationError: true, originalError: error, }; } // For standard Error types, use error code and message patterns const errorCode = error.code || ''; const errorMessage = error.message.toLowerCase(); // Check error codes first (more reliable) for (const [code, errorType] of Object.entries(this.config.errorTypeMappings)) { if (errorCode === code) { return this.createClassificationFromType(errorType, error); } } // Fallback to message pattern matching (less reliable) const isNetworkError = /network|connection|dns|host|enotfound|econnrefused|econnreset/.test(errorMessage); const isTimeoutError = /timeout|etimedout/.test(errorMessage); const isAuthError = /auth|unauthorized|forbidden|permission|access denied|invalid.*token|expired.*token/.test(errorMessage); const isValidationError = /validation|invalid|malformed|bad request|schema/.test(errorMessage); const isConfigurationError = /config|configuration|setup|initialization|missing.*key|invalid.*setting/.test(errorMessage); return { isNetworkError, isTimeoutError, isAuthError, isValidationError, isConfigurationError, originalError: error, }; } createClassificationFromType(errorType, error) { const classification = { isNetworkError: false, isTimeoutError: false, isAuthError: false, isValidationError: false, isConfigurationError: false, originalError: error, }; switch (errorType) { case 'NetworkError': classification.isNetworkError = true; break; case 'TimeoutError': classification.isTimeoutError = true; break; case 'AuthenticationError': case 'AuthorizationError': classification.isAuthError = true; break; case 'ValidationError': classification.isValidationError = true; break; case 'ConfigurationError': classification.isConfigurationError = true; break; } return classification; } updateConfig(config) { this.config = { ...this.config, ...config }; } getConfig() { return { ...this.config }; } delay(ms) { return new Promise((resolve) => setTimeout(resolve, ms)); } /** * Convert a standard error to an appropriate BaseError type based on classification */ convertToTypedError(error, context) { // If it's already a BaseError, return as is if (error instanceof ToolExecutionError || error instanceof ContentGenerationError || error instanceof ServiceUnavailableError || error instanceof NetworkError || error instanceof TimeoutError || error instanceof AuthenticationError || error instanceof AuthorizationError || error instanceof ValidationError || error instanceof ConfigurationError) { return error; } const classification = this.classifyError(error); if (classification.isNetworkError) { return new NetworkError(error.message, context, error); } if (classification.isTimeoutError) { return new TimeoutError(error.message, context, error); } if (classification.isAuthError) { // Determine if it's authentication or authorization based on message if (error.message.toLowerCase().includes('unauthorized') || error.message.toLowerCase().includes('invalid') || error.message.toLowerCase().includes('expired')) { return new AuthenticationError(error.message, context, error); } return new AuthorizationError(error.message, context, error); } if (classification.isValidationError) { return new ValidationError(error.message, context, error); } if (classification.isConfigurationError) { return new ConfigurationError(error.message, context, error); } // Return the original error if no specific type matches return error; } } // Helper functions for easy usage export function createMcpErrorWrapper(errorHandler, logger, config) { return new McpErrorWrapper(errorHandler, logger, config); } // Generic decorator factory to reduce duplication function createErrorHandlingDecorator(errorWrapper, type, name) { return function (_target, _propertyKey, descriptor) { const originalMethod = descriptor.value; descriptor.value = async function (...args) { const context = { [type === 'tool' ? 'toolName' : 'promptName']: name, arguments: args.length > 0 ? args[0] : {}, requestId: this.requestId, userId: this.userId, }; const wrapMethod = type === 'tool' ? errorWrapper.wrapToolExecution.bind(errorWrapper) : errorWrapper.wrapPromptExecution.bind(errorWrapper); return wrapMethod(() => originalMethod.apply(this, args), context); }; return descriptor; }; } // Decorator function for tool methods export function withToolErrorHandling(errorWrapper, toolName) { return createErrorHandlingDecorator(errorWrapper, 'tool', toolName); } // Decorator function for prompt methods export function withPromptErrorHandling(errorWrapper, promptName) { return createErrorHandlingDecorator(errorWrapper, 'prompt', promptName); } //# sourceMappingURL=McpErrorWrapper.js.map