UNPKG

@simonecoelhosfo/optimizely-mcp-server

Version:

Optimizely MCP Server for AI assistants with integrated CLI tools

1,154 lines (1,153 loc) 52.1 kB
/** * Automated Prescriptive Error Enhancement System * @description Converts generic validation errors into intelligent, actionable guidance * * This system uses pattern detection to automatically enhance any error with: * - Clear explanations of what went wrong * - Specific actions to fix the problem * - Template mode suggestions when appropriate * - Field examples and documentation links * * The system requires zero maintenance - new fields and entities automatically * get prescriptive guidance based on schema metadata and platform context. * * @author Optimizely MCP Server * @version 1.0.0 */ import { FIELDS } from '../generated/fields.generated.js'; import { getLogger } from '../logging/Logger.js'; /** * Automated system for enhancing validation errors with prescriptive guidance */ export class AutomatedPrescriptiveErrorEnhancer { patterns; constructor() { this.patterns = [ { name: 'MissingRequiredField', detect: (error) => /Missing required field '(\w+)'/.test(error), enhance: (error, context) => this.enhanceMissingFieldError(error, context) }, { name: 'AlreadyExists', detect: (error) => /already in use.*\(id:\s*(\d+)\)|name.*already in use|key.*already in use/.test(error), enhance: (error, context) => this.enhanceAlreadyExistsError(error, context) }, { name: 'TemplateRequired', detect: (error) => /Template mode required|complex dependencies/.test(error), enhance: (error, context) => this.enhanceTemplateModeError(error, context) }, { name: 'InvalidFieldType', detect: (error) => /Field '(\w+)' must be.*but got/.test(error), enhance: (error, context) => this.enhanceInvalidTypeError(error, context) }, { name: 'InvalidEnumValue', detect: (error) => /Invalid value "([^"]+)" for field '(\w+)'/.test(error), enhance: (error, context) => this.enhanceInvalidEnumError(error, context) }, { name: 'PlatformMismatch', detect: (error, context) => this.isPlatformMismatchError(error, context), enhance: (error, context) => this.enhancePlatformMismatchError(error, context) }, { name: 'AudienceConditionValidation', detect: (error, context) => this.isAudienceConditionError(error, context), enhance: (error, context) => this.enhanceAudienceConditionError(error, context) }, { name: 'RulesetOperation', detect: (error) => /ruleset.*operation|individual.*rule.*endpoint|rules.*don't.*have.*individual.*endpoints|method.*not.*allowed.*ruleset/i.test(error), enhance: (error, context) => this.enhanceRulesetOperationError(error, context) }, { name: 'TemplateNotFound', detect: (error) => /template.*not.*found|unknown.*template|invalid.*template.*type/i.test(error), enhance: (error, context) => this.enhanceTemplateNotFoundError(error, context) }, { name: 'ParameterError', detect: (error) => /parameter.*order|wrong.*parameter|missing.*parameter|invalid.*parameter.*sequence/i.test(error), enhance: (error, context) => this.enhanceParameterError(error, context) }, { name: 'EntityExists', detect: (error) => /entity.*exists|already.*exists.*use.*existing|duplicate.*entity/i.test(error), enhance: (error, context) => this.enhanceEntityExistsError(error, context) }, { name: 'ForeignKeyConstraint', detect: (error) => /foreign.*key.*constraint|missing.*dependency|required.*entity.*not.*found|create.*dependency.*first/i.test(error), enhance: (error, context) => this.enhanceForeignKeyConstraintError(error, context) }, { name: 'ValidationError', detect: (error) => /validation.*failed|invalid.*format|schema.*validation|field.*validation.*error/i.test(error), enhance: (error, context) => this.enhanceValidationError(error, context) }, { name: 'PermissionDenied', detect: (error) => /permission.*denied|access.*denied|unauthorized|forbidden.*operation|insufficient.*permissions/i.test(error), enhance: (error, context) => this.enhancePermissionDeniedError(error, context) }, { name: 'RateLimitError', detect: (error) => /rate.*limit|too.*many.*requests|quota.*exceeded|throttled|429.*error/i.test(error), enhance: (error, context) => this.enhanceRateLimitError(error, context) }, { name: 'NetworkError', detect: (error) => /network.*error|connection.*failed|timeout|dns.*resolution|host.*unreachable|502|503|504/i.test(error), enhance: (error, context) => this.enhanceNetworkError(error, context) } ]; } /** * Main entry point for error enhancement */ enhanceError(originalError, context = {}) { getLogger().debug({ originalError, context }, 'AutomatedPrescriptiveErrorEnhancer: Enhancing error'); // Try each pattern until we find a match for (const pattern of this.patterns) { if (pattern.detect(originalError, context)) { getLogger().info({ patternName: pattern.name, originalError }, 'AutomatedPrescriptiveErrorEnhancer: Pattern matched, enhancing error'); return pattern.enhance(originalError, context); } } // Fallback: create generic but still enhanced response return this.createGenericPrescriptiveError(originalError, context); } /** * Enhances missing required field errors */ enhanceMissingFieldError(error, context) { const fieldMatch = error.match(/Missing required field '(\w+)'/); const field = fieldMatch?.[1] || context.field || 'unknown'; // Check if template mode should be suggested if (this.shouldUseTemplate(field, context.entityType)) { return { status: 'TEMPLATE_MODE_REQUIRED', error: { code: `MISSING_${field.toUpperCase()}`, message: this.getFieldGuidance(field, context) + ' - use template mode for automatic setup' }, required_action: { tool: 'manage_entity_lifecycle', mode: 'template', parameters: { entity_type: context.entityType, project_id: context.projectId } }, hints: this.getTemplateHints(field, context), documentation: this.getDocumentationUrl(context.entityType, 'template-mode') }; } // Regular field validation error return { status: 'VALIDATION_FAILED', error: { code: `MISSING_${field.toUpperCase()}`, message: this.getFieldGuidance(field, context), field }, fix_options: [{ priority: 'recommended', action: `Add ${field} field`, command: this.getFieldExamples(field, context)[0] ? `"${field}": ${JSON.stringify(this.getFieldExamples(field, context)[0])}` : `"${field}": "your_value_here"`, reason: `Required for ${context.entityType} creation` }], examples: this.getFieldExamples(field, context), documentation: this.getDocumentationUrl(context.entityType, field) }; } /** * Enhances already exists errors (duplicate keys/names) */ enhanceAlreadyExistsError(error, context) { const idMatch = error.match(/\(id:\s*(\d+)\)/); const existingId = idMatch?.[1]; const entityType = this.extractEntityTypeFromError(error, context); return { status: 'ERROR', error: { code: `${entityType.toUpperCase()}_ALREADY_EXISTS`, message: `${entityType} already exists`, existing_id: existingId }, fix_options: [ { priority: 'recommended', action: `Use existing ${entityType}`, command: existingId ? `{"${entityType}": {"ref": {"id": "${existingId}"}}}` : `{"${entityType}": {"ref": {"name": "existing_name"}}}`, reason: 'Reuse prevents duplicates and maintains consistency' }, { priority: 'alternative', action: 'Create with different key/name', command: 'Change the key or name field to a unique value', reason: 'If you need a separate entity with different properties' } ], hints: [ existingId ? `Existing ${entityType} ID: ${existingId}` : `${entityType} name/key already exists`, 'Using ref prevents conflicts and saves API calls' ] }; } /** * Enhances template mode requirement errors */ enhanceTemplateModeError(error, context) { return { status: 'TEMPLATE_MODE_REQUIRED', error: { code: 'COMPLEX_ENTITY_DEPENDENCIES', message: `${context.entityType} with complex configuration requires template mode` }, required_action: { tool: 'manage_entity_lifecycle', mode: 'template', parameters: { entity_type: context.entityType, project_id: context.projectId } }, hints: [ 'Template creates pages/events/variations automatically', 'Manual creation of dependencies will fail' ], documentation: this.getDocumentationUrl(context.entityType, 'template-mode') }; } /** * Enhances invalid type errors */ enhanceInvalidTypeError(error, context) { const fieldMatch = error.match(/Field '(\w+)'/); const field = fieldMatch?.[1] || 'unknown'; const expectedType = this.extractExpectedType(error); const providedValue = this.extractProvidedValue(error); return { status: 'VALIDATION_FAILED', error: { code: 'INVALID_FIELD_TYPE', message: `Field '${field}' expects ${expectedType}`, field, provided: providedValue, expected: expectedType }, examples: this.getTypeExamples(field, expectedType, context), hints: this.getTypeHints(field, expectedType), documentation: this.getDocumentationUrl(context.entityType, field) }; } /** * Enhances invalid enum value errors */ enhanceInvalidEnumError(error, context) { const matches = error.match(/Invalid value "([^"]+)" for field '(\w+)'.*Valid options are: ([^.]+)/); const providedValue = matches?.[1]; const field = matches?.[2]; const validOptions = matches?.[3]?.split(', ') || []; return { status: 'VALIDATION_FAILED', error: { code: 'INVALID_ENUM_VALUE', message: `Field '${field}' must be one of: ${validOptions.join(', ')}`, field, provided: providedValue, expected: validOptions }, examples: validOptions.slice(0, 3).map(option => ({ [field]: option })), hints: [ 'Values are case-sensitive', `Try: "${validOptions[0]}" instead of "${providedValue}"` ] }; } /** * Enhances platform mismatch errors */ enhancePlatformMismatchError(error, context) { const currentPlatform = context.platform || 'unknown'; const requiredPlatform = this.inferRequiredPlatform(error, context); return { status: 'ERROR', error: { code: 'WRONG_PROJECT_TYPE', message: this.getPlatformGuidanceMessage(context.entityType, requiredPlatform) }, project_info: { current: currentPlatform, required: requiredPlatform }, fix_options: [ { priority: 'recommended', action: `Switch to ${requiredPlatform} project`, command: `Use project_id with platform='${requiredPlatform}'`, reason: `${context.entityType} entities require ${requiredPlatform} experimentation` } ], documentation: this.getDocumentationUrl('project', 'platform-types') }; } /** * Creates a fallback enhanced response for unmatched patterns */ createGenericPrescriptiveError(error, context) { return { status: 'ERROR', error: { code: 'ENHANCED_VALIDATION_ERROR', message: error }, hints: [ 'Check field names and types against schema', 'Use get_entity_documentation for detailed guidance' ], documentation: this.getDocumentationUrl(context.entityType) }; } /** * Determines if template mode should be suggested for a missing field */ shouldUseTemplate(field, entityType) { if (!entityType) return false; const templateTriggerFields = { 'page_ids': ['experiment'], 'page_id': ['experiment'], 'variations': ['flag', 'experiment'], 'rules': ['flag'], 'variable_definitions': ['flag'], 'targeting_rules': ['flag'], 'ab_test': ['flag'], 'metrics': ['experiment', 'campaign'] }; return templateTriggerFields[field]?.includes(entityType) || false; } /** * Gets field guidance from schema metadata */ getFieldGuidance(field, context) { if (!context.entityType) { return `${field} is required`; } const entityFields = FIELDS[context.entityType]; const description = entityFields?.fieldDescriptions?.[field]; if (description) { return description; } // Fallback guidance based on common field patterns const commonGuidance = { 'page_ids': 'Array of page IDs where the experiment will run', 'page_id': 'Page ID where the experiment will run', 'weight': 'Traffic allocation percentage (0-10000 basis points)', 'variations': 'Array of experiment variations with different treatments', 'name': 'Human-readable name for the entity', 'key': 'Unique identifier for the entity', 'project_id': 'ID of the Optimizely project' }; return commonGuidance[field] || `${field} is required for ${context.entityType} creation`; } /** * Gets template-specific hints */ getTemplateHints(field, context) { const hints = { 'page_ids': ['Template auto-creates pages from URL patterns', 'Existing page? Use: {"ref":{"id":"123"}}'], 'variations': ['Template creates variations automatically', 'Specify variation names and traffic allocation'], 'rules': ['Template handles targeting rules setup', 'Define audience and percentage rollout'], 'metrics': ['Template creates events and metrics together', 'Specify event names and aggregation types'] }; return hints[field] || ['Template mode handles complex dependencies automatically']; } /** * Gets field fix options for manual correction */ getFieldFixOptions(field, context) { const examples = this.getFieldExamples(field, context); const firstExample = examples[0]; return [ { priority: 'recommended', action: `Add ${field} field`, command: firstExample ? `"${field}": ${JSON.stringify(firstExample)}` : `"${field}": "your_value_here"`, reason: `Required for ${context.entityType} creation` } ]; } /** * Gets field examples from schema metadata */ getFieldExamples(field, context) { if (!context.entityType) { return []; } const entityFields = FIELDS[context.entityType]; const example = entityFields?.fieldExamples?.[field]; if (example !== undefined) { return Array.isArray(example) ? example : [example]; } // Fallback examples const commonExamples = { 'weight': [5000, 3333, 2500], 'page_ids': [[12345, 67890], [98765]], 'name': ['My Test Experiment', 'Homepage Banner Test'], 'key': ['homepage_banner_test', 'checkout_flow_v2'], 'status': ['running', 'paused', 'not_started'] }; return commonExamples[field] || []; } /** * Gets type-specific examples */ getTypeExamples(field, expectedType, context) { if (expectedType === 'integer') { if (field === 'weight') return [{ value: 5000, meaning: '50%' }, { value: 3333, meaning: '33.33%' }]; if (field.includes('id')) return [12345, 67890, 98765]; return [100, 500, 1000]; } if (expectedType === 'array') { if (field === 'page_ids') return [[12345, 67890]]; if (field === 'variations') return [['control', 'treatment']]; return [['item1', 'item2']]; } return this.getFieldExamples(field, context); } /** * Gets type-specific hints */ getTypeHints(field, expectedType) { const hints = { 'integer': ['Use numbers without quotes', 'For percentages: 5000 = 50%'], 'array': ['Use square brackets: [item1, item2]', 'Can be empty: []'], 'string': ['Use quotes: "my_value"', 'Avoid special characters in keys'], 'boolean': ['Use true or false (no quotes)', 'Not "true" or "false" strings'] }; return hints[expectedType] || [`Expected ${expectedType} type`]; } /** * Generates documentation URLs */ getDocumentationUrl(entityType, section) { if (!entityType) return 'resource://entity-rules'; if (section === 'template-mode') { return `resource://visual-workflows#${entityType}-template-mode`; } if (section) { return `resource://entity-rules#${entityType}-${section}`; } return `resource://entity-rules#${entityType}`; } /** * Helper methods for pattern detection and data extraction */ isPlatformMismatchError(error, context) { // Platform mismatch patterns const patterns = [ /flag.*feature.*experimentation/i, /campaign.*web.*experimentation/i, /wrong.*project.*type/i, /platform.*mismatch/i ]; return patterns.some(pattern => pattern.test(error)); } extractEntityTypeFromError(error, context) { if (context.entityType) return context.entityType; // Try to extract from error message const patterns = [ { pattern: /UserEvent/i, type: 'event' }, { pattern: /Event/i, type: 'event' }, { pattern: /Experiment/i, type: 'experiment' }, { pattern: /Campaign/i, type: 'campaign' }, { pattern: /Page/i, type: 'page' }, { pattern: /Audience/i, type: 'audience' }, { pattern: /Attribute/i, type: 'attribute' }, { pattern: /Flag/i, type: 'flag' } ]; for (const { pattern, type } of patterns) { if (pattern.test(error)) { return type; } } return 'entity'; } extractExpectedType(error) { const typeMatch = error.match(/must be.*?(integer|string|array|boolean|number)/i); return typeMatch?.[1]?.toLowerCase() || 'unknown'; } extractProvidedValue(error) { const valueMatch = error.match(/got (.+?)(?:\s|$)/); return valueMatch?.[1] || 'unknown'; } inferRequiredPlatform(error, context) { if (context.entityType === 'flag' || error.includes('flag')) return 'feature'; if (context.entityType === 'campaign' || error.includes('campaign')) return 'web'; return 'feature'; // Default assumption } getPlatformGuidanceMessage(entityType, requiredPlatform) { const entityName = entityType || 'entity'; const platform = requiredPlatform || 'appropriate'; return `${entityName} operations require ${platform} experimentation project`; } /** * Detects audience condition validation errors */ isAudienceConditionError(error, context) { // Check if this is an audience entity with condition-related error if (context.entityType !== 'audience') return false; // Pattern: Unsupported type errors in conditions if (error.includes('Unsupported type:') && error.includes('conditions:')) { return true; } // Pattern: Invalid condition structure if (error.includes('conditions') && (error.includes('invalid') || error.includes('malformed') || error.includes('unsupported') || error.includes('not recognized'))) { return true; } return false; } /** * Enhances audience condition validation errors with template guidance */ enhanceAudienceConditionError(error, context) { // Extract the invalid condition type if possible const typeMatch = error.match(/Unsupported type:\s*'([^']+)'/); const invalidType = typeMatch?.[1]; // Try to suggest the correct type let suggestedFix = ''; let correctType = ''; if (invalidType) { // Comprehensive mapping of common mistakes and variations for condition types const commonMistakes = { // cookies condition variations 'cookie': 'cookies', 'cookis': 'cookies', 'coookie': 'cookies', 'cookies_targeting': 'cookies', 'cookie_value': 'cookies', 'browser_cookie': 'cookies', 'user_cookie': 'cookies', // code condition variations 'javascript': 'code', 'js': 'code', 'script': 'code', 'custom_code': 'code', 'js_code': 'code', 'javascript_code': 'code', // query condition variations 'query_param': 'query', 'query_parameter': 'query', 'url_param': 'query', 'url_parameter': 'query', 'get_param': 'query', 'parameter': 'query', 'params': 'query', 'query_string': 'query', // referrer condition variations 'referrer_url': 'referrer', 'referer': 'referrer', 'referring_url': 'referrer', 'ref_url': 'referrer', 'referral_url': 'referrer', 'previous_url': 'referrer', 'origin_url': 'referrer', // campaign condition variations 'utm_campaign': 'campaign', 'utm': 'campaign', 'marketing_campaign': 'campaign', 'ad_campaign': 'campaign', 'campaign_name': 'campaign', 'campaign_id': 'campaign', // browser_version condition variations 'browser': 'browser_version', 'user_agent': 'browser_version', 'browser_type': 'browser_version', 'web_browser': 'browser_version', 'client_browser': 'browser_version', 'browser_name': 'browser_version', // device condition variations 'device_type': 'device', 'user_device': 'device', 'client_device': 'device', 'mobile_device': 'device', 'hardware': 'device', 'screen_size': 'device', // custom_attribute condition variations 'custom_attr': 'custom_attribute', 'attribute': 'custom_attribute', 'user_attribute': 'custom_attribute', 'custom_property': 'custom_attribute', 'user_property': 'custom_attribute', 'profile_attribute': 'custom_attribute', 'attr': 'custom_attribute', // ip condition variations 'ip_address': 'ip', 'user_ip': 'ip', 'client_ip': 'ip', 'remote_ip': 'ip', 'origin_ip': 'ip', 'visitor_ip': 'ip', // language condition variations 'lang': 'language', 'locale': 'language', 'browser_language': 'language', 'user_language': 'language', 'client_language': 'language', 'i18n': 'language', 'localization': 'language', // location condition variations 'geo': 'location', 'geography': 'location', 'geolocation': 'location', 'country': 'location', 'region': 'location', 'city': 'location', 'geographic': 'location', 'geoip': 'location', 'user_location': 'location', // platform condition variations 'os': 'platform', 'operating_system': 'platform', 'user_platform': 'platform', 'system': 'platform', 'client_platform': 'platform', 'device_platform': 'platform', 'device_os': 'platform', // first_session condition variations 'first_visit': 'first_session', 'new_user': 'first_session', 'new_visitor': 'first_session', 'returning_user': 'first_session', 'repeat_visitor': 'first_session', 'user_type': 'first_session', 'visitor_type': 'first_session', 'session_type': 'first_session', // time_and_day condition variations 'time': 'time_and_day', 'day': 'time_and_day', 'schedule': 'time_and_day', 'time_of_day': 'time_and_day', 'day_of_week': 'time_and_day', 'hour': 'time_and_day', 'weekday': 'time_and_day', 'datetime': 'time_and_day', 'time_schedule': 'time_and_day', // source_type condition variations 'source': 'source_type', 'traffic_source': 'source_type', 'referral_source': 'source_type', 'visit_source': 'source_type', 'acquisition_source': 'source_type', 'channel': 'source_type', 'traffic_channel': 'source_type', // event condition variations 'custom_event': 'event', 'user_event': 'event', 'tracking_event': 'event', 'action': 'event', 'user_action': 'event', 'behavior': 'event', 'interaction': 'event', // custom_tag condition variations 'tag': 'custom_tag', 'user_tag': 'custom_tag', 'profile_tag': 'custom_tag', 'category_tag': 'custom_tag', 'segment_tag': 'custom_tag', 'custom_label': 'custom_tag', // custom_dimension condition variations 'custom_dim': 'custom_dimension', 'dimension': 'custom_dimension', 'user_dimension': 'custom_dimension', 'analytics_dimension': 'custom_dimension', 'segment_dimension': 'custom_dimension', 'custom_segment': 'custom_dimension' }; correctType = commonMistakes[invalidType.toLowerCase()]; // If no exact match, try fuzzy matching for typos if (!correctType) { correctType = this.findClosestConditionType(invalidType) || invalidType; } if (correctType && correctType !== invalidType) { suggestedFix = `Did you mean "${correctType}" instead of "${invalidType}"?`; } } return { status: 'TEMPLATE_MODE_REQUIRED', error: { code: 'AUDIENCE_CONDITION_VALIDATION_FAILED', message: `Invalid audience condition structure detected. ${suggestedFix}`.trim(), field: 'conditions', provided: invalidType, expected: correctType || 'valid condition type' }, required_action: { tool: 'get_entity_templates', parameters: { entity_type: 'audience', project_id: context.projectId } }, fix_options: [ { priority: 'recommended', action: 'Use template mode for correct condition structure', command: 'get_entity_templates entity_type=audience', reason: 'Audience conditions require precise formatting and valid condition types' }, { priority: 'alternative', action: 'Review available condition types', command: 'get_entity_templates entity_type=audience project_id=' + (context.projectId || 'YOUR_PROJECT_ID'), reason: 'Templates show all 20+ supported condition types with examples' } ], hints: [ '⚠️ CRITICAL: Conditions must be JSON STRING, not JavaScript array', `Available types: cookies, code, query, referrer, campaign, browser_version, device, custom_attribute, ip, language, location, platform, first_session, time_and_day, source_type`, 'Format: conditions: "[\\"and\\", {\\"type\\": \\"cookies\\", \\"name\\": \\"cookie_name\\", \\"match_type\\": \\"exists\\"}]"', suggestedFix ? `Suggestion: ${suggestedFix}` : 'Check template for correct condition type spelling' ].filter(Boolean), documentation: 'resource://entity-templates#audience-conditions' }; } /** * Finds the closest valid condition type using fuzzy matching for typos */ findClosestConditionType(invalidType) { const validTypes = [ 'cookies', 'code', 'query', 'referrer', 'campaign', 'browser_version', 'device', 'custom_attribute', 'ip', 'language', 'location', 'platform', 'first_session', 'time_and_day', 'source_type', 'event', 'custom_tag', 'custom_dimension', 'browser' ]; const input = invalidType.toLowerCase(); let bestMatch = null; let bestScore = 0; for (const validType of validTypes) { const score = this.calculateSimilarity(input, validType); if (score > bestScore && score > 0.6) { // Minimum similarity threshold bestScore = score; bestMatch = validType; } } return bestMatch; } /** * Calculates similarity between two strings using a simple algorithm */ calculateSimilarity(str1, str2) { const len1 = str1.length; const len2 = str2.length; if (len1 === 0) return len2 === 0 ? 1 : 0; if (len2 === 0) return 0; // Check for exact substring matches first if (str2.includes(str1) || str1.includes(str2)) { return Math.min(str1.length, str2.length) / Math.max(str1.length, str2.length); } // Simple character matching algorithm let matches = 0; const shorter = len1 < len2 ? str1 : str2; const longer = len1 < len2 ? str2 : str1; for (let i = 0; i < shorter.length; i++) { if (longer.includes(shorter[i])) { matches++; } } return matches / Math.max(len1, len2); } /** * Enhances ruleset operation errors with proper workflow guidance */ enhanceRulesetOperationError(error, context) { return { status: 'WORKFLOW_GUIDANCE_REQUIRED', error: { code: 'RULESET_OPERATION_ERROR', message: 'Rulesets require specific update patterns - individual rules cannot be modified directly' }, required_action: { tool: 'manage_entity_lifecycle', mode: 'template', parameters: { entity_type: 'ruleset_concepts', project_id: context.projectId } }, fix_options: [ { priority: 'recommended', action: 'Update entire ruleset', command: '1. GET current ruleset → 2. Modify rules array → 3. PATCH entire ruleset', reason: 'Rules are part of rulesets, not individual entities' }, { priority: 'alternative', action: 'Use template mode for rule changes', reason: 'Template mode handles ruleset modifications automatically' } ], hints: [ 'Rules do NOT have individual endpoints', 'Always PATCH the parent ruleset with modified rules array', 'GET ruleset first to see current structure', 'Use PATCH method, not PUT or POST for ruleset updates' ], documentation: 'resource://docs/ruleset_concepts' }; } /** * Enhances template not found errors with available template list */ enhanceTemplateNotFoundError(error, context) { // Extract requested template from error if possible const templateMatch = error.match(/template[\s"']*([a-zA-Z_]+)/i); const requestedTemplate = templateMatch?.[1]; const availableTemplates = [ 'flag', 'experiment', 'audience', 'event', 'attribute', 'ruleset', 'page', 'variation', 'project', 'variable_definition', 'feature', 'environment', 'webhook', 'extension', 'group', 'campaign', 'personalization_experiment', 'rule', 'collaborator', 'list_attribute', 'results', 'segment', 'variable', 'flag_remove_rule', 'flag_simple_update' ]; return { status: 'TEMPLATE_MODE_REQUIRED', error: { code: 'TEMPLATE_NOT_FOUND', message: requestedTemplate ? `Template "${requestedTemplate}" not found` : 'Invalid or missing template type' }, required_action: { tool: 'manage_entity_lifecycle', mode: 'template', parameters: { entity_type: 'flag', // Default to most common project_id: context.projectId } }, fix_options: [ { priority: 'recommended', action: 'Choose from available templates', command: `Available: ${availableTemplates.slice(0, 5).join(', ')}, ...`, reason: 'Use exact template names from the available list' } ], hints: [ 'Template names must be exact matches', `Available templates: ${availableTemplates.join(', ')}`, 'Use "flag" for feature flags, "experiment" for A/B tests', 'Check spelling and use underscore format (not camelCase)' ], examples: availableTemplates.slice(0, 8), documentation: 'resource://entity-templates' }; } /** * Enhances parameter order errors with correct sequence */ enhanceParameterError(error, context) { const correctOrder = [ 'operation', 'entity_type', 'entity_data', 'entity_id', 'project_id', 'options', 'mode', 'template_data' ]; return { status: 'VALIDATION_FAILED', error: { code: 'PARAMETER_ORDER_ERROR', message: 'Parameters must be provided in the correct order' }, fix_options: [ { priority: 'recommended', action: 'Use correct parameter order', command: `manage_entity_lifecycle(${correctOrder.join(', ')})`, reason: 'MCP requires exact parameter sequence' } ], hints: [ 'Parameter order is CRITICAL for MCP tools', `Correct order: ${correctOrder.join(' → ')}`, 'operation: "create"|"update"|"delete"', 'mode: "direct"|"template" (optional)', 'All parameters after project_id are optional' ], examples: [ 'manage_entity_lifecycle("create", "flag", {data}, null, "12345")', 'manage_entity_lifecycle("update", "experiment", {data}, "exp_123", "12345", {}, "template")' ], documentation: 'resource://parameter-order-guide' }; } /** * Enhances entity exists errors with adoption options */ enhanceEntityExistsError(error, context) { const idMatch = error.match(/id[:\s]*(\d+)/i); const nameMatch = error.match(/name[:\s]*["']([^"']+)["']/i); const existingId = idMatch?.[1]; const existingName = nameMatch?.[1]; return { status: 'ERROR', error: { code: 'ENTITY_ALREADY_EXISTS', message: 'Entity already exists - use existing entity or modify name/key', existing_id: existingId }, fix_options: [ { priority: 'recommended', action: 'Adopt existing entity', command: existingId ? `{"ref": {"id": "${existingId}"}}` : `{"ref": {"name": "${existingName || 'existing_name'}"}}`, reason: 'Reuse existing entity instead of creating duplicate' }, { priority: 'alternative', action: 'Use different name/key', command: 'Change name or key to make it unique', reason: 'Create new entity with unique identifier' } ], hints: [ 'Each entity name/key must be unique within project', 'Use ref pattern to reference existing entities', 'Check existing entities with list_entities tool', existingId ? `Existing entity ID: ${existingId}` : 'Use descriptive, unique names' ], examples: [ '{"ref": {"id": "12345"}}', '{"ref": {"name": "Homepage Banner"}}', '{"ref": {"key": "homepage_test"}}' ], documentation: 'resource://entity-adoption' }; } /** * Enhances foreign key constraint errors with dependency guidance */ enhanceForeignKeyConstraintError(error, context) { // Try to extract what dependency is missing const dependencyPatterns = [ { pattern: /project/i, entity: 'project', action: 'Create project first' }, { pattern: /page/i, entity: 'page', action: 'Create page first' }, { pattern: /audience/i, entity: 'audience', action: 'Create audience first' }, { pattern: /metric|event/i, entity: 'event', action: 'Create event first' }, { pattern: /campaign/i, entity: 'campaign', action: 'Create campaign first' } ]; const missingDep = dependencyPatterns.find(dep => dep.pattern.test(error)); const createOrder = ['project', 'page', 'audience', 'event', 'experiment']; return { status: 'MISSING_DEPENDENCY', error: { code: 'FOREIGN_KEY_CONSTRAINT', message: missingDep ? `Missing ${missingDep.entity} dependency` : 'Missing required dependency - create prerequisite entities first' }, required_action: { tool: 'manage_entity_lifecycle', mode: 'template', parameters: { entity_type: missingDep?.entity || 'project', project_id: context.projectId } }, fix_options: [ { priority: 'recommended', action: missingDep?.action || 'Create missing dependency', reason: 'Dependencies must exist before creating dependent entities' }, { priority: 'alternative', action: 'Use existing entity reference', command: '{"ref": {"id": "existing_id"}}', reason: 'Reference existing dependency instead of creating new' } ], hints: [ `Creation order: ${createOrder.join(' → ')}`, 'Projects must exist before other entities', 'Pages required for web experiments', 'Use list_entities to find existing dependencies', 'Template mode auto-creates some dependencies' ], documentation: 'resource://entity-dependencies' }; } /** * Enhances validation errors with format examples */ enhanceValidationError(error, context) { // Extract field name if possible const fieldMatch = error.match(/field[\s"']*(\w+)/i) || error.match(/'(\w+)'.*validation/i); const field = fieldMatch?.[1]; const formatExamples = { 'audience_conditions': '["and", {"type": "custom_attribute", "name": "tier", "value": "premium"}]', 'weight': '5000 (basis points: 5000 = 50%)', 'key': '"homepage_test" (snake_case, no spaces)', 'email': '"user@example.com"', 'url': '"https://example.com/page"', 'percentage': '0.95 (decimal: 0.95 = 95%)' }; return { status: 'VALIDATION_FAILED', error: { code: 'SCHEMA_VALIDATION_ERROR', message: field ? `Validation failed for field '${field}'` : 'Data does not match required schema format', field }, fix_options: [ { priority: 'recommended', action: field ? `Fix ${field} format` : 'Check data format', command: field && formatExamples[field] ? `"${field}": ${formatExamples[field]}` : 'Ensure data matches schema requirements', reason: 'Field must match expected type and format' } ], hints: [ field ? `${field} format issues are common` : 'Check field types and formats', 'JSON strings must use double quotes', // Context-aware hints for variable values vs general fields error.includes('variables') && error.includes('value') ? 'Feature flag variable values MUST be strings: "true", "false", "5000"' : 'Boolean values: true/false (not "true"/"false")', error.includes('variables') && error.includes('value') ? 'Variable values are ALWAYS strings: "5000" not 5000' : 'Numbers without quotes: 5000 not "5000"', 'Arrays use square brackets: [item1, item2]' ], examples: field ? [formatExamples[field]].filter(Boolean) : [ '{"name": "Test", "weight": 5000}', '{"conditions": "[\\"and\\", {}]"}' ], documentation: `resource://field-formats${field ? `#${field}` : ''}` }; } /** * Enhances permission denied errors with access guidance */ enhancePermissionDeniedError(error, context) { return { status: 'ERROR', error: { code: 'PERMISSION_DENIED', message: 'Insufficient permissions for this operation' }, fix_options: [ { priority: 'recommended', action: 'Check API token permissions', reason: 'Token may lack required scopes for this operation' }, { priority: 'alternative', action: 'Verify project access', reason: 'Account may not have access to this project' }, { priority: 'alternative', action: 'Check user role permissions', reason: 'User role may not allow this type of modification' } ], hints: [ 'API tokens have different permission levels', 'Some operations require Admin or Editor roles', 'Feature/Web projects have different permission requirements', 'Check token scopes in Optimizely dashboard', 'Verify project membership and role assignment' ], examples: [ 'Required scopes: read:projects, write:experiments', 'Admin role needed for project settings', 'Editor role needed for experiment creation' ], documentation: 'resource://permission-requirements' }; } /** * Enhances rate limit errors with retry guidance */ enhanceRateLimitError(error, context) { // Extract rate limit info if available const retryMatch = error.match(/retry.*after[\s:]*(\d+)/i); const retryAfter = retryMatch?.[1]; return { status: 'ERROR', error: { code: 'RATE_LIMIT_EXCEEDED', message: 'API rate limit exceeded - too many requests' }, fix_options: [ { priority: 'recommended', action: retryAfter ? `Wait ${retryAfter} seconds before retrying` : 'Wait before retrying request', reason: 'API enforces rate limits to ensure service availability' }, { priority: 'alternative', action: 'Implement exponential backoff', reason: 'Automatic retry with increasing delays' } ], hints: [ 'Optimizely API limits: ~100 requests per minute', 'Bulk operations count as multiple requests', 'Use template mode to reduce API calls', retryAfter ? `Retry after: ${retryAfter} seconds` : 'Wait 60 seconds before retrying', 'Consider batching operations to reduce request frequency' ], examples: [ 'setTimeout(() => retry(), 60000)', 'Exponential backoff: 1s, 2s, 4s, 8s delays' ], documentation: 'resource://rate-limiting' }; } /** * Enhances network errors with connectivity guidance */ enhanceNetworkError(error, context) { const errorType = error.includes('timeout') ? 'timeout' : error.includes('dns') ? 'dns' : error.includes('502') || error.includes('503') || error.includes('504') ? 'server' : 'connection'; const guidance = { timeout: 'Request timed out - server took too long to respond', dns: 'DNS resolution failed - cannot reach Optimizely servers',