@simonecoelhosfo/optimizely-mcp-server
Version:
Optimizely MCP Server for AI assistants with integrated CLI tools
382 lines (379 loc) • 16.3 kB
JavaScript
/**
* 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