@simonecoelhosfo/optimizely-mcp-server
Version:
Optimizely MCP Server for AI assistants with integrated CLI tools
492 lines • 19.7 kB
JavaScript
/**
* 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