UNPKG

@simonecoelhosfo/optimizely-mcp-server

Version:

Optimizely MCP Server for AI assistants with integrated CLI tools

492 lines 19.7 kB
/** * Step Executor * @description Executes individual orchestration steps * @author Optimizely MCP Server * @version 1.0.0 */ import { getLogger } from '../../logging/Logger.js'; import { DependencyResolver } from './DependencyResolver.js'; import { IsolatedVMSandbox } from '../plugins/IsolatedVMSandbox.js'; import { EntityOrchestrator } from '../EntityOrchestrator.js'; import { PreExecutionValidator } from '../validators/PreExecutionValidator.js'; export class StepExecutor { logger = getLogger(); context; dependencyResolver; sandbox; mcpTools; adoptionService; preExecutionValidator; constructor(context) { this.context = context; this.dependencyResolver = new DependencyResolver(); this.preExecutionValidator = new PreExecutionValidator(); // EntityAdoptionService will be injected via setAdoptionService method } /** * Set the adoption service - called by TemplateOrchestrationEngine */ setAdoptionService(adoptionService) { this.adoptionService = adoptionService; } /** * Extract project ID from various sources */ extractProjectId(state) { return state.parameters.project_id || this.context.project_id || ''; } /** * Create EntityOrchestrator instance with proper dependencies */ createEntityOrchestrator() { const mcpToolsInternal = this.mcpTools; return new EntityOrchestrator(mcpToolsInternal.apiHelper, mcpToolsInternal.cacheManager, mcpToolsInternal.storage, mcpToolsInternal.entityRouter); } /** * Execute a single step based on its type */ async executeStep(step, state, context) { this.logger.info({ stepId: step.id, stepType: step.type, stepName: step.name }, 'Executing step'); const startTime = Date.now(); try { let result; switch (step.type) { case 'template': result = await this.executeTemplateStep(step.template, step.id, state); break; case 'conditional': result = await this.executeConditionalStep(step.condition, state, context); break; case 'loop': result = await this.executeLoopStep(step.loop, state, context); break; case 'plugin': result = await this.executePluginStep(step.plugin, state); break; case 'wait': result = await this.executeWaitStep(step.wait, state); break; case 'parallel': result = await this.executeParallelStep(step.parallel, state, context); break; default: throw new Error(`Unknown step type: ${step.type}`); } const executionTime = Date.now() - startTime; this.logger.info({ stepId: step.id, executionTime, resultType: typeof result }, 'Step executed successfully'); return result; } catch (error) { const executionTime = Date.now() - startTime; this.logger.error({ stepId: step.id, error: error instanceof Error ? error.message : String(error), executionTime }, 'Step execution failed'); throw error; } } /** * Execute template step using new Direct Template Architecture */ async executeTemplateStep(config, effectiveStepId, state) { // Resolve variables and parameters const cleanInputs = await this.resolveVariables(config.template_data || config.entity_data || config.inputs || {}, state); // Extract required fields const entityType = config.entity_type; const operation = config.operation || 'create'; const mode = config.mode || 'direct'; // Validate required fields if (!entityType) { throw new Error(`Step ${effectiveStepId} missing required field: entity_type`); } // Initialize MCP tools if needed if (!this.mcpTools) { if (this.context.mcpTools) { this.mcpTools = this.context.mcpTools; } else { if (!this.context.cacheManager) { throw new Error('Either mcpTools or cacheManager is required in OrchestrationContext'); } const { OptimizelyMCPTools } = await import('../../tools/OptimizelyMCPTools.js'); this.mcpTools = new OptimizelyMCPTools(this.context.cacheManager); } } // Handle adopt_if_exists (default true for create operations) const shouldCheckAdoption = operation === 'create' && (config.adopt_if_exists !== false) && this.adoptionService; if (shouldCheckAdoption && this.adoptionService) { const adoptionResult = await this.adoptionService.checkForAdoption(entityType, cleanInputs, this.extractProjectId(state), { checkApi: false, // FIX: Changed from true to prevent Spanish audience bug adoptionStrategy: 'always' }); if (adoptionResult.found) { // Return adopted entity return this.formatAdoptionResult(adoptionResult, entityType, effectiveStepId); } } // Prepare data for entity creation const templateData = { ...cleanInputs, // Ensure project_id is set project_id: cleanInputs.project_id || this.extractProjectId(state) }; // Pre-execution validation if (this.preExecutionValidator) { const validationResult = this.preExecutionValidator.validate(entityType, templateData, { additionalContext: { operation, mode, projectId: templateData.project_id } }); if (!validationResult.valid) { throw new Error(`Validation failed: ${validationResult.errors?.join('; ')}`); } } // Execute via EntityOrchestrator const entityOrchestrator = this.createEntityOrchestrator(); const result = await entityOrchestrator.executeDirectStep(templateData.project_id, { stepName: effectiveStepId, entityType, operation, data: templateData, dependencies: [], optional: config.optional || false }, state); // Store result in state this.storeEntityInState(result, entityType, effectiveStepId, state); return result; } /** * Execute conditional step */ async executeConditionalStep(config, state, context) { // Evaluate condition const conditionResult = await this.evaluateCondition(config.if, state); this.logger.debug({ condition: config.if, result: conditionResult }, 'Evaluated condition'); // Execute appropriate branch const stepsToExecute = conditionResult ? config.then : (config.else || []); const results = []; for (const step of stepsToExecute) { const result = await this.executeStep(step, state, context); results.push(result); } return results; } /** * Execute loop step */ async executeLoopStep(config, state, context) { // Debug logging console.log(`🔄 [LoopStep] Starting loop execution`); console.log(`🔄 [LoopStep] config.over: ${config.over}`); console.log(`🔄 [LoopStep] state.parameters:`, state.parameters); // Handle template expressions (${variable}) vs JSONPath expressions let array; if (config.over.startsWith('${') && config.over.endsWith('}')) { // This is a template expression like ${campaign_numbers} const varName = config.over.slice(2, -1); // Remove ${ and } console.log(`🔄 [LoopStep] Template expression detected, looking for: ${varName}`); // First check parameters, then variables, then outputs array = state.parameters[varName] || state.variables[varName] || state.outputs[varName]; console.log(`🔄 [LoopStep] Found in parameters: ${!!state.parameters[varName]}`); console.log(`🔄 [LoopStep] Found in variables: ${!!state.variables[varName]}`); console.log(`🔄 [LoopStep] Found in outputs: ${!!state.outputs[varName]}`); } else { // This is a JSONPath expression const arrayPath = config.over.startsWith('$') ? config.over : `$.${config.over}`; console.log(`🔄 [LoopStep] JSONPath expression: ${arrayPath}`); array = state.get(arrayPath.substring(2)); // Remove '$.' } console.log(`🔄 [LoopStep] Array found: ${!!array}, type: ${typeof array}, isArray: ${Array.isArray(array)}`); if (array) { console.log(`🔄 [LoopStep] Array contents:`, JSON.stringify(array)); console.log(`🔄 [LoopStep] Array constructor:`, array.constructor.name); } if (!Array.isArray(array)) { // Special handling for array-like objects if (array && typeof array === 'object' && 'length' in array) { console.log(`🔄 [LoopStep] Found array-like object, converting to array`); array = Array.from(array); } else { throw new Error(`Loop 'over' must reference an array, got: ${typeof array}`); } } const results = []; const maxIterations = config.max_iterations || array.length; for (let i = 0; i < Math.min(array.length, maxIterations); i++) { // Set iterator variables state.variables[config.iterator] = array[i]; if (config.index) { state.variables[config.index] = i; } // Check break condition if (config.break_if && await this.evaluateCondition(config.break_if, state)) { this.logger.debug({ iteration: i, breakCondition: config.break_if }, 'Breaking loop'); break; } // Execute loop steps const stepResults = []; for (const step of config.steps) { const result = await this.executeStep(step, state, context); stepResults.push(result); } results.push(stepResults); } // Clean up iterator variables delete state.variables[config.iterator]; if (config.index) { delete state.variables[config.index]; } return results; } /** * Execute plugin step */ async executePluginStep(config, state) { // Initialize sandbox if needed if (!this.sandbox) { this.sandbox = new IsolatedVMSandbox({ memoryLimit: 128, timeout: 30000, inspector: true }); } // Resolve inputs const resolvedInputs = config.inputs ? await this.dependencyResolver.resolveReferences(config.inputs, state) : {}; // Create plugin context const pluginContext = { entityRouter: this.context.entityRouter, metricsProvider: this.context.metricsProvider, state, parameters: { ...state.parameters, ...resolvedInputs }, outputs: state.outputs, logger: this.logger, permissions: this.getPluginPermissions() }; // Execute plugin code const result = await this.sandbox.execute(config.code, pluginContext); if (!result.success) { throw new Error(`Plugin execution failed: ${result.error}`); } // Store outputs if specified if (config.outputs && result.output) { config.outputs.forEach(key => { if (result.output[key] !== undefined) { state.outputs[key] = result.output[key]; } }); } return result.output; } /** * Execute wait step */ async executeWaitStep(config, state) { if (config.duration) { // Simple duration wait this.logger.debug({ duration: config.duration }, 'Waiting for duration'); await new Promise(resolve => setTimeout(resolve, config.duration)); } else if (config.until) { // Wait until condition is true const maxWait = config.max_wait || 300000; // 5 minutes default const pollInterval = config.poll_interval || 1000; // 1 second default const startTime = Date.now(); this.logger.debug({ condition: config.until, maxWait, pollInterval }, 'Waiting for condition'); while (Date.now() - startTime < maxWait) { if (await this.evaluateCondition(config.until, state)) { return; } await new Promise(resolve => setTimeout(resolve, pollInterval)); } throw new Error(`Wait condition timeout: ${config.until}`); } } /** * Execute parallel step */ async executeParallelStep(config, state, context) { const maxConcurrency = config.max_concurrency || config.steps.length; const results = []; // Execute in batches based on max concurrency for (let i = 0; i < config.steps.length; i += maxConcurrency) { const batch = config.steps.slice(i, i + maxConcurrency); const batchPromises = batch.map(step => this.executeStep(step, state, context).catch(error => { if (config.fail_fast) { throw error; } return { error: error.message }; })); const batchResults = await Promise.all(batchPromises); results.push(...batchResults); } return results; } /** * Execute plugin code directly (for hooks) */ async executePluginCode(code, state) { return this.executePluginStep({ code, inputs: {} }, state); } /** * Evaluate a condition expression */ async evaluateCondition(condition, state) { try { // Use JSONPath to evaluate const result = state.get(condition); // Convert to boolean return !!result; } catch (error) { this.logger.warn({ condition, error: error instanceof Error ? error.message : String(error) }, 'Failed to evaluate condition'); return false; } } /** * Parse template ID to extract entity type and operation */ parseTemplateId(templateId) { // Format: entity_operation (e.g., flag_create, audience_update) const parts = templateId.split('_'); if (parts.length < 2) { throw new Error(`Invalid template ID format: ${templateId}`); } const operation = parts[parts.length - 1]; const entityType = parts.slice(0, -1).join('_'); return [entityType, operation]; } /** * Get plugin permissions based on context */ getPluginPermissions() { // Default permissions - can be customized based on template or user return { read_entities: true, write_state: true, access_metrics: true, external_requests: false // Disabled by default for security }; } /** * Resolve variables using dependency resolver */ async resolveVariables(inputs, state) { // Use the existing dependency resolver for JSONPath resolution return await this.dependencyResolver.resolveReferences(inputs, state); } /** * Format adoption result to match expected structure */ formatAdoptionResult(adoptionResult, entityType, stepId) { return { success: true, message: `${entityType} adopted successfully!`, data: { result: 'success', metadata: { summary: { headline: `${entityType} '${adoptionResult.entity.name || adoptionResult.entity.key}' adopted successfully`, facts: { entity_id: adoptionResult.entity.id, adopted: true, adoption_reason: adoptionResult.adoptionReason } }, entity_type: entityType, operation: 'adopt', timestamp: new Date().toISOString() }, data: adoptionResult.entity, _adoptionInfo: { adopted: true, reason: adoptionResult.adoptionReason, searchCriteria: adoptionResult.searchCriteria } } }; } /** * Store entity result in state */ storeEntityInState(result, entityType, stepId, state) { if (result && (result.result === 'success' || result.success) && result.data) { const responseData = result.data; const createdEntity = responseData.data || responseData; const entityKey = `created_${entityType}_${stepId}`; state.variables[entityKey] = createdEntity; // Store in created_entities array if (!state.outputs.created_entities) { state.outputs.created_entities = []; } const entityEntry = { step_id: stepId, entity_type: entityType, entity_id: createdEntity.id, entity_key: createdEntity.key, entity_name: createdEntity.name, data: createdEntity }; state.outputs.created_entities.push(entityEntry); state.recordStepComplete(stepId, result); this.logger.debug({ stepId, entityType, entityId: createdEntity.id, entityKey: createdEntity.key }, 'Stored created entity in state'); } else { // Handle error cases if (result?.error || result?.status === 'HARD_STOP_REQUIRED') { const errorMessage = result.error?.message || result.message || 'Entity creation failed'; const errorType = result.error?.type || result.status || 'UNKNOWN_ERROR'; const stepError = new Error(`${errorType}: ${errorMessage}`); state.recordStepError(stepId, stepError); throw stepError; } const formatError = new Error(`Unexpected result format from executeDirectStep for ${entityType}`); state.recordStepError(stepId, formatError); throw formatError; } } /** * Clean up resources */ async destroy() { if (this.sandbox) { await this.sandbox.destroy(); } } } //# sourceMappingURL=StepExecutor.js.map