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