@simonecoelhosfo/optimizely-mcp-server
Version:
Optimizely MCP Server for AI assistants with integrated CLI tools
647 lines • 29.8 kB
JavaScript
/**
* 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