UNPKG

@simonecoelhosfo/optimizely-mcp-server

Version:

Optimizely MCP Server for AI assistants with integrated CLI tools

364 lines 14.1 kB
/** * Template Executor - Actually executes templates against Optimizely API */ import axios from 'axios'; import chalk from 'chalk'; import { TemplateValidator } from './TemplateValidator'; import { ReferenceResolver } from './ReferenceResolver'; export class TemplateExecutor { apiClient; validator; resolver; constructor() { this.validator = new TemplateValidator(); this.resolver = new ReferenceResolver(); this.apiClient = axios.create({ baseURL: 'https://api.optimizely.com/v2', headers: { 'Content-Type': 'application/json' } }); } /** * Execute a template */ async execute(template, context, options = {}) { // Set API token this.apiClient.defaults.headers.common['Authorization'] = `Bearer ${context.apiToken}`; // Validate template first const validation = await this.validator.validate(template); if (!validation.valid && validation.errors.length > 0) { throw new Error(`Template validation failed: ${validation.errors[0].message}`); } const result = { success: true, steps: [], summary: { total: 0, successful: 0, failed: 0, skipped: 0 }, errors: [], createdEntities: [] }; // Execute each step for (const step of template.steps) { result.summary.total++; if (options.dryRun) { console.log(chalk['blue'](`[DRY RUN] Would execute step: ${step.id}`)); } const stepResult = await this.executeStep(step, context, options); result.steps.push(stepResult); if (stepResult.status === 'success') { result.summary.successful++; // Store result for reference if (stepResult.result?.id) { context.results[step.id] = stepResult.result; result.createdEntities.push({ stepId: step.id, entityType: stepResult.entityType || 'unknown', entityId: stepResult.result.id, name: stepResult.result.name }); } } else if (stepResult.status === 'failed') { result.summary.failed++; result.success = false; result.errors.push(`Step ${step.id}: ${stepResult.error}`); if (options.stopOnError) { console.error(chalk['red'](`Stopping execution due to error in step ${step.id}`)); break; } } else { result.summary.skipped++; } } return result; } /** * Execute a single step */ async executeStep(step, context, options = {}) { const startTime = Date.now(); try { if (step.type === 'template') { const entityType = this.extractEntityType(step.template?.system_template_id); const action = this.extractAction(step.template?.system_template_id); if (!entityType || !action) { throw new Error(`Invalid system_template_id: ${step.template?.system_template_id}`); } // Resolve references in inputs const resolvedInputs = await this.resolveInputs(step.template?.inputs || {}, context); if (options.dryRun) { return { stepId: step.id, status: 'success', action: `${action} ${entityType}`, entityType, result: { id: `dry-run-${step.id}`, ...resolvedInputs }, duration: Date.now() - startTime }; } // Execute API call const result = await this.executeApiCall(entityType, action, resolvedInputs, context); return { stepId: step.id, status: 'success', action: `${action} ${entityType}`, entityType, result, duration: Date.now() - startTime }; } else { // Handle other step types return { stepId: step.id, status: 'skipped', action: 'skip', error: `Unsupported step type: ${step.type}`, duration: Date.now() - startTime }; } } catch (error) { return { stepId: step.id, status: 'failed', action: 'error', error: error instanceof Error ? error.message : String(error), duration: Date.now() - startTime }; } } /** * Execute API call */ async executeApiCall(entityType, action, inputs, context) { const endpoint = this.getApiEndpoint(entityType, action, inputs, context); const method = this.getHttpMethod(action); const body = this.prepareRequestBody(entityType, action, inputs); console.log(chalk['gray'](`API ${method} ${endpoint}`)); try { const response = await this.apiClient.request({ method, url: endpoint, data: body }); return response.data; } catch (error) { if (error.response) { const status = error.response.status; const message = error.response.data?.message || error.response.statusText; const details = error.response.data?.errors ? '\n' + error.response.data.errors.map((e) => ` - ${e.field}: ${e.message}`).join('\n') : ''; throw new Error(`API Error (${status}): ${message}${details}`); } throw error; } } /** * Get API endpoint for entity and action */ getApiEndpoint(entityType, action, inputs, context) { const projectId = inputs.project_id || context.projectId; // Map entity types to API endpoints const endpoints = { project: { create: '/projects', update: `/projects/${inputs.id}`, get: `/projects/${inputs.id}` }, experiment: { create: `/projects/${projectId}/experiments`, update: `/experiments/${inputs.id}`, get: `/experiments/${inputs.id}` }, campaign: { create: `/projects/${projectId}/campaigns`, update: `/campaigns/${inputs.id}`, get: `/campaigns/${inputs.id}` }, audience: { create: `/projects/${projectId}/audiences`, update: `/audiences/${inputs.id}`, get: `/audiences/${inputs.id}` }, event: { create: `/projects/${projectId}/events`, update: `/events/${inputs.id}`, get: `/events/${inputs.id}` }, page: { create: `/projects/${projectId}/pages`, update: `/pages/${inputs.id}`, get: `/pages/${inputs.id}` }, flag: { create: `/projects/${projectId}/flags`, update: `/flags/${inputs.flag_key}`, get: `/flags/${inputs.flag_key}` }, ruleset: { create: `/flags/${inputs.flag_key}/environments/${inputs.environment_key || 'development'}/rulesets`, update: `/flags/${inputs.flag_key}/environments/${inputs.environment_key || 'development'}/rulesets/${inputs.id}`, get: `/flags/${inputs.flag_key}/environments/${inputs.environment_key || 'development'}/rulesets` }, variation: { create: this.getVariationEndpoint(inputs, projectId), update: `/variations/${inputs.id}`, get: `/variations/${inputs.id}` } }; const entityEndpoints = endpoints[entityType]; if (!entityEndpoints) { throw new Error(`Unknown entity type: ${entityType}`); } const endpoint = entityEndpoints[action]; if (!endpoint) { throw new Error(`Unknown action '${action}' for entity type '${entityType}'`); } return endpoint; } /** * Get HTTP method for action */ getHttpMethod(action) { const methods = { create: 'POST', update: 'PUT', patch: 'PATCH', delete: 'DELETE', get: 'GET' }; return methods[action] || 'POST'; } /** * Prepare request body */ prepareRequestBody(entityType, action, inputs) { if (action === 'get' || action === 'delete') { return undefined; } // Remove internal fields const body = { ...inputs }; delete body.project_id; // Usually in URL, not body delete body.id; // Usually in URL for updates // Special handling for certain entities if (entityType === 'audience' && typeof body.conditions === 'string') { try { body.conditions = JSON.parse(body.conditions); } catch (e) { // Keep as string if not valid JSON } } return body; } /** * Resolve inputs with references */ async resolveInputs(inputs, context) { const resolved = {}; for (const [key, value] of Object.entries(inputs)) { if (typeof value === 'string' && value.startsWith('${') && value.endsWith('}')) { // Resolve reference const ref = value.slice(2, -1); if (ref.includes('.')) { // Step reference like ${step_id.field} const [stepId, field] = ref.split('.'); const stepResult = context.results[stepId]; resolved[key] = stepResult?.[field] || value; } else { // Parameter reference like ${project_id} resolved[key] = context.parameters[ref] || value; } } else if (Array.isArray(value)) { // Recursively resolve arrays resolved[key] = await Promise.all(value.map(item => typeof item === 'object' ? this.resolveInputs(item, context) : item)); } else if (typeof value === 'object' && value !== null) { // Recursively resolve objects resolved[key] = await this.resolveInputs(value, context); } else { resolved[key] = value; } } return resolved; } /** * Extract entity type from system_template_id */ extractEntityType(templateId) { if (!templateId) return null; const parts = templateId.split('/'); return parts.length >= 3 ? parts[1] : null; } /** * Extract action from system_template_id */ extractAction(templateId) { if (!templateId) return null; const parts = templateId.split('/'); return parts.length >= 3 ? parts[2] : null; } /** * Get variation endpoint (special case) */ getVariationEndpoint(inputs, projectId) { if (inputs.experiment_id) { return `/experiments/${inputs.experiment_id}/variations`; } else if (inputs.flag_key) { return `/flags/${inputs.flag_key}/variations`; } throw new Error('Variation creation requires experiment_id or flag_key'); } /** * Format results for display */ formatResults(result) { const lines = []; lines.push(chalk['blue'](chalk['bold']('\nEXECUTION SUMMARY'))); lines.push('================='); lines.push(`Total Steps: ${result.summary.total}`); lines.push(`${chalk['green']('✓')} Successful: ${result.summary.successful}`); if (result.summary.failed > 0) { lines.push(`${chalk['red']('✗')} Failed: ${result.summary.failed}`); } if (result.summary.skipped > 0) { lines.push(`${chalk['yellow']('⊘')} Skipped: ${result.summary.skipped}`); } if (result.createdEntities.length > 0) { lines.push(chalk['green'](chalk['bold']('\n✅ CREATED ENTITIES:'))); for (const entity of result.createdEntities) { lines.push(` • ${entity.entityType}: ${entity.name || entity.entityId} (ID: ${entity.entityId})`); } } if (result.errors.length > 0) { lines.push(chalk['red'](chalk['bold']('\n❌ ERRORS:'))); for (const error of result.errors) { lines.push(` • ${error}`); } } lines.push(chalk['blue'](chalk['bold']('\nSTEP DETAILS:'))); for (const step of result.steps) { const statusIcon = step.status === 'success' ? chalk['green']('✓') : step.status === 'failed' ? chalk['red']('✗') : chalk['yellow']('⊘'); lines.push(` ${statusIcon} ${step.stepId}: ${step.action} (${step.duration}ms)`); if (step.error) { lines.push(chalk['red'](` Error: ${step.error}`)); } else if (step.result?.id) { lines.push(chalk['gray'](` Created: ${step.result.name || step.result.key || step.result.id}`)); } } return lines.join('\n'); } } //# sourceMappingURL=TemplateExecutor.js.map