UNPKG

@simonecoelhosfo/optimizely-mcp-server

Version:

Optimizely MCP Server for AI assistants with integrated CLI tools

647 lines 29.8 kB
/** * MCP Error Mapping Utilities * @description Provides standardized error mapping from business logic errors * to MCP-compliant error types with appropriate error codes and messaging * * This module ensures consistent error handling across the entire codebase * while maintaining MCP protocol compliance and providing clear error context * for debugging and client error handling. * * @author Optimizely MCP Server * @version 1.0.0 */ import { ErrorCode, McpError } from '@modelcontextprotocol/sdk/types.js'; import { OptimizelyAPIError } from '../api/OptimizelyAPIHelper.js'; import { ConfigValidationError } from '../config/ConfigManager.js'; import { getLogger } from '../logging/Logger.js'; import { automatedErrorEnhancer } from './AutomatedPrescriptiveErrorEnhancer.js'; import { HardStopError, HardStopErrorType } from './HardStopError.js'; import { validationErrorTransformer } from './ValidationErrorTransformer.js'; /** * MCP Error Categories for internal classification */ export var MCPErrorCategory; (function (MCPErrorCategory) { /** Invalid input parameters or validation failures */ MCPErrorCategory["VALIDATION"] = "validation"; /** Authentication or authorization failures */ MCPErrorCategory["AUTHENTICATION"] = "authentication"; /** Resource not found or access denied */ MCPErrorCategory["NOT_FOUND"] = "not_found"; /** Rate limiting or quota exceeded */ MCPErrorCategory["RATE_LIMIT"] = "rate_limit"; /** External service or network failures */ MCPErrorCategory["EXTERNAL_SERVICE"] = "external_service"; /** Database or storage failures */ MCPErrorCategory["STORAGE"] = "storage"; /** Configuration or setup issues */ MCPErrorCategory["CONFIGURATION"] = "configuration"; /** Unexpected internal errors */ MCPErrorCategory["INTERNAL"] = "internal"; })(MCPErrorCategory || (MCPErrorCategory = {})); /** * MCP Error Mapper class for converting various error types to MCP errors * @description Provides centralized error handling and mapping logic */ export class MCPErrorMapper { /** * Creates a hard stop error for unrecoverable scenarios * @param code - Error type code * @param reason - Specific reason for the error * @param message - User-friendly error message * @param httpStatus - HTTP status code (default 409) * @returns HardStopError instance */ static createHardStopError(code, reason, message, httpStatus = 409) { return new HardStopError(code, reason, message, "ASK_USER", httpStatus); } /** * Detects if an error should trigger a hard stop * @param error - The error to analyze * @param context - Error context * @returns True if error requires hard stop */ static shouldHardStop(error, context) { const errorMessage = error?.message || String(error); // Check for patterns that require hard stop if (errorMessage.includes('already exists') || errorMessage.includes('already in use')) { return true; } if (errorMessage.includes('FOREIGN KEY constraint')) { return true; } if (errorMessage.includes('Required entity not found') && context?.operation === 'create') { return true; } // CRITICAL: Custom attribute validation failures are hard stops if (errorMessage.includes('Custom attribute(s) do not exist') || errorMessage.includes('Custom attribute') && errorMessage.includes('does not exist')) { return true; } // Zod validation errors are hard stops if (error.name === 'ZodError' || (error.issues && Array.isArray(error.issues))) { return true; } // Entity type mismatches (e.g., creating flags in Web projects) if (errorMessage.includes('not supported in') || errorMessage.includes('only available for') || errorMessage.includes('platform mismatch')) { return true; } // Database schema errors if (errorMessage.includes('no such column') || errorMessage.includes('table') && errorMessage.includes('has no column')) { return true; } // Project type ambiguity if ((errorMessage.includes('project type') || errorMessage.includes('Project type')) && (errorMessage.includes('ambiguous') || errorMessage.includes('not specified'))) { return true; } // CRITICAL: 405 Method Not Allowed errors are hard stops if (error instanceof OptimizelyAPIError && error.status === 405) { return true; } if (errorMessage.includes('405') && errorMessage.includes('Method Not Allowed')) { return true; } if (error instanceof OptimizelyAPIError && error.status === 409) { return true; } return false; } /** * Converts any error to appropriate MCP error type with context * @param error - The original error to convert * @param context - Additional context information * @returns MCP-compliant error with appropriate error code */ static toMCPError(error, context) { // Normalize context to ErrorContext object const errorContext = typeof context === 'string' ? { operation: context } : context || {}; // Check if this is already a HardStopError if (error instanceof HardStopError) { return this.createMCPError(ErrorCode.InvalidRequest, error.message, { ...errorContext, hardStop: error.toResponse() }, MCPErrorCategory.VALIDATION); } // CRITICAL: Check if this error should trigger a hard stop if (this.shouldHardStop(error, errorContext)) { const errorMessage = error?.message || String(error); // Create appropriate HardStopError based on the error pattern let hardStopError; if (error.name === 'ZodError' || (error.issues && Array.isArray(error.issues))) { // Format Zod errors into readable message let zodMessage = 'Validation failed:\n'; if (error.issues) { error.issues.forEach((issue) => { const path = issue.path.join('.'); zodMessage += `- ${path}: ${issue.message}\n`; }); } else { zodMessage = errorMessage; } hardStopError = this.createHardStopError(HardStopErrorType.ZOD_VALIDATION_ERROR, 'VALIDATION_FAILED', zodMessage, 400); } else if (errorMessage.includes('Custom attribute(s) do not exist') || (errorMessage.includes('Custom attribute') && errorMessage.includes('does not exist'))) { hardStopError = this.createHardStopError(HardStopErrorType.REQUIRED_REFERENCE_MISSING, 'CUSTOM_ATTRIBUTE_NOT_FOUND', errorMessage, 400); } else if ((error instanceof OptimizelyAPIError && error.status === 405) || (errorMessage.includes('405') && errorMessage.includes('Method Not Allowed'))) { // Extract entity type from context if available const entityType = errorContext.metadata?.entityType || 'this entity'; const operation = errorContext.operation?.toLowerCase() || 'this operation'; hardStopError = this.createHardStopError(HardStopErrorType.OPERATION_NOT_SUPPORTED, 'METHOD_NOT_ALLOWED', `❌ OPERATION NOT SUPPORTED: The Optimizely API does not support ${operation} for ${entityType}.\n\n` + `This is a limitation of the Optimizely API, not the MCP server.\n` + `Available alternatives:\n` + `• Use 'archive' instead of 'delete' for most entities\n` + `• Update the entity with archived: true\n` + `• Check API documentation for supported operations`, 405); } else if (errorMessage.includes('already exists') || errorMessage.includes('already in use')) { hardStopError = this.createHardStopError(HardStopErrorType.ENTITY_ALREADY_EXISTS, 'ENTITY_KEY_CONFLICT', errorMessage, 409); } else if (errorMessage.includes('FOREIGN KEY constraint')) { hardStopError = this.createHardStopError(HardStopErrorType.FOREIGN_KEY_CONSTRAINT, 'FK_CONSTRAINT_VIOLATION', errorMessage, 409); } else if (errorMessage.includes('not supported in') || errorMessage.includes('only available for') || errorMessage.includes('platform mismatch')) { hardStopError = this.createHardStopError(HardStopErrorType.ENTITY_TYPE_MISMATCH, 'PLATFORM_MISMATCH', errorMessage + '\n\nYou are trying to create an entity that is not supported by this project type.', 400); } else if (errorMessage.includes('no such column') || (errorMessage.includes('table') && errorMessage.includes('has no column'))) { hardStopError = this.createHardStopError(HardStopErrorType.DATABASE_SCHEMA_ERROR, 'DATABASE_COLUMN_MISSING', errorMessage + '\n\nThis appears to be a database schema issue. The MCP server may need to be updated.', 500); } else if (errorMessage.includes('project type') && (errorMessage.includes('ambiguous') || errorMessage.includes('not specified'))) { hardStopError = this.createHardStopError(HardStopErrorType.PROJECT_TYPE_AMBIGUOUS, 'PROJECT_TYPE_REQUIRED', errorMessage + '\n\nPlease specify whether this is a Feature Experimentation or Web Experimentation project.', 400); } else { hardStopError = this.createHardStopError(HardStopErrorType.DATABASE_CONSTRAINT_VIOLATION, 'DB_CONSTRAINT_VIOLATION', errorMessage, 409); } return this.createMCPError(ErrorCode.InvalidRequest, hardStopError.message, { ...errorContext, hardStop: hardStopError.toResponse() }, MCPErrorCategory.VALIDATION); } // If already an MCP error, enhance with context and return if (error instanceof McpError) { return this.enhanceMCPError(error, errorContext); } // Map specific error types if (error instanceof OptimizelyAPIError) { return this.mapOptimizelyAPIError(error, errorContext); } if (error instanceof ConfigValidationError) { return this.mapConfigValidationError(error, errorContext); } // Handle specific error patterns by message const errorMessage = error?.message || String(error); if (this.isValidationError(errorMessage)) { return this.createMCPError(ErrorCode.InvalidParams, errorMessage, errorContext, MCPErrorCategory.VALIDATION); } if (this.isAuthenticationError(errorMessage)) { return this.createMCPError(ErrorCode.InvalidParams, errorMessage, errorContext, MCPErrorCategory.AUTHENTICATION); } if (this.isNotFoundError(errorMessage)) { return this.createMCPError(ErrorCode.InvalidParams, errorMessage, errorContext, MCPErrorCategory.NOT_FOUND); } if (this.isRateLimitError(errorMessage)) { return this.createMCPError(ErrorCode.InternalError, errorMessage, errorContext, MCPErrorCategory.RATE_LIMIT); } if (this.isStorageError(errorMessage)) { return this.createMCPError(ErrorCode.InternalError, errorMessage, errorContext, MCPErrorCategory.STORAGE); } // Default to internal error for unclassified errors return this.createMCPError(ErrorCode.InternalError, errorMessage, errorContext, MCPErrorCategory.INTERNAL); } /** * Maps Optimizely API errors to appropriate MCP error codes * @param error - OptimizelyAPIError instance * @param context - Error context * @returns Mapped MCP error * @private */ static mapOptimizelyAPIError(error, context) { const status = error.status; const baseMessage = error.message; // Map HTTP status codes to MCP error codes switch (true) { case status === 400: // Transform validation errors into user-friendly guidance if (this.isValidationError(baseMessage)) { const validationContext = { entityType: context?.metadata?.entityType || 'unknown', operation: context?.operation || 'unknown', projectId: context?.metadata?.projectId, payload: context?.metadata?.payload, apiEndpoint: context?.metadata?.apiEndpoint }; const transformed = validationErrorTransformer.transformValidationError(baseMessage, validationContext); return this.createMCPError(ErrorCode.InvalidParams, transformed.userMessage, { ...context, metadata: { ...context?.metadata, validationGuidance: { suggestedFix: transformed.suggestedFix, examplePayload: transformed.examplePayload, documentationLink: transformed.documentationLink, technicalDetails: transformed.technicalDetails } } }, MCPErrorCategory.VALIDATION); } return this.createMCPError(ErrorCode.InvalidParams, `Bad request: ${baseMessage}`, context, MCPErrorCategory.VALIDATION); case status === 401: return this.createMCPError(ErrorCode.InvalidParams, `Authentication failed: ${baseMessage}`, context, MCPErrorCategory.AUTHENTICATION); case status === 403: return this.createMCPError(ErrorCode.InvalidParams, `Access denied: ${baseMessage}`, context, MCPErrorCategory.AUTHENTICATION); case status === 404: return this.createMCPError(ErrorCode.InvalidParams, `Resource not found: ${baseMessage}`, context, MCPErrorCategory.NOT_FOUND); case status === 422: // Also handle 422 validation errors if (this.isValidationError(baseMessage)) { const validationContext = { entityType: context?.metadata?.entityType || 'unknown', operation: context?.operation || 'unknown', projectId: context?.metadata?.projectId, payload: context?.metadata?.payload, apiEndpoint: context?.metadata?.apiEndpoint }; const transformed = validationErrorTransformer.transformValidationError(baseMessage, validationContext); return this.createMCPError(ErrorCode.InvalidParams, transformed.userMessage, { ...context, metadata: { ...context?.metadata, validationGuidance: { suggestedFix: transformed.suggestedFix, examplePayload: transformed.examplePayload, documentationLink: transformed.documentationLink, technicalDetails: transformed.technicalDetails } } }, MCPErrorCategory.VALIDATION); } return this.createMCPError(ErrorCode.InvalidParams, `Validation failed: ${baseMessage}`, context, MCPErrorCategory.VALIDATION); case status === 429: return this.createMCPError(ErrorCode.InternalError, `Rate limit exceeded: ${baseMessage}`, context, MCPErrorCategory.RATE_LIMIT); case status && status >= 500: return this.createMCPError(ErrorCode.InternalError, `Optimizely service error: ${baseMessage}`, context, MCPErrorCategory.EXTERNAL_SERVICE); default: return this.createMCPError(ErrorCode.InternalError, `API error: ${baseMessage}`, context, MCPErrorCategory.EXTERNAL_SERVICE); } } /** * Maps configuration validation errors to MCP errors * @param error - ConfigValidationError instance * @param context - Error context * @returns Mapped MCP error * @private */ static mapConfigValidationError(error, context) { return this.createMCPError(ErrorCode.InvalidParams, `Configuration error: ${error.message}`, { ...context, metadata: { field: error.field, expected: error.expected } }, MCPErrorCategory.CONFIGURATION); } /** * Check if an error message indicates a validation error * @param message - Error message to check * @returns True if this appears to be a validation error * @private */ static isValidationError(message) { const validationPatterns = [ 'validation', 'schema', 'invalid', 'required', 'missing', 'type', 'Expected', 'Received', 'oneOf', 'allOf', 'additionalProperties', 'pattern', 'format', 'minimum', 'maximum', 'minLength', 'maxLength', 'enum', 'const', 'ZodError', 'must be', 'should be', 'is not', 'does not match', 'failed to parse', 'unexpected property', 'unknown property' ]; const lowerMessage = message.toLowerCase(); return validationPatterns.some(pattern => lowerMessage.includes(pattern.toLowerCase())); } /** * Creates a standardized MCP error with enhanced context * @param code - MCP error code * @param message - Error message * @param context - Error context * @param category - Error category for internal classification * @returns Created MCP error * @private */ static createMCPError(code, message, context, category) { // Format message with context let formattedMessage = message; if (context.operation) { formattedMessage = `${context.operation}: ${message}`; } // Log error for debugging getLogger().error({ errorCode: code, category, operation: context.operation, toolName: context.toolName, resourceUri: context.resourceUri, metadata: context.metadata, originalMessage: message }, 'Error mapped to MCP error type'); const mcpError = new McpError(code, formattedMessage); // Add custom properties for internal use (if supported by MCP SDK) try { mcpError._category = category; mcpError._context = context; } catch { // Ignore if MCP SDK doesn't support custom properties } return mcpError; } /** * Enhances an existing MCP error with additional context * @param error - Existing MCP error * @param context - Additional context to add * @returns Enhanced MCP error * @private */ static enhanceMCPError(error, context) { if (context.operation) { // Create new error with enhanced message if context provided const enhancedMessage = `${context.operation}: ${error.message}`; return new McpError(error.code, enhancedMessage); } return error; } static isAuthenticationError(message) { const patterns = [ /unauthorized/i, /authentication.*failed/i, /invalid.*token/i, /access.*denied/i, /permission.*denied/i, /forbidden/i ]; return patterns.some(pattern => pattern.test(message)); } static isNotFoundError(message) { const patterns = [ /not.*found/i, /does.*not.*exist/i, /unknown.*project/i, /unknown.*flag/i, /unknown.*experiment/i, /unknown.*campaign/i, /unknown.*page/i, /unknown.*audience/i, /unknown.*attribute/i, /unknown.*event/i, /entity.*not.*found/i, /resource.*not.*found/i ]; return patterns.some(pattern => pattern.test(message)); } static isRateLimitError(message) { const patterns = [ /rate.*limit/i, /too.*many.*requests/i, /quota.*exceeded/i, /throttled/i ]; return patterns.some(pattern => pattern.test(message)); } static isStorageError(message) { const patterns = [ /database.*error/i, /sqlite.*error/i, /storage.*failed/i, /connection.*failed/i, /query.*failed/i ]; return patterns.some(pattern => pattern.test(message)); } } /** * Convenience functions for common error creation patterns */ export class MCPErrorUtils { /** * Creates a validation error for invalid tool parameters * @param toolName - Name of the tool * @param message - Validation error message * @param field - Field that failed validation * @returns MCP error for invalid parameters */ static invalidToolParameter(toolName, message, field) { return MCPErrorMapper.toMCPError(new Error(message), { operation: `Tool ${toolName} parameter validation`, toolName, metadata: { field } }); } /** * Creates an error for unknown tools * @param toolName - Name of the unknown tool * @returns MCP error for unknown method */ static unknownTool(toolName) { return new McpError(ErrorCode.MethodNotFound, `Unknown tool: ${toolName}`); } /** * Creates an error for unknown resources * @param resourceUri - URI of the unknown resource * @returns MCP error for unknown resource */ static unknownResource(resourceUri) { return new McpError(ErrorCode.InvalidParams, `Unknown resource: ${resourceUri}`); } /** * Creates an error for tool execution timeouts * @param toolName - Name of the tool that timed out * @param timeoutMs - Timeout value in milliseconds * @returns MCP error for execution timeout */ static toolTimeout(toolName, timeoutMs) { return MCPErrorMapper.toMCPError(new Error(`Tool execution timed out after ${timeoutMs}ms`), { operation: `Tool ${toolName} execution`, toolName, metadata: { timeoutMs } }); } /** * Creates a prescriptive validation error with intelligent guidance * @param originalError - The validation error message * @param context - Context for error enhancement * @returns Enhanced MCP error with prescriptive guidance */ static validationError(originalError, context) { return createPrescriptiveError(new Error(originalError), context); } /** * Creates an error for resource content size limits * @param resourceUri - URI of the oversized resource * @param size - Actual size in bytes * @param limit - Size limit in bytes * @returns MCP error for oversized content */ static resourceTooLarge(resourceUri, size, limit) { return MCPErrorMapper.toMCPError(new Error(`Resource content too large: ${size} bytes exceeds limit of ${limit} bytes`), { operation: 'Resource content access', resourceUri, metadata: { size, limit } }); } /** * Creates a clear "entity not found" error for AI agents * @param entityType - Type of entity (project, flag, experiment, etc.) * @param entityId - ID or key of the entity that wasn't found * @param projectId - Optional project ID for context * @returns MCP error with clear not found message */ static entityNotFound(entityType, entityId, projectId) { const contextMessage = projectId ? ` in project ${projectId}` : ''; const message = `ENTITY NOT FOUND: ${entityType} '${entityId}' does not exist${contextMessage}. This is not a parameter error - the ${entityType} ID/key you provided is invalid or the entity has been deleted.`; return MCPErrorMapper.toMCPError(new Error(message), { operation: `Get ${entityType} details`, metadata: { entityType, entityId, projectId, errorType: 'ENTITY_NOT_FOUND', hint: `Verify the ${entityType} ID/key exists and you have access to it. Use list_entities to see available ${entityType}s.` } }); } /** * Creates a clear "project not found" error for AI agents * @param projectId - Project ID that wasn't found * @returns MCP error with clear project not found message */ static projectNotFound(projectId) { const message = `PROJECT NOT FOUND: Project '${projectId}' does not exist. This is not a parameter error - the project ID you provided is invalid or you don't have access to it.`; return MCPErrorMapper.toMCPError(new Error(message), { operation: 'Get project details', metadata: { projectId, errorType: 'PROJECT_NOT_FOUND', hint: 'Use list_projects to see available projects. Verify the project ID is correct and you have access to it.' } }); } /** * Creates a clear "invalid parameters" error for AI agents * @param toolName - Name of the tool * @param missingParams - Array of missing parameter names * @param providedParams - Array of provided parameter names * @returns MCP error with clear parameter validation message */ static invalidParameters(toolName, missingParams, providedParams) { const message = `PARAMETER ERROR: Tool '${toolName}' is missing required parameters: ${missingParams.join(', ')}. You provided: ${providedParams.join(', ') || 'none'}. This is a parameter error, not an entity not found error.`; return MCPErrorMapper.toMCPError(new Error(message), { operation: `Tool ${toolName} parameter validation`, toolName, metadata: { missingParams, providedParams, errorType: 'PARAMETER_ERROR', hint: `Provide the required parameters: ${missingParams.join(', ')}` } }); } /** * Creates a prescriptive entity validation error * @param entityType - Type of entity being validated * @param originalError - The validation error message * @param context - Additional context for enhancement * @returns Enhanced MCP error with prescriptive guidance */ static entityValidationError(entityType, originalError, context = {}) { return createPrescriptiveError(new Error(originalError), { entityType, operation: 'create', ...context }); } } /** * Enhanced error creation with prescriptive guidance * @param error - Original error to enhance * @param context - Additional context for enhancement * @returns Enhanced MCP error with prescriptive guidance */ export function createPrescriptiveError(error, context = {}) { const errorMessage = error?.message || String(error); // Get prescriptive enhancement const enhancement = automatedErrorEnhancer.enhanceError(errorMessage, context); // Create MCP error with enhanced guidance const enhancedMessage = `${enhancement.error.message}\n\nPrescriptive Guidance: ${JSON.stringify(enhancement, null, 2)}`; const mcpError = new McpError(ErrorCode.InvalidParams, enhancedMessage); // Attach enhancement data for programmatic access try { mcpError._prescriptiveGuidance = enhancement; mcpError._originalError = errorMessage; } catch { // Ignore if MCP SDK doesn't support custom properties } return mcpError; } /** * Type guard to check if an error is an MCP error * @param error - Error to check * @returns True if error is an MCP error */ export function isMCPError(error) { return error instanceof McpError; } /** * Extract error category from MCP error (if available) * @param error - MCP error to analyze * @returns Error category or undefined */ export function getMCPErrorCategory(error) { return error._category; } /** * Extract error context from MCP error (if available) * @param error - MCP error to analyze * @returns Error context or undefined */ export function getMCPErrorContext(error) { return error._context; } /** * Extract prescriptive guidance from enhanced MCP error (if available) * @param error - Enhanced MCP error to analyze * @returns Prescriptive guidance or undefined */ export function getPrescriptiveGuidance(error) { return error._prescriptiveGuidance; } /** * Extract original error message from enhanced MCP error (if available) * @param error - Enhanced MCP error to analyze * @returns Original error message or undefined */ export function getOriginalErrorMessage(error) { return error._originalError; } //# sourceMappingURL=MCPErrorMapping.js.map