UNPKG

@simonecoelhosfo/optimizely-mcp-server

Version:

Optimizely MCP Server for AI assistants with integrated CLI tools

382 lines (379 loc) 16.3 kB
/** * Validation Error Transformer * * Transforms cryptic API validation errors into clear, actionable guidance for AI agents. * This ensures that AI agents never see raw ZOD or JSON schema validation errors. * * @author Optimizely MCP Server * @version 1.0.0 */ import { getLogger } from '../logging/Logger.js'; export class ValidationErrorTransformer { logger = getLogger(); /** * Transform any validation error into user-friendly guidance */ transformValidationError(errorMessage, context) { this.logger.info({ errorMessage, context }, 'ValidationErrorTransformer: Transforming validation error'); // Check for specific patterns in the error message if (this.isFlagAbTestError(errorMessage, context)) { return this.transformFlagAbTestError(errorMessage, context); } if (this.isRulesetValidationError(errorMessage, context)) { return this.transformRulesetError(errorMessage, context); } if (this.isVariationValidationError(errorMessage, context)) { return this.transformVariationError(errorMessage, context); } if (this.isSchemaValidationError(errorMessage)) { return this.transformSchemaError(errorMessage, context); } if (this.isMissingFieldError(errorMessage)) { return this.transformMissingFieldError(errorMessage, context); } if (this.isTypeError(errorMessage)) { return this.transformTypeError(errorMessage, context); } // Default transformation return this.createDefaultTransformation(errorMessage, context); } /** * Check if this is a ruleset validation error */ isRulesetValidationError(message, context) { return context.entityType === 'ruleset' || message.includes('ruleset') || message.includes('rule') || message.includes('variations') || message.includes('traffic_allocation'); } /** * Transform ruleset-specific validation errors */ transformRulesetError(message, context) { // Analyze the payload to understand what the agent was trying to do const payload = context.payload; // Check if agent is trying to add A/B test with wrong structure if (payload && Array.isArray(payload)) { const hasVariations = payload.some(op => op.value && op.value.variations && Array.isArray(op.value.variations)); if (hasVariations) { return { userMessage: "It looks like you're trying to add an A/B test to a Feature Flag. The structure you're using is incorrect for Feature Experimentation.", technicalDetails: message, suggestedFix: "For Feature Experimentation flags, you need to:\n1. First create variations using manage_entity_lifecycle with entity_type='variation'\n2. Then update the ruleset to reference those variations by key\n3. Use percentage_included instead of traffic_allocation", examplePayload: { operation: "update", entity_type: "ruleset", project_id: context.projectId, entity_data: [{ op: "add", path: "/rules/0", value: { key: "experiment_rule", enabled: true, audience_conditions: "everyone", percentage_included: 10000, variations: [ { key: "control", weight: 5000 }, { key: "treatment", weight: 5000 } ] } }] }, documentationLink: "resource://feature-experimentation-ab-test-guide" }; } } // Check for other common ruleset errors if (message.includes('traffic_allocation')) { return { userMessage: "Feature Experimentation doesn't use 'traffic_allocation'. Use 'percentage_included' for the rule and 'weight' for each variation.", technicalDetails: message, suggestedFix: "Replace 'traffic_allocation' with variation weights. Each variation should have a 'weight' field (in basis points, totaling 10000).", examplePayload: { variations: [ { key: "control", weight: 5000 }, { key: "treatment", weight: 5000 } ] } }; } if (message.includes('single_experiment')) { return { userMessage: "The rule type 'single_experiment' is not valid for Feature Experimentation. Use 'a/b' or 'targeted_delivery'.", technicalDetails: message, suggestedFix: "Change the rule type to 'a/b' for experiments or 'targeted_delivery' for rollouts.", examplePayload: { type: "a/b", key: "experiment_rule", enabled: true } }; } return this.createDefaultRulesetGuide(message, context); } /** * Check if this is a flag update with ab_test structure */ isFlagAbTestError(message, context) { return context.entityType === 'flag' && context.operation === 'update' && (message.includes('ab_test') || context.payload?.ab_test || context.payload?.entity_data?.ab_test); } /** * Transform flag ab_test errors */ transformFlagAbTestError(message, context) { return { userMessage: "You cannot update a Feature Flag with an 'ab_test' structure. Feature Experimentation uses a different approach.", technicalDetails: message, suggestedFix: `Feature Experimentation workflow for A/B tests: 1. Create variations separately: manage_entity_lifecycle( operation="create", entity_type="variation", entity_data={ key: "control_variation", name: "Control", weight: 5000 }, options={flag_key: "${context.payload?.entity_id || 'your_flag_key'}"} ) 2. Update the ruleset (not the flag): manage_entity_lifecycle( operation="update", entity_type="ruleset", project_id="${context.projectId}", entity_data=[{ op: "add", path: "/rules/0", value: { key: "ab_test_rule", enabled: true, audience_conditions: "everyone", percentage_included: 10000, variations: [ {key: "control_variation", weight: 5000}, {key: "treatment_variation", weight: 5000} ] } }], options={ flag_key: "${context.payload?.entity_id || 'your_flag_key'}", environment_key: "development" } ) Key differences: - Flags don't have 'ab_test' fields - Variations are separate entities (not embedded) - A/B tests are rules in the ruleset - Metrics are added to experiments, not flags`, documentationLink: "resource://feature-experimentation-ab-test-guide" }; } /** * Check if this is a variation validation error */ isVariationValidationError(message, context) { return context.entityType === 'variation' || message.includes('variation') || message.includes('weight') || message.includes('flag_key'); } /** * Transform variation-specific validation errors */ transformVariationError(message, context) { if (message.includes('flag_key')) { return { userMessage: "When creating variations, the flag_key should be in the options parameter, not in entity_data.", technicalDetails: message, suggestedFix: "Move flag_key from entity_data to options parameter", examplePayload: { operation: "create", entity_type: "variation", project_id: context.projectId, entity_data: { key: "treatment_variation", name: "Treatment", weight: 5000 }, options: { flag_key: "your_flag_key" } } }; } if (message.includes('weight') && context.payload?.platform === 'feature') { return { userMessage: "Weight is required for Feature Experimentation variations. It must be in basis points (10000 = 100%).", technicalDetails: message, suggestedFix: "Add a 'weight' field to each variation. For equal distribution among N variations, use weight = 10000/N.", examplePayload: { key: "variation_key", name: "Variation Name", weight: 5000 // 50% for 2 variations } }; } return this.createDefaultVariationGuide(message, context); } /** * Check if this is a JSON schema validation error */ isSchemaValidationError(message) { return message.includes('schema') || message.includes('JSON') || message.includes('oneOf') || message.includes('additionalProperties') || message.includes('type should be') || message.includes('Expected') || message.includes('Received'); } /** * Transform schema validation errors */ transformSchemaError(message, context) { // Extract field name if possible const fieldMatch = message.match(/["']([^"']+)["']/); const fieldName = fieldMatch ? fieldMatch[1] : 'unknown field'; // Extract expected vs received if present const expectedMatch = message.match(/Expected\s+(\w+)/i); const receivedMatch = message.match(/Received\s+(\w+)/i); let suggestedFix = `Check the data type and structure for '${fieldName}'.`; if (expectedMatch && receivedMatch) { suggestedFix = `The field '${fieldName}' should be ${expectedMatch[1]} but you provided ${receivedMatch[1]}.`; } return { userMessage: `Invalid data structure for ${context.entityType}. ${suggestedFix}`, technicalDetails: message, suggestedFix: suggestedFix, documentationLink: `resource://entity-documentation?entity=${context.entityType}` }; } /** * Check if this is a missing required field error */ isMissingFieldError(message) { return message.includes('required') || message.includes('missing') || message.includes('must have') || message.includes('undefined'); } /** * Transform missing field errors */ transformMissingFieldError(message, context) { // Try to extract the missing field name const fieldMatch = message.match(/["']([^"']+)["']|field\s+(\w+)|property\s+(\w+)/i); const fieldName = fieldMatch ? (fieldMatch[1] || fieldMatch[2] || fieldMatch[3]) : 'unknown field'; return { userMessage: `Missing required field '${fieldName}' for ${context.entityType}.`, technicalDetails: message, suggestedFix: `Add the '${fieldName}' field to your payload. Use get_entity_documentation to see all required fields.`, documentationLink: `resource://entity-documentation?entity=${context.entityType}&aspect=fields` }; } /** * Check if this is a type mismatch error */ isTypeError(message) { return message.includes('type') || message.includes('string') || message.includes('number') || message.includes('boolean') || message.includes('array') || message.includes('object'); } /** * Transform type errors */ transformTypeError(message, context) { return { userMessage: "Data type mismatch in your payload.", technicalDetails: message, suggestedFix: "Check that all fields have the correct data type. Common issues:\n- IDs should be numbers, not strings\n- Conditions should be JSON strings, not objects\n- Weights should be numbers (basis points)", documentationLink: `resource://entity-documentation?entity=${context.entityType}&aspect=validation` }; } /** * Create default transformation for unknown errors */ createDefaultTransformation(message, context) { return { userMessage: `Validation error for ${context.entityType} ${context.operation}. The provided data doesn't match the expected format.`, technicalDetails: message, suggestedFix: `Use get_entity_documentation with entity_type='${context.entityType}' to see the correct format, or use template mode for complex operations.`, documentationLink: `resource://entity-documentation?entity=${context.entityType}` }; } /** * Create default ruleset guidance */ createDefaultRulesetGuide(message, context) { return { userMessage: "Invalid ruleset update structure for Feature Experimentation.", technicalDetails: message, suggestedFix: "For Feature Experimentation rulesets:\n1. Create variations first using entity_type='variation'\n2. Update ruleset with JSON Patch operations\n3. Reference variations by key, not ID\n4. Use percentage_included for traffic, not traffic_allocation", examplePayload: [{ op: "add", path: "/rules/0", value: { key: "experiment_rule", enabled: true, audience_conditions: "everyone", percentage_included: 10000, variations: [ { key: "existing_variation_key", weight: 5000 }, { key: "another_variation_key", weight: 5000 } ] } }], documentationLink: "resource://feature-experimentation-ab-test-guide" }; } /** * Create default variation guidance */ createDefaultVariationGuide(message, context) { return { userMessage: "Invalid variation structure.", technicalDetails: message, suggestedFix: "For creating variations:\n1. Put flag_key in options, not entity_data\n2. Include weight field (basis points)\n3. Provide unique key and descriptive name", examplePayload: { operation: "create", entity_type: "variation", entity_data: { key: "treatment_v1", name: "Treatment Variation", weight: 5000 }, options: { flag_key: "your_flag_key" } } }; } /** * Check if error message contains patterns that should trigger template mode */ shouldSuggestTemplateMode(message, context) { const complexPatterns = [ 'multiple validation errors', 'complex structure', 'nested', 'array of', 'one of', 'schema' ]; return complexPatterns.some(pattern => message.toLowerCase().includes(pattern)) || context.operation === 'create' && ['experiment', 'campaign', 'flag'].includes(context.entityType); } } // Export singleton instance export const validationErrorTransformer = new ValidationErrorTransformer(); //# sourceMappingURL=ValidationErrorTransformer.js.map