@simonecoelhosfo/optimizely-mcp-server
Version:
Optimizely MCP Server for AI assistants with integrated CLI tools
469 lines (393 loc) • 18.8 kB
JavaScript
/**
* Dependency Validator for Optimizely Entities
*
* This validator ensures that entity dependencies are satisfied before operations proceed.
* It provides prescriptive guidance when dependencies are missing, following the operational
* sequencing patterns defined in cursor agent rules.
*
* Key Dependencies:
* - Audiences require Attributes to exist first
* - Metrics require Events to exist first
* - Experiments require Metrics (which require Events)
* - Flags may require Variations and Variables
*/
// CRITICAL: SafeIdConverter prevents scientific notation in large IDs
// DO NOT REMOVE OR REPLACE WITH toString() - This caused duplicate records bug
// Evidence: 5.02479302872269e+15 vs 5024793028722690 for same entity
// Fixed: January 26, 2025 - See CONTEXT-RECOVERY-V2.md for details
import { safeIdToString } from '../utils/SafeIdConverter.js';
export class DependencyValidator {
static dependencies = [
// AUDIENCE DEPENDENCIES
{
entityType: 'audience',
field: 'conditions',
dependsOn: 'attribute',
extractKey: (conditions) => {
const attributeKeys = [];
try {
const parsed = JSON.parse(conditions);
// Recursively find all attribute references
const findAttributes = (obj) => {
if (Array.isArray(obj)) {
obj.forEach(findAttributes);
}
else if (obj && typeof obj === 'object') {
if (obj.type === 'attribute' && obj.name) {
attributeKeys.push(obj.name);
}
Object.values(obj).forEach(findAttributes);
}
};
findAttributes(parsed);
}
catch (e) {
// Invalid JSON, will be caught by other validation
}
return attributeKeys;
},
errorMessage: (key) => `Custom attribute '${key}' does not exist`,
prescriptiveGuidance: (key, projectId) => `
DEPENDENCY ERROR: Audience condition references attribute '${key}' incorrectly.
PRESCRIPTIVE STEPS TO RESOLVE:
1. **LIST existing attributes to find the correct reference:**
\`list_entities entity_type=attribute project_id=${projectId}\`
2. **CHECK the attribute details:**
- The 'name' field is what you use in conditions (e.g., "Region")
- The 'key' field is for API references (not used in conditions)
- The 'id' field is the numeric ID
3. **USE the correct format in conditions:**
For Web Experimentation audiences:
\`"conditions": "[\\\"and\\\", {\\\"type\\\": \\\"custom_attribute\\\", \\\"name\\\": \\\"ATTRIBUTE_NAME\\\", \\\"value\\\": \\\"YOUR_VALUE\\\"}]"\`
Note: Use the NAME field from the attribute, not the key!
4. **COMPLETE WORKING EXAMPLE:**
\`manage_entity_lifecycle operation=create entity_type=audience project_id=${projectId} entity_data='{"name": "Audience Name", "description": "Description", "conditions": "[\\\\"and\\\\", {\\\\"type\\\\": \\\\"custom_attribute\\\\", \\\\"name\\\\": \\\\"Region\\\\", \\\\"value\\\\": \\\\"Italy 7\\\\"}]", "project_id": ${projectId}}'\`
TIP: KEY INSIGHT: Web Experimentation uses the attribute "name" field (e.g., "Region"), not the "key" field (e.g., "region")
WARNING: COMMON MISTAKES:
- Using attribute KEY instead of NAME field
- Wrong escaping of JSON in conditions string
- Missing project_id in the request body
`
},
{
entityType: 'metric',
field: 'event_id',
dependsOn: 'event',
// CRITICAL: Use SafeIdConverter to prevent scientific notation
extractKey: (eventId) => safeIdToString(eventId),
errorMessage: (key) => `Event with ID '${key}' does not exist`,
prescriptiveGuidance: (key, projectId) => `
DEPENDENCY ERROR: Event must exist before creating a metric.
PRESCRIPTIVE STEPS TO RESOLVE:
1. **LIST available events:**
\`list_entities entity_type=event project_id=${projectId}\`
2. **IF EVENT MISSING, create it first:**
\`manage_entity_lifecycle operation=create entity_type=event project_id=${projectId} entity_data='{"key": "conversion_event", "name": "Conversion Event", "project_id": ${projectId}}'\`
3. **THEN create metric using the event ID from response**
TIP: OPERATIONAL SEQUENCE: Create Event → Get Event ID → Create Metric
NOTE: NOTE: One event can have multiple metrics (total conversions, unique conversions, revenue, etc.)
`
},
{
entityType: 'experiment',
field: 'metrics',
dependsOn: 'event',
extractKey: (metrics) => {
if (!Array.isArray(metrics))
return [];
return metrics
.filter(m => m.event_id)
// CRITICAL: Use SafeIdConverter to prevent scientific notation
.map(m => safeIdToString(m.event_id));
},
errorMessage: (key) => `Metric references event '${key}' which does not exist`,
prescriptiveGuidance: (key, projectId) => `
DEPENDENCY ERROR: Events must exist before adding metrics to experiments.
PRESCRIPTIVE STEPS TO RESOLVE:
1. **CREATE the missing event:**
\`manage_entity_lifecycle operation=create entity_type=event project_id=${projectId} entity_data='{"key": "goal_event", "name": "Goal Event", "project_id": ${projectId}}'\`
2. **NOTE the event ID from response**
3. **CREATE metric configuration:**
\`{
"event_id": <event_id_from_step_2>,
"aggregator": "count",
"winning_direction": "increasing"
}\`
4. **ADD metric to experiment**
TIP: OPERATIONAL SEQUENCE: Create Events → Configure Metrics → Create Experiment
NOTE: TIP: Common metric types:
- Total conversions: aggregator="count"
- Unique conversions: aggregator="unique"
- Revenue: aggregator="sum" with event_properties
`
},
// VARIATION DEPENDENCIES
{
entityType: 'variation',
field: 'variables',
dependsOn: 'variable_definition',
extractKey: (variables) => {
return Object.keys(variables || {});
},
errorMessage: (key) => `Variable '${key}' is not defined in flag schema`,
prescriptiveGuidance: (key, projectId) => `
SCHEMA COMPLIANCE ERROR: Variation must match flag's variable definitions.
PRESCRIPTIVE STEPS TO RESOLVE:
1. **GET flag's variable definitions:**
\`list_entities entity_type=variable_definition project_id=${projectId} filters={"flag_key": "YOUR_FLAG_KEY"}\`
2. **VERIFY all variables are included:**
- Your variation MUST have values for ALL defined variables
- Each value must match the defined type (string, boolean, integer, double, json)
3. **FIX missing or incorrect variables:**
- Add missing variable: "${key}"
- Check type matches definition
4. **RETRY with complete variables object**
TIP: KEY INSIGHT: Variations are instances of the schema defined by variable definitions.
WARNING: COMMON MISTAKES:
- Missing required variables
- Wrong data types (e.g., string "true" instead of boolean true)
- Extra variables not in schema
`
},
// PAGE DEPENDENCIES
{
entityType: 'experiment',
field: 'page_ids',
dependsOn: 'page',
extractKey: (pageIds) => {
// CRITICAL: Use SafeIdConverter for each page ID to prevent scientific notation
return (pageIds || []).map(id => safeIdToString(id));
},
errorMessage: (key) => `Page with ID '${key}' does not exist`,
prescriptiveGuidance: (key, projectId) => `
DEPENDENCY ERROR: Page must exist before being referenced in experiment.
PRESCRIPTIVE STEPS TO RESOLVE:
1. **LIST available pages:**
\`list_entities entity_type=page project_id=${projectId}\`
2. **IF PAGE MISSING, create it first:**
\`manage_entity_lifecycle operation=create entity_type=page project_id=${projectId} entity_data='{"name": "Home Page", "edit_url": "https://example.com", "project_id": ${projectId}}'\`
3. **NOTE the page ID from response**
4. **USE page ID in experiment creation**
TIP: OPERATIONAL SEQUENCE: Create Pages → Create Experiment with page_ids
NOTE: NOTE: Pages define where experiments run in Web Experimentation
`
},
// CAMPAIGN DEPENDENCIES
{
entityType: 'experiment',
field: 'campaign_id',
dependsOn: 'campaign',
// CRITICAL: Use SafeIdConverter to prevent scientific notation
extractKey: (campaignId) => campaignId ? safeIdToString(campaignId) : '',
errorMessage: (key) => `Campaign with ID '${key}' does not exist`,
prescriptiveGuidance: (key, projectId) => `
DEPENDENCY ERROR: Campaign must exist before creating experiments.
PRESCRIPTIVE STEPS TO RESOLVE:
1. **LIST available campaigns:**
\`list_entities entity_type=campaign project_id=${projectId}\`
2. **IF CAMPAIGN MISSING, create it first:**
\`manage_entity_lifecycle operation=create entity_type=campaign project_id=${projectId} entity_data='{"name": "Landing Page Campaign", "project_id": ${projectId}}'\`
3. **NOTE the campaign ID from response**
4. **USE campaign ID in experiment creation**
TIP: CONTEXT: Campaigns group related experiments in Web Experimentation
WARNING: NOTE: Feature Experimentation experiments don't use campaigns
`
}
];
/**
* Check dependencies for an entity and return prescriptive guidance if missing
*/
static async checkDependencies(entityType, data, projectId, apiHelper // OptimizelyAPIHelper instance if available
) {
const errors = [];
// Find all dependency checks for this entity type
const relevantChecks = this.dependencies.filter(check => check.entityType === entityType);
for (const check of relevantChecks) {
const fieldValue = data[check.field];
if (!fieldValue)
continue; // Field not present, skip check
const dependencyKeys = check.extractKey(fieldValue);
const keysToCheck = Array.isArray(dependencyKeys) ? dependencyKeys : [dependencyKeys];
for (const key of keysToCheck) {
if (!key)
continue;
// If we have an API helper, we could check if the dependency exists
// For now, we'll return prescriptive guidance on any error
const guidance = check.prescriptiveGuidance(key, projectId);
// This is where we'd check if the dependency exists
// For now, we'll add this to the entity builder's validation
// to be triggered when the API returns a dependency error
}
}
return {
isValid: errors.length === 0,
errors
};
}
/**
* Generate prescriptive guidance for a specific error
*/
static generateGuidanceForError(entityType, errorMessage, projectId, operation) {
// Check for condition format errors FIRST - but ONLY if it's actually an error about custom_attribute
if (entityType === 'audience' && errorMessage.includes('HTTP 400') && !errorMessage.toLowerCase().includes('does not exist')) {
// Only show this if it's a generic 400 error, not a specific "does not exist" error
return `
CONDITION FORMAT ERROR: Invalid audience conditions format.
UNDERSTANDING PLATFORM DIFFERENCES:
**Web Experimentation** (is_flags_enabled: false):
CORRECT: Uses: \`{"type": "custom_attribute", "name": "KEY", "value": "VALUE"}\`
**Feature Experimentation** (is_flags_enabled: true):
CORRECT: Uses: \`{"type": "attribute", "name": "KEY", "value": "VALUE"}\`
NOTE: CHECK YOUR PROJECT TYPE:
\`get_entity_details entity_type=project entity_id=${projectId}\`
Look for: "is_flags_enabled" field
CORRECT FORMAT FOR YOUR PROJECT:
If Web Experimentation:
\`"conditions": "[\\"and\\", {\\"type\\": \\"custom_attribute\\", \\"name\\": \\"region\\", \\"value\\": \\"Italy 7\\"}]"\`
If Feature Experimentation:
\`"conditions": "[\\"and\\", {\\"type\\": \\"attribute\\", \\"name\\": \\"region\\", \\"value\\": \\"Italy 7\\"}]"\`
TIP: KEY INSIGHT: The attribute type depends on your project platform!
WARNING: IMPORTANT:
- Always use the attribute KEY (not display name) in conditions
- Ensure proper JSON escaping in the conditions string
- Include project_id in the request body
`;
}
// Check for schema validation errors
if (entityType === 'variation' && (errorMessage.includes('schema') || errorMessage.includes('variable'))) {
return `
SCHEMA COMPLIANCE ERROR: Variation variables don't match flag schema.
PRESCRIPTIVE STEPS TO RESOLVE:
1. **GET the flag's variable definitions:**
\`list_entities entity_type=variable_definition project_id=${projectId} filters={"flag_key": "YOUR_FLAG_KEY"}\`
2. **CHECK each variable requirement:**
- Variable key (exact match required)
- Variable type (string, boolean, integer, double, json)
- Default value format
3. **CREATE variation with ALL variables:**
Example for a flag with 2 variables:
\`{
"variables": {
"show_banner": true, // boolean type
"banner_text": "Welcome!" // string type
}
}\`
4. **COMMON FIXES:**
- Add missing variables
- Fix type mismatches (e.g., "true" → true)
- Remove extra variables not in schema
TIP: REMEMBER: Every variation MUST have values for ALL variable definitions
`;
}
// Check for platform mismatch errors
if (errorMessage.includes('not supported') || errorMessage.includes('not available')) {
if (entityType === 'flag' || entityType === 'ruleset' || entityType === 'rule') {
return `
PLATFORM ERROR: ${entityType} is only available in Feature Experimentation projects.
PRESCRIPTIVE STEPS TO RESOLVE:
1. **CHECK project type:**
\`get_entity_details entity_type=project entity_id=${projectId}\`
Look for: "is_flags_enabled": true
2. **IF FALSE (Web Experimentation):**
Use these entities instead:
- campaigns (group experiments)
- experiments (A/B tests)
- pages (where tests run)
3. **LIST available entities:**
\`list_entities entity_type=campaign project_id=${projectId}\`
\`list_entities entity_type=experiment project_id=${projectId}\`
TIP: KEY INSIGHT: Feature Experimentation uses flags, Web uses campaigns/experiments
`;
}
}
// Check for wrong operation errors
if (errorMessage.includes('cannot be deleted') || errorMessage.includes('archive instead')) {
return `
OPERATION ERROR: ${entityType} cannot be deleted, only archived.
PRESCRIPTIVE STEPS TO RESOLVE:
1. **USE archive operation instead:**
\`manage_entity_lifecycle operation=archive entity_type=${entityType} project_id=${projectId} entity_id=["ENTITY_KEY_OR_ID"]\`
2. **TO RESTORE later:**
\`manage_entity_lifecycle operation=unarchive entity_type=${entityType} project_id=${projectId} entity_id=["ENTITY_KEY_OR_ID"]\`
TIP: WHY: Archiving preserves data integrity and allows restoration
WARNING: ENTITIES THAT SUPPORT DELETE:
- attributes, events, pages, extensions, webhooks
ARCHIVE-ONLY ENTITIES:
- flags, experiments, campaigns, audiences
`;
}
// Check for JSON Patch format errors
if (entityType === 'flag' && operation === 'update' && errorMessage.includes('patch')) {
return `
UPDATE FORMAT ERROR: Flags require JSON Patch format (RFC 6902).
PRESCRIPTIVE FIX:
WRONG: WRONG (full object):
\`{"description": "New description"}\`
CORRECT: CORRECT (JSON Patch):
\`[{"op": "replace", "path": "/description", "value": "New description"}]\`
COMMON OPERATIONS:
1. **REPLACE a field:**
\`[{"op": "replace", "path": "/description", "value": "Updated description"}]\`
2. **ADD a field:**
\`[{"op": "add", "path": "/new_field", "value": "new value"}]\`
3. **REMOVE a field:**
\`[{"op": "remove", "path": "/field_to_remove"}]\`
4. **MULTIPLE changes:**
\`[
{"op": "replace", "path": "/name", "value": "New Name"},
{"op": "replace", "path": "/description", "value": "New Description"}
]\`
TIP: TIP: Use JSON Patch for flags, regular JSON for most other entities
`;
}
// Check if this is a dependency error we can provide guidance for
for (const check of this.dependencies) {
if (check.entityType !== entityType)
continue;
// Pattern match common dependency errors
if (errorMessage.toLowerCase().includes('does not exist') ||
errorMessage.toLowerCase().includes('not found') ||
errorMessage.toLowerCase().includes('invalid') && errorMessage.toLowerCase().includes(check.dependsOn)) {
// Extract the key from error message if possible
const keyMatch = errorMessage.match(/['"]([^'"]+)['"]/);
if (keyMatch) {
const key = keyMatch[1];
return check.prescriptiveGuidance(key, projectId);
}
}
}
return null;
}
/**
* Format error response for MCP client consumption
*/
static formatMCPError(operation, entityType, errorMessage, projectId) {
const guidance = this.generateGuidanceForError(entityType, errorMessage, projectId, operation);
if (guidance) {
return {
error: errorMessage,
prescriptiveGuidance: guidance,
nextSteps: this.extractNextSteps(guidance)
};
}
return {
error: errorMessage
};
}
/**
* Extract actionable next steps from prescriptive guidance
*/
static extractNextSteps(guidance) {
const steps = [];
const lines = guidance.split('\n');
for (const line of lines) {
// Look for numbered steps with backticks (commands)
const commandMatch = line.match(/^\d️⃣.*?`([^`]+)`/);
if (commandMatch) {
steps.push(commandMatch[1]);
}
}
return steps;
}
}
//# sourceMappingURL=DependencyValidator.js.map