UNPKG

@simonecoelhosfo/optimizely-mcp-server

Version:

Optimizely MCP Server for AI assistants with integrated CLI tools

932 lines (924 loc) 861 kB
/** * Optimizely MCP Tools - Business Logic Layer * @description Core business logic for Optimizely data operations that remains completely * unchanged in the MCP server implementation. This module serves as the stable business * logic layer that is consumed by the MCP protocol adapter layer. * * ADAPTER PATTERN IMPLEMENTATION: * This file represents the preserved business logic layer in our MCP adapter pattern. * The MCP protocol layer (src/mcp/) wraps these tools without modifying them, ensuring: * - Complete separation of business logic and protocol concerns * - Zero changes to existing business logic during MCP implementation * - Clear adapter pattern boundary between protocol and domain logic * - Stable API contract that can be used by other interfaces (CLI, REST, etc.) * * Key Principles: * - Business logic methods remain completely unchanged * - Error handling uses business-appropriate error types (converted by MCP layer) * - Input/output formats optimized for business needs, not MCP protocol * - No MCP-specific code or dependencies in this layer * - Can be used independently of MCP protocol layer * * MCP Integration: * The MCP layer (MCPToolHandlers.ts) calls these methods and: * - Validates MCP input parameters and converts to business format * - Maps business errors to MCP error types via MCPErrorMapper * - Formats business results to MCP response format * - Handles MCP protocol concerns (timeouts, validation, etc.) * * @author Optimizely MCP Server * @version 1.0.0 * @since 1.0.0 - Original business logic implementation * @pattern Adapter Pattern - Business Logic Layer */ import { getLogger } from '../logging/Logger.js'; import { safeLogObject, getObjectMetadata } from '../logging/LoggingUtils.js'; import { MCPErrorMapper, MCPErrorUtils, createPrescriptiveError } from '../errors/MCPErrorMapping.js'; import { ConfigurableDefaultsManager } from '../defaults/ConfigurableDefaultsManager.js'; import { SchemaAwareEntityBuilder } from '../builders/SchemaAwareEntityBuilder.js'; import { FIELDS } from '../generated/fields.generated.js'; import { ENTITY_TEMPLATES } from '../templates/EntityTemplates.js'; import { OpenAPIReferenceHandler } from './OpenAPIReferenceHandler.js'; import { HardStopError, HardStopErrorType } from '../errors/HardStopError.js'; import { ProjectEntityValidator } from '../validation/ProjectEntityValidator.js'; import { PrescriptiveValidator } from '../validation/PrescriptiveValidator.js'; import { safeIdToString } from '../utils/SafeIdConverter.js'; import { EntityMigrationOrchestrator } from '../migration/EntityMigrationOrchestrator.js'; import { ReferenceResolver } from '../templates/ReferenceResolver.js'; import { EntityRouter } from '../tools/EntityRouter.js'; import { orchestrationSamplesProvider } from './OrchestrationSamplesProvider.js'; import * as fs from 'fs'; import * as IndividualTools from './individual/index.js'; /** * Optimizely MCP Tools - Core Business Logic Implementation * @description Provides comprehensive business logic for Optimizely data operations * including feature flags, experiments, audiences, and analytics. This class implements * the business domain logic independent of any protocol layer (MCP, REST, CLI, etc.). * * Business Capabilities: * - Feature flag management and querying with environment status * - Experiment lifecycle management and detailed analysis * - Cross-environment comparison and configuration analysis * - Full-text search across all Optimizely entities * - Analytics and insights generation for project health * - Change history tracking and recent activity monitoring * * Architecture Pattern: * This class follows the adapter pattern where it serves as the stable business * logic layer. The MCP protocol layer wraps these methods without modification, * ensuring clean separation between business logic and protocol concerns. * * Error Handling: * Uses business-appropriate error handling with MCPErrorMapper for protocol * layer conversion. Errors are logged with appropriate context and converted * to MCP errors by the protocol layer, not within these business methods. * * Data Dependencies: * - CacheManager: Provides cached Optimizely data access * - SQLiteEngine: Underlying storage for complex queries * - Logger: Business operation logging and debugging */ export class OptimizelyMCPTools { configManager; /** Cache manager for accessing synchronized Optimizely data */ cache; /** Schema-aware entity creation system */ defaultsManager; schemaBuilder; /** OpenAPI reference handler */ openAPIHandler; /** Entity router for orchestrator to reuse */ entityRouter; /** Individual tool instances for modular architecture */ individualTools = {}; /** Session tracking for documentation presentation (Tool Handler Optimization) */ docsPresentedThisSession = new Set(); docsReadForComplexity = new Set(); /** * Creates a new Optimizely MCP Tools instance * @param cacheManager - Initialized cache manager for data access * @description Initializes the business logic layer with data access dependencies. * The cache manager should be properly initialized before passing to this constructor. */ syncScheduler; // Will be set if auto-sync is enabled constructor(cacheManager, configManager) { this.configManager = configManager; this.cache = cacheManager; // Initialize schema-aware entity creation system this.defaultsManager = new ConfigurableDefaultsManager(); this.schemaBuilder = new SchemaAwareEntityBuilder(this.defaultsManager); // Initialize OpenAPI reference handler this.openAPIHandler = new OpenAPIReferenceHandler(); // FIX: Create EntityRouter instance for orchestrator to reuse this.entityRouter = new EntityRouter(this.cache.client, this.cache, this.cache.storage); // Initialize individual tools this.initializeIndividualTools(); getLogger().info('OptimizelyMCPTools: Initialized with schema-aware entity creation and OpenAPI reference handler'); } /** * Initializes all individual tool instances with proper dependency injection */ initializeIndividualTools() { // Create comprehensive storage adapter that handles all SQLiteEngine interface differences const storageAdapter = { query: (sql, params) => this.cache.storage.query(sql, params || []), get: (sql, params) => this.cache.storage.get(sql, params || []), run: async (sql, params) => { await this.cache.storage.run(sql, params || []); // Convert return type to void as expected by tools }, all: (sql, params) => this.cache.storage.query(sql, params || []), // Use query as all getDatabase: () => this.cache.storage.getDatabase() }; // Create missing method implementations that tools require const missingMethods = { handleRecentChangesQuery: this.handleRecentChangesQuery.bind(this), formatDocumentationResponse: this._formatDocumentationResponse.bind(this), getRuleset: async (projectId, flagKey, environmentKey) => { // Placeholder implementation - will be implemented when needed throw new Error('getRuleset method not yet implemented in modular architecture'); }, transformRuleForFeatureExperimentation: (rule) => { // Placeholder implementation - will be implemented when needed return rule; // For now, just return the rule unchanged }, // Documentation-related methods docsReadForComplexity: this.docsReadForComplexity, markDocsReadFor: this.markDocsReadFor.bind(this), isComplexEntityType: this.isComplexEntityType.bind(this), getAnalyticsViewDocumentation: this.getAnalyticsViewDocumentation.bind(this), getUpdateTemplatesDocumentation: this.getUpdateTemplatesDocumentation.bind(this), getAllUpdateTemplatesDocumentation: this.getAllUpdateTemplatesDocumentation.bind(this), formatWorkflowTemplateResponse: this.formatWorkflowTemplateResponse.bind(this), // Core tool methods analyzeData: this.analyzeData.bind(this), listEntities: this.listEntities.bind(this), // Adapter for manageEntityLifecycle with different signatures manageEntityLifecycle: ((operation, entityType, entityDataOrId, entityIdOrKey, projectId, options) => { // Map the different signatures to the actual method if (typeof entityDataOrId === 'string' && !entityIdOrKey) { // GetFlagHistory signature: (operation, entityType, entityId, entityKey, projectId, data) return this.manageEntityLifecycle(operation, entityType, undefined, entityDataOrId, projectId, options); } else { // Default signature: (operation, entityType, entityData, entityId, projectId, options) return this.manageEntityLifecycle(operation, entityType, entityDataOrId, entityIdOrKey, projectId, options); } }).bind(this) }; // Base dependencies with comprehensive interface coverage const baseDeps = { storage: storageAdapter, logger: getLogger(), errorMapper: MCPErrorMapper, entityRouter: this.entityRouter, schemaBuilder: this.schemaBuilder, defaultsManager: this.defaultsManager, openAPIHandler: this.openAPIHandler, configManager: this.configManager, cacheManager: this.cache, apiClient: this.cache?.client, toolsInstance: this, // Some tools need reference to main tools instance ...missingMethods }; // Initialize all 30 individual tools try { // Phase 1 - Simple Tools (5) this.individualTools.list_projects = IndividualTools.createListProjectsTool(baseDeps); this.individualTools.get_project_data = IndividualTools.createGetProjectDataTool(baseDeps); this.individualTools.get_system_status = IndividualTools.createGetSystemStatusTool(baseDeps); this.individualTools.get_recommendations = IndividualTools.createGetRecommendationsTool(baseDeps); this.individualTools.get_optimization_analysis = IndividualTools.createGetOptimizationAnalysisTool(baseDeps); // Phase 2 - Moderate Complexity (8) this.individualTools.list_entities = IndividualTools.createListEntitiesTool(baseDeps); this.individualTools.get_entity_details = IndividualTools.createGetEntityDetailsTool(baseDeps); this.individualTools.get_entity_documentation = IndividualTools.createGetEntityDocumentationTool(baseDeps); this.individualTools.get_entity_templates = IndividualTools.createGetEntityTemplatesTool(baseDeps); this.individualTools.export_data = IndividualTools.createExportDataTool(baseDeps); this.individualTools.compare_environments = IndividualTools.createCompareEnvironmentsTool(baseDeps); this.individualTools.manage_cache = IndividualTools.createManageCacheTool(baseDeps); this.individualTools.archive_flags_bulk = IndividualTools.createArchiveFlagsBulkTool(baseDeps); // Phase 3 - Complex Tools (9) this.individualTools.get_results = IndividualTools.createGetResultsTool(baseDeps); this.individualTools.update_flags_bulk = IndividualTools.createUpdateFlagsBulkTool(baseDeps); this.individualTools.get_flag_history = IndividualTools.createGetFlagHistoryTool(baseDeps); this.individualTools.get_flag_entities = IndividualTools.createGetFlagEntitiesTool(baseDeps); this.individualTools.analyze_data = IndividualTools.createAnalyzeDataTool(baseDeps); this.individualTools.migrate_entities = IndividualTools.createMigrateEntitiesTool(baseDeps); this.individualTools.get_migration_status = IndividualTools.createGetMigrationStatusTool(baseDeps); this.individualTools.manage_flag_state = IndividualTools.createManageFlagStateTool(baseDeps); this.individualTools.update_ruleset = IndividualTools.createUpdateRulesetTool(baseDeps); // Phase 4 - Final Tools (8) this.individualTools.get_openapi_reference = IndividualTools.createGetOpenAPIReferenceTool(baseDeps); this.individualTools.get_optimizely_api_reference = IndividualTools.createGetOptimizelyAPIReferenceTool(baseDeps); this.individualTools.orchestrate_template = IndividualTools.createOrchestrateTemplateTool(baseDeps); this.individualTools.manage_orchestration_templates = IndividualTools.createManageOrchestrationTemplatesTool(baseDeps); this.individualTools.get_orchestration_samples = IndividualTools.createGetOrchestrationSamplesTool(baseDeps); this.individualTools.validate_template = IndividualTools.createValidateTemplateTool(baseDeps); this.individualTools.get_tool_reference = IndividualTools.createGetToolReferenceTool(baseDeps); this.individualTools.manage_entity_lifecycle = IndividualTools.createManageEntityLifecycleTool(baseDeps); getLogger().info('OptimizelyMCPTools: Successfully initialized all 30 individual tool modules'); } catch (error) { getLogger().error({ error: error instanceof Error ? error.message : String(error), stack: error instanceof Error ? error.stack : undefined }, 'OptimizelyMCPTools: Failed to initialize individual tools - falling back to monolithic implementation'); // Clear individual tools on failure to ensure fallback works this.individualTools = {}; } } /** * Sets the sync scheduler reference (called by the server if auto-sync is enabled) */ setSyncScheduler(scheduler) { this.syncScheduler = scheduler; } /** * TOOL HANDLER OPTIMIZATION HELPER METHODS * These methods support the enhanced tool handler architecture that improves * agent compliance by providing progressive documentation and complexity enforcement. */ /** * Gets the count of configured projects * @returns Number of projects configured in the cache */ getConfiguredProjectsCount() { try { // Get project filter to check if specific projects are configured const projectFilter = this.cache.getProjectFilter(); if (projectFilter && !projectFilter.isAutoDiscoverEnabled()) { const allowedIds = projectFilter.getAllowedProjectIds(); return allowedIds.length; } // For auto-discover mode, we don't know the exact count synchronously // Return a reasonable default return 5; // Typical number of projects } catch (error) { getLogger().warn({ error }, 'Failed to get configured projects count'); return 1; // Default to 1 if unable to determine } } /** * Checks if an entity type is complex and requires special guidance * @param entityType - The entity type to check * @returns True if entity type has complex creation patterns */ isComplexEntityType(entityType) { const complexEntityTypes = ['flag', 'experiment', 'audience', 'campaign']; return complexEntityTypes.includes(entityType); } /** * Checks if entity creation data indicates complex operation requiring template mode * @param entityType - The entity type being created * @param entityData - The creation data provided * @returns True if operation appears complex */ isComplexCreation(entityType, entityData) { if (!entityData) return false; switch (entityType) { case 'flag': return !!(entityData.ab_test || entityData.variations || entityData.rules || entityData.variables); case 'experiment': return !!(entityData.variations || entityData.page || entityData.new_page || entityData.events); case 'audience': return !!(entityData.custom_attributes || entityData.conditions); default: return false; } } /** * Checks if entity update data indicates complex operation requiring template mode * @param entityType - The entity type being updated * @param entityData - The update data provided * @returns True if operation appears complex */ isComplexUpdate(entityType, entityData) { if (!entityData) return false; switch (entityType) { case 'flag': // Complex flag updates return !!(entityData.ab_test || entityData.variations || entityData.metrics || entityData.traffic_allocation || entityData.new_variation || entityData.variables || entityData.variable_definitions || entityData.audience_conditions); case 'experiment': // Complex experiment updates return !!(entityData.variation || entityData.new_variation || entityData.traffic_allocation || entityData.page_ids || entityData.url_targeting || entityData.metrics); case 'audience': // Complex audience updates return !!(entityData.conditions || entityData.new_attributes || entityData.custom_attributes); case 'page': // Complex page updates return !!(entityData.conditions || entityData.url_targeting || entityData.activation_type || entityData.activation_code); case 'campaign': // Complex campaign updates return !!(entityData.new_experiments || entityData.experiments || entityData.page_ids || entityData.url_targeting || entityData.experiment_priorities || entityData.metrics); default: return false; } } /** * Checks if entity update data is simple enough for direct operation (opposite of isComplexUpdate) * @param entityType - The entity type being updated * @param entityData - The update data provided * @returns True if operation is simple and can bypass template orchestration */ isSimpleUpdate(entityType, entityData) { if (!entityData) return false; // Check if any complex fields are present const complexUpdate = this.isComplexUpdate(entityType, entityData); if (complexUpdate) return false; // Additional checks for specific entity types switch (entityType) { case 'flag': // Simple flag updates: basic fields only const flagFields = Object.keys(entityData).filter(k => !k.startsWith('_') && k !== 'entity_id'); const simpleFlag = flagFields.every(field => ['name', 'description', 'archived'].includes(field)); return simpleFlag; case 'experiment': // Simple experiment updates: basic fields only const experimentFields = Object.keys(entityData).filter(k => !k.startsWith('_') && k !== 'entity_id'); const simpleExperiment = experimentFields.every(field => ['name', 'description', 'status'].includes(field)); return simpleExperiment; case 'audience': // Simple audience updates: basic fields only const audienceFields = Object.keys(entityData).filter(k => !k.startsWith('_') && k !== 'entity_id'); const simpleAudience = audienceFields.every(field => ['name', 'description', 'archived'].includes(field)); return simpleAudience; default: // For other entity types, assume simple if no complex indicators return true; } } /** * Gets template mode information for an entity type * @param entityType - The entity type * @returns Template mode availability info */ getTemplateInfo(entityType) { const templateInfo = { flag: { available: true, when_needed: "Creating flags with A/B tests, variables, or complex rules", benefit: "Automatically creates variables, variations, and rulesets" }, experiment: { available: true, when_needed: "Creating experiments with new pages or multiple variations", benefit: "Automatically creates pages, events, and proper variation structure" }, audience: { available: true, when_needed: "Creating audiences with custom attributes", benefit: "Automatically creates required attribute definitions" } }; return templateInfo[entityType] || { available: false }; } /** * Gets complexity note for entity type * @param entityType - The entity type * @returns Complexity guidance note */ getComplexityNote(entityType) { const notes = { flag: "Flags with A/B tests require template mode for proper orchestration", experiment: "Experiments with variations often need pages and events created together", audience: "Audiences with custom attributes require attribute creation first", campaign: "Campaigns typically need audiences and experiments configured properly" }; return notes[entityType] || "Simple creation available for most use cases"; } /** * Gets complexity indicators from entity data * @param entityType - The entity type * @param entityData - The entity creation data * @returns Array of detected complexity indicators */ getComplexityIndicators(entityType, entityData) { const indicators = []; if (entityType === 'flag') { if (entityData.ab_test) indicators.push("A/B test configuration detected"); if (entityData.variations) indicators.push("Multiple variations specified"); if (entityData.variables) indicators.push("Variable definitions provided"); if (entityData.rules) indicators.push("Complex rules configuration"); } if (entityType === 'experiment') { if (entityData.variations) indicators.push("Variations require proper page targeting"); if (entityData.page || entityData.new_page) indicators.push("New page creation needed"); if (entityData.events) indicators.push("Event tracking configuration"); } return indicators; } /** * Marks documentation as read for complexity enforcement * @param entityType - The entity type documentation was read for */ markDocsReadFor(entityType) { this.docsReadForComplexity.add(entityType); } /** * Checks if documentation has been read for an entity type * @param entityType - The entity type to check * @returns True if docs were read this session */ hasReadDocsFor(entityType) { return this.docsReadForComplexity.has(entityType); } /** * Lists all projects with basic statistics - simple, direct answer * @returns Promise resolving to project summary with counts * @description Provides immediate project overview with key metrics. * This is the primary tool for answering "how many projects do I have?" * and similar basic queries without complex filtering. */ async listProjects() { // Delegate to individual tool return this.individualTools.list_projects.handler({}); /* SHADOW IMPLEMENTATION - TO BE REMOVED getLogger().info('OptimizelyMCPTools.listProjects: Getting project overview'); try { if (!this.cache) { throw MCPErrorMapper.toMCPError( new Error("Cache manager not initialized"), { operation: 'List projects overview' } ); } // Get ALL projects with counts using SQL aggregation // CRITICAL: Query all projects first, then apply filter in memory (like EntityRouter does) // This ensures newly created projects added via addAllowedProjectId() are included const allProjects = await this.cache.storage.query(` SELECT p.id, p.name, p.platform, p.is_flags_enabled, p.account_id, p.last_modified, (SELECT COUNT(*) FROM flags WHERE project_id = p.id) as flag_count, -- Platform-aware experiment counting CASE WHEN p.platform = 'custom' OR p.is_flags_enabled = 1 THEN -- Feature Experimentation: Count A/B test rulesets in flag_environments (SELECT COUNT(*) FROM flag_environments WHERE project_id = p.id AND (data_json LIKE '%"type":"a/b_test"%' OR data_json LIKE '%"type":"experiment"%')) ELSE -- Web Experimentation: Count standalone experiments (SELECT COUNT(*) FROM experiments WHERE project_id = p.id) END as experiment_count, (SELECT COUNT(*) FROM environments WHERE project_id = p.id) as environment_count FROM projects p ORDER BY p.name `, []); // DO NOT apply project filter here - we want to show ALL projects in the database // The filter should only be applied during sync operations, not when querying existing data let projects = allProjects; const projectSummaries: ProjectSummary[] = projects.map(p => { const isFeatureExperimentation = p.platform === 'custom' || Boolean(p.is_flags_enabled); return { id: p.id, name: p.name, platform: p.platform || 'unknown', is_flags_enabled: Boolean(p.is_flags_enabled), account_id: p.account_id, flag_count: p.flag_count || 0, experiment_count: p.experiment_count || 0, environment_count: p.environment_count || 0, last_modified: p.last_modified, platform_type: isFeatureExperimentation ? 'Feature Experimentation' : 'Web Experimentation', experiment_explanation: isFeatureExperimentation ? 'A/B tests are RULESETS with type="a/b_test" within flags. Query with: analyze_data from flags_unified_view WHERE rule_type="a/b" OR list_entities with entity_type="ruleset"' : 'Experiments are standalone entities. Query with: analyze_data from experiments_unified_view WHERE platform="web" OR list_entities with entity_type="experiment"' }; }); // Calculate summary statistics const featureExperimentationProjects = projectSummaries.filter(p => p.platform_type === 'Feature Experimentation'); const webExperimentationProjects = projectSummaries.filter(p => p.platform_type === 'Web Experimentation'); const summary = { flags_enabled_projects: projectSummaries.filter(p => p.is_flags_enabled).length, total_flags: projectSummaries.reduce((sum, p) => sum + p.flag_count, 0), total_experiments: projectSummaries.reduce((sum, p) => sum + p.experiment_count, 0), platforms: projectSummaries.reduce((acc, p) => { acc[p.platform] = (acc[p.platform] || 0) + 1; return acc; }, {} as Record<string, number>), platform_breakdown: { feature_experimentation: { count: featureExperimentationProjects.length, experiment_note: "Experiment counts represent A/B test rulesets (type='a/b_test') within flags", total_experiments: featureExperimentationProjects.reduce((sum, p) => sum + p.experiment_count, 0) }, web_experimentation: { count: webExperimentationProjects.length, experiment_note: "Experiment counts represent standalone experiment entities", total_experiments: webExperimentationProjects.reduce((sum, p) => sum + p.experiment_count, 0) } } }; return { result: "success", metadata: { operation: "list_projects", total_count: projectSummaries.length, operation_successful: true, timestamp: new Date().toISOString() }, total_projects: projectSummaries.length, projects: projectSummaries, summary, _platform_education: { key_understanding: "Experiment counts are platform-specific", feature_experimentation: "A/B tests are rulesets within flags, not standalone experiments", web_experimentation: "Experiments are standalone entities with their own variations", how_to_find_experiments: { feature_experimentation: "Use analyze_data with flags_unified_view WHERE rule_type='a/b' OR use list_entities with entity_type='ruleset' and filter type='a/b_test'", web_experimentation: "Use analyze_data with experiments_unified_view WHERE platform='web' OR use list_entities with entity_type='experiment'" } as any } }; } catch (error: any) { getLogger().error({ error: error.message, stack: error.stack }, 'OptimizelyMCPTools.listProjects: Failed to get project overview'); throw MCPErrorMapper.toMCPError(error, 'Failed to get project overview'); } */ } /** * Diagnoses sync issues and provides troubleshooting information * @returns Promise resolving to diagnostic information * @description Helps troubleshoot why flags might not be syncing for certain projects */ /** * Get diagnostic information (wrapper for backward compatibility) * @returns Diagnostics data * @deprecated Use getSystemStatus({ include_diagnostics: true }) instead */ async getDiagnostics() { getLogger().warn('DEPRECATED: getDiagnostics called directly. Use getSystemStatus({ include_diagnostics: true }) instead'); // Get system status with diagnostics const result = await this.getSystemStatus({ include_diagnostics: true, detailed: true }); // Extract and return just the diagnostics data for backward compatibility return { database_status: result.diagnostics.database_status, project_types: result.diagnostics.project_types, sync_issues: result.diagnostics.sync_issues, recommendations: result.diagnostics.recommendations, auto_sync_status: result.diagnostics.auto_sync_status }; } /** * Unified cache management method that consolidates initialize_cache and refresh_cache * @param args - Cache management arguments * @returns Promise resolving to cache operation results */ async manageCache(args) { const { operation, project_id, options = {} } = args; getLogger().info({ operation, project_id, options }, 'OptimizelyMCPTools.manageCache: Starting cache management operation'); // Validate operation if (!['initialize', 'refresh', 'clear'].includes(operation)) { throw MCPErrorMapper.toMCPError(new Error(`Invalid operation: ${operation}. Must be 'initialize', 'refresh', or 'clear'`), { operation: 'manage_cache' }); } // Validate operation-specific parameters if (operation === 'initialize') { // Initialize doesn't support project_id or refresh-specific options if (project_id) { throw MCPErrorMapper.toMCPError(new Error("Cannot specify project_id for initialize operation - initialization syncs all projects"), { operation: 'manage_cache' }); } if (options.force || options.incremental || options.views_only) { throw MCPErrorMapper.toMCPError(new Error("Options 'force', 'incremental', and 'views_only' are not valid for initialize operation"), { operation: 'manage_cache' }); } } if (operation === 'clear') { // Clear only supports wait_for_completion if (project_id) { throw MCPErrorMapper.toMCPError(new Error("Cannot specify project_id for clear operation - clear removes all cached data"), { operation: 'manage_cache' }); } if (options.force || options.incremental || options.views_only) { throw MCPErrorMapper.toMCPError(new Error("Options 'force', 'incremental', and 'views_only' are not valid for clear operation"), { operation: 'manage_cache' }); } } // Route to appropriate operation switch (operation) { case 'initialize': return this.initializeCache({ wait_for_completion: options.wait_for_completion, progress_callback: options.progress_callback }); case 'refresh': return this.refreshCache({ projectId: project_id, force: options.force, incremental: options.incremental, views_only: options.views_only, progressCallback: options.progress_callback }); case 'clear': return this.clearCache({ wait_for_completion: options.wait_for_completion, progress_callback: options.progress_callback }); default: // This should never happen due to validation above throw MCPErrorMapper.toMCPError(new Error(`Unhandled operation: ${operation}`), { operation: 'manage_cache' }); } } /** * Initializes an empty cache with data from Optimizely (wrapper for backward compatibility) * @param options - Initialization options * @returns Promise resolving to initialization results * @deprecated Use manageCache with operation='initialize' instead */ async initializeCache(options) { getLogger().warn('DEPRECATED: initializeCache called directly. Use manageCache with operation="initialize" instead'); if (!this.cache) { throw MCPErrorMapper.toMCPError(new Error("Cache manager not initialized"), { operation: 'Initialize cache' }); } // Check if cache is already initialized const syncStatus = await this.cache.getSyncStatus(); if (syncStatus.project_count > 0) { return { success: true, message: 'Cache is already initialized', projectsSynced: syncStatus.project_count, lastFullSync: syncStatus.last_full_sync, lastIncrementalSync: syncStatus.last_incremental_sync }; } // Start initialization const startTime = Date.now(); const syncPromise = this.cache.fullSync(null, options?.progress_callback); // If user wants to wait for completion if (options?.wait_for_completion) { const result = await syncPromise; return { success: true, message: 'Cache initialization completed successfully', projectsSynced: result.projectsSynced, duration: Date.now() - startTime, timestamp: new Date().toISOString() }; } // Return immediately, sync continues in background syncPromise.catch(error => { getLogger().error({ error: error.message }, 'Background cache initialization failed'); }); return { success: true, message: 'Cache initialization started in background', estimatedDuration: '7-10 minutes', timestamp: new Date().toISOString() }; } /** * Clears all cached data * @param options - Clear options * @returns Promise resolving to clear results */ async clearCache(options) { getLogger().info('OptimizelyMCPTools.clearCache: Clearing all cached data'); if (!this.cache) { throw MCPErrorMapper.toMCPError(new Error("Cache manager not initialized"), { operation: 'Clear cache' }); } try { // Call cache manager's init with confirmReset to clear database await this.cache.init({ confirmReset: true }); return { success: true, message: 'Cache cleared successfully', timestamp: new Date().toISOString(), nextStep: 'Use manage_cache with operation="initialize" to repopulate cache' }; } catch (error) { getLogger().error({ error: error.message }, 'Failed to clear cache'); throw MCPErrorMapper.toMCPError(error, { operation: 'Clear cache' }); } } /** * Refreshes the cache by performing a full sync * @param options - Options for the refresh operation * @returns Promise resolving to refresh results * @description Forces a complete refresh of all cached data from Optimizely API * @deprecated Use manageCache with operation='refresh' instead */ async refreshCache(options) { getLogger().warn('DEPRECATED: refreshCache called directly. Use manageCache with operation="refresh" instead'); getLogger().info({ options }, 'OptimizelyMCPTools.refreshCache: Starting cache refresh'); const startTime = Date.now(); try { if (!this.cache) { throw MCPErrorMapper.toMCPError(new Error("Cache manager not initialized"), { operation: 'Refresh cache' }); } // Handle views_only option - recreate views without any data operations if (options?.views_only) { getLogger().debug('OptimizelyMCPTools.refreshCache: Views-only refresh requested'); // Validate conflicting options if (options.force) { throw MCPErrorMapper.toMCPError(new Error("Cannot use 'force' with 'views_only' - force clears data but views_only preserves it"), { operation: 'Refresh cache' }); } if (options.incremental) { throw MCPErrorMapper.toMCPError(new Error("Cannot use 'incremental' with 'views_only' - incremental syncs data but views_only only recreates views"), { operation: 'Refresh cache' }); } if (options.projectId) { getLogger().warn('Project ID ignored for views_only refresh - views are global across all projects'); } try { // Get database instance const db = this.cache.storage.getDatabase(); if (!db) { throw new Error('Database instance not available'); } // Report progress if (options.progressCallback) { options.progressCallback({ phase: 'Recreating views', current: 0, total: 1, message: 'Recreating all database views...', percent: 0 }); } // Use ViewManager to recreate all views const { ViewManager } = await import('../storage/ViewManager.js'); const viewManager = new ViewManager(db); // Get stats before recreation const viewsBefore = viewManager.getViewStats(); // Recreate all views (drops existing and creates new) await viewManager.createAllViews(); // Validate views were created correctly const validation = viewManager.validateViews(); if (!validation.valid) { throw new Error(`View validation failed: ${validation.missing.length} missing views: ${validation.missing.join(', ')}`); } // Report completion if (options.progressCallback) { options.progressCallback({ phase: 'Recreating views', current: 1, total: 1, message: `Successfully recreated ${viewsBefore.total} views`, percent: 100 }); } const duration = Date.now() - startTime; return { success: true, projectsSynced: 0, // No projects synced in views_only mode duration, timestamp: new Date().toISOString(), message: `Successfully recreated ${viewsBefore.total} views without affecting cached data`, viewsRecreated: viewsBefore.total }; } catch (error) { getLogger().error('OptimizelyMCPTools.refreshCache: Failed to recreate views', error instanceof Error ? error.message : String(error)); throw MCPErrorMapper.toMCPError(error, { operation: 'Recreate views' }); } } // If force is true AND incremental is not requested, clear existing data first if (options?.force && !options?.incremental) { getLogger().debug('OptimizelyMCPTools.refreshCache: Force refresh requested, clearing existing data'); // Report progress for cache clearing if (options.progressCallback) { options.progressCallback({ phase: 'Clearing cache', current: 0, total: 1, message: 'Clearing existing cache data...', percent: 0 }); } // Delete in proper order to respect foreign key constraints // Child tables first, then parent tables (matches SQLiteEngine.reset() order) // Use a transaction with foreign keys temporarily disabled to avoid constraint issues await this.cache.storage.run('BEGIN TRANSACTION'); try { // Temporarily disable foreign keys for bulk deletion await this.cache.storage.run('PRAGMA foreign_keys = OFF'); // Delete all tables in reverse dependency order await this.cache.storage.run('DELETE FROM change_history'); await this.cache.storage.run('DELETE FROM sync_state'); await this.cache.storage.run('DELETE FROM sync_metadata'); await this.cache.storage.run('DELETE FROM changes'); await this.cache.storage.run('DELETE FROM reports'); await this.cache.storage.run('DELETE FROM rules'); await this.cache.storage.run('DELETE FROM rulesets'); await this.cache.storage.run('DELETE FROM variable_definitions'); await this.cache.storage.run('DELETE FROM variations'); await this.cache.storage.run('DELETE FROM experiment_results'); await this.cache.storage.run('DELETE FROM flag_environments'); await this.cache.storage.run('DELETE FROM flags'); await this.cache.storage.run('DELETE FROM features'); await this.cache.storage.run('DELETE FROM experiments'); await this.cache.storage.run('DELETE FROM campaigns'); await this.cache.storage.run('DELETE FROM pages'); await this.cache.storage.run('DELETE FROM audiences'); await this.cache.storage.run('DELETE FROM attributes'); await this.cache.storage.run('DELETE FROM events'); await this.cache.storage.run('DELETE FROM collaborators'); await this.cache.storage.run('DELETE FROM groups'); await this.cache.storage.run('DELETE FROM extensions'); await this.cache.storage.run('DELETE FROM webhooks'); await this.cache.storage.run('DELETE FROM list_attributes'); await this.cache.storage.run('DELETE FROM environments'); // Delete projects last since everything depends on them await this.cache.storage.run('DELETE FROM projects'); // Re-enable foreign keys await this.cache.storage.run('PRAGMA foreign_keys = ON'); await this.cache.storage.run('COMMIT'); getLogger().debug('OptimizelyMCPTools.refreshCache: Successfully cleared all cache data'); // Recreate views after clearing data getLogger().debug('OptimizelyMCPTools.refreshCache: Recreating database views'); // Report progress for view recreation if (options.progressCallback) { options.progressCallback({ phase: 'Recreating views', current: 0, total: 1, message: 'Recreating database views...', percent: 0 }); } // Import ViewManager and recreate views const { ViewManager } = await import('../storage/ViewManager.js'); const db = this.cache.storage.getDatabase(); if (!db) { throw new Error('Database instance not available'); } const viewManager = new ViewManager(db); await viewManager.createAllViews(); getLogger().debug('OptimizelyMCPTools.refreshCache: Successfully recreated views'); // Report progress for cache clearing completion if (options.progressCallback) { options.progressCallback({ phase: 'Clearing cache', current: 1, total: 1, message: 'Cache cleared and views recreated successfully', percent: 100 }); } } catch (error) { await this.cache.storage.run('ROLLBACK'); await this.cache.storage.run('PRAGMA foreign_keys = ON'); throw error; } } // Perform sync - prioritize incremental over force to prevent accidental data loss let result; if (options?.incremental) { // If user requests incremental, honor it even if force is also set getLogger().debug('OptimizelyMCPTools.refreshCache: Using incremental sync (incremental takes precedence over force)'); result = await this.cache.incrementalSync(options?.projectId); return { success: result.success, projectsSynced: result.projectsSynced, duration: result.duration, timestamp: result.timestamp, totalChanges: result.totalChanges, totalCreated: result.totalCreated, totalUpdated: result.totalUpdated, totalDeleted: result.totalDeleted, message: result.totalChanges === 0 ? 'No changes detected since last sync' : `Incrementally synced ${result.totalChanges} changes (${result.totalCreated} created, ${result.totalUpdated} updated, ${result.totalDeleted} deleted)` }; } else { // Full sync with progress callback result = await this.cache.fullSync(options?.projectId, options?.progressCallback); } // ALWAYS recreate views after any sync operation to ensure they're up to date getLogger().debug('OptimizelyMCPTools.refreshCache: Recreating database views after sync'); // Import ViewManager and recreate views const { ViewManager } = await import('../storage/ViewManager.js'); const db = this.cache.storage.getDatabas