@simonecoelhosfo/optimizely-mcp-server
Version:
Optimizely MCP Server for AI assistants with integrated CLI tools
1,096 lines (1,069 loc) ⢠65 kB
JavaScript
#!/usr/bin/env node
/**
* Optimizely MCP Server CLI - Comprehensive Analytics & Management Tool
* @description A powerful CLI for managing Optimizely data with structured
* analytics, advanced querying, and comprehensive data operations.
*
* Features:
* - š Structured Analytics Engine (PRIMARY TOOL)
* - Interactive REPL mode with autocomplete
* - Granular sync control with entity/environment filtering
* - Advanced query interface with SQL support
* - Export/Import in multiple formats (JSON, CSV, YAML)
* - Real-time watch mode with auto-sync
* - Diff and comparison tools
* - Performance analytics and monitoring
* - Database backup/restore
* - Schema migration management
*
* @author Optimizely MCP Server Team
* @version 3.1.0
*/
import { Command } from 'commander';
import chalk from 'chalk';
import path from 'path';
import { fileURLToPath } from 'url';
import * as yaml from 'js-yaml';
import dotenv from 'dotenv';
// Core imports
import { ConfigManager } from '../config/ConfigManager.js';
import { CacheManager } from '../cache/CacheManager.js';
import { OptimizelyMCPTools } from '../tools/OptimizelyMCPTools.js';
import { SQLiteEngine } from '../storage/SQLiteEngine.js';
import { OptimizelyAPIHelper } from '../api/OptimizelyAPIHelper.js';
import { createLogger } from '../logging/Logger.js';
import { ProjectFilter } from '../config/ProjectFilter.js';
/**
* Main CLI Class - Orchestrates all commands
*/
export class OptlyCLI {
program;
context;
replServer;
watchIntervals = new Map();
constructor() {
this.program = new Command();
this.setupProgram();
}
/**
* Setup main program with all commands
*/
setupProgram() {
this.program
.name('optly')
.description('Optimizely MCP Server CLI - Comprehensive cache and data management')
.version('3.0.0')
.option('-v, --verbose', 'verbose output with debug information')
.option('--json', 'output results in JSON format')
.option('--csv', 'output results in CSV format')
.option('--yaml', 'output results in YAML format')
.option('-o, --output <file>', 'write output to file')
.option('--no-color', 'disable colored output')
.option('--profile <name>', 'use configuration profile')
.hook('preAction', async (thisCommand, actionCommand) => {
// Skip initialization for sync command as it delegates to enhanced script
const commandName = actionCommand.name();
// console.log('DEBUG: Command name:', commandName, 'Args:', actionCommand.args);
if (commandName !== 'sync' && commandName !== 'setup-docs' && commandName !== 'flags' && commandName !== 'experiments') {
// Initialize context before any command except sync, setup-docs, flags, and experiments
await this.initializeContext(thisCommand.opts());
}
});
// Add all command groups
this.addSyncCommands();
this.addQueryCommands();
this.addEntityCommands();
this.addExportImportCommands();
this.addDatabaseCommands();
this.addAnalyticsCommands();
this.addWatchCommands();
this.addDiffCommands();
this.addConfigCommands();
this.addInteractiveCommand();
this.addShortcuts();
}
/**
* Initialize CLI context
*/
async initializeContext(options) {
if (this.context)
return; // Already initialized
// Disable colors if requested
if (options.noColor) {
chalk.level = 0;
}
// Load .env file from current directory if it exists
dotenv.config({ path: path.resolve(process.cwd(), '.env') });
// Also try to load from project root (where CLI is installed)
dotenv.config({ path: path.resolve(path.dirname(fileURLToPath(import.meta.url)), '../../.env') });
// Load configuration
const config = new ConfigManager();
try {
await config.loadConfig();
}
catch (error) {
// Handle missing configuration gracefully
if (error.message?.includes('apiToken')) {
console.error(chalk.red('\nā Missing API Token\n'));
console.error('Please set the OPTIMIZELY_API_TOKEN environment variable:');
console.error(chalk.cyan(' export OPTIMIZELY_API_TOKEN="your-api-token-here"'));
console.error('\nOr create a .env file in the current directory with:');
console.error(chalk.cyan(' OPTIMIZELY_API_TOKEN=your-api-token-here'));
process.exit(1);
}
if (error.message?.includes('projects')) {
console.error(chalk.red('\nā Missing Project Configuration\n'));
console.error('Please configure which projects to access. Set one of:');
console.error(chalk.cyan(' export OPTIMIZELY_PROJECT_IDS="12345,67890"'));
console.error(chalk.cyan(' export OPTIMIZELY_AUTO_DISCOVER_ALL=true'));
console.error('\nOr add to your .env file:');
console.error(chalk.cyan(' OPTIMIZELY_PROJECT_IDS=12345,67890'));
process.exit(1);
}
throw error;
}
const serverConfig = config.getConfig();
// Initialize logging
createLogger({
logLevel: options.verbose ? 'debug' : 'info',
logFile: serverConfig.logging.logFile,
consoleLogging: false, // Disable for CLI
prettyPrint: false
});
// Initialize API helper
const api = new OptimizelyAPIHelper(serverConfig.optimizely.apiToken, {
baseUrl: serverConfig.optimizely.baseUrl,
flagsUrl: serverConfig.optimizely.flagsUrl
});
// Initialize storage
const storage = new SQLiteEngine({
path: serverConfig.storage.databasePath || './data/optimizely-cache.db'
});
await storage.init();
// Initialize project filter
const projectFilter = new ProjectFilter(serverConfig.optimizely.projects);
// Initialize cache manager
const cache = new CacheManager(storage, api, projectFilter, serverConfig);
await cache.init();
// Initialize tools
const tools = new OptimizelyMCPTools(cache);
// Create context
this.context = {
config,
cache,
tools,
storage,
api,
options
};
}
/**
* Add sync-related commands
*/
addSyncCommands() {
const sync = this.program
.command('sync')
.description('Enhanced cache synchronization with advanced options')
.option('-p, --project <id>', 'sync specific project')
.option('-m, --multi-project <ids...>', 'sync multiple projects (space-separated IDs)')
.option('-f, --force', 'clear existing data before sync (auto-resets database if schema outdated)')
.option('-r, --reset', 'reset database if schema is outdated (alias for --force)')
.option('-i, --incremental', 'use incremental sync instead of full sync')
.option('-v, --verbose', 'enable verbose output')
.option('-c, --continue-on-error', 'continue syncing other projects if one fails (multi-project mode)')
.option('-s, --no-summary', 'disable summary report generation')
.option('-e, --export <path>', 'export performance reports to specified directory')
.option('--json', 'output summary in JSON format')
.option('--markdown', 'output summary in Markdown format')
.option('--compact', 'use compact progress display (default)')
.option('--classic', 'use classic verbose progress display')
.option('--tables <tables...>', 'sync only specific database tables (space-separated table names)')
.option('--entities <entities...>', 'sync only specific entity types (space-separated: flags, experiments, audiences, etc.)')
.option('--database-path <path>', 'specify custom database file path (defaults to ./data/optimizely-cache.db)')
.option('--list-tables', 'list all available database tables and exit')
.option('--list-entities', 'list all available entity types and exit')
.option('--views-only', 'only recreate database views without clearing or syncing data')
.addHelpText('after', `
Examples:
optly sync # Full sync with progress bars
optly sync --verbose # Verbose output with detailed logs
optly sync --project 12345 # Sync specific project
optly sync --force # Force refresh (auto-resets if needed)
optly sync --views-only # Only recreate views without syncing data
optly sync --database-path /tmp/my-cache.db # Use custom database path
optly sync --multi-project 123 456 789 # Sync multiple projects
optly sync --tables experiments flags # Sync only specific tables
optly sync --entities experiment flag # Sync only specific entity types
optly sync --project 12345 --export ./reports # Export performance reports
`)
.action(async (options) => {
await this.handleSync(options);
});
sync
.command('status')
.description('Show sync status and statistics')
.action(async () => {
await this.handleSyncStatus();
});
}
/**
* Add query-related commands
*/
addQueryCommands() {
const query = this.program
.command('query')
.description('Query cached data')
.argument('[entity]', 'entity type to query')
.option('--sql <query>', 'raw SQL query')
.option('--filter <conditions>', 'filter conditions (e.g., "enabled=true AND environment=production")')
.option('--project <id>', 'filter by project')
.option('--limit <n>', 'limit results', '100')
.option('--offset <n>', 'offset results', '0')
.option('--sort <field>', 'sort by field')
.option('--desc', 'sort descending')
.addHelpText('after', `
Examples:
optly query flags --project 12345 --filter "enabled=true"
optly query experiments --sort created_at --desc --limit 10
optly query --sql "SELECT * FROM flags WHERE key LIKE 'checkout%'"
optly query audiences --project 12345 --filter "name LIKE '%mobile%'"
optly query flags --filter "environment='production' AND enabled=true" --limit 50
Available Tables:
flags, experiments, audiences, events, attributes, pages, campaigns,
variations, environments, rulesets, rules, variable_definitions
Filter Examples:
enabled=true # Boolean values
name LIKE '%test%' # String patterns
created_at > '2024-01-01' # Date comparisons
status IN ('running','paused') # Multiple values
`)
.action(async (entity, options) => {
await this.handleQuery(entity, options);
});
// COMMENTED OUT: search_all tool is redundant with list_entities functionality
// this.program
// .command('search')
// .description('Search across all entities')
// .argument('<keyword>', 'search keyword')
// .option('--type <entity>', 'limit search to entity type')
// .option('--project <id>', 'limit search to project')
// .option('--limit <n>', 'limit results', '20')
// .action(async (keyword, options) => {
// await this.handleSearch(keyword, options);
// });
}
/**
* Add entity management commands
*/
addEntityCommands() {
const entity = this.program
.command('entity')
.alias('e')
.description('Manage entities')
.addHelpText('after', `
Supported Entity Types:
Feature Experimentation: flag, environment, ruleset, rule, variation, variable_definition
Web Experimentation: campaign, experiment, page, audience, attribute, event, extension, webhook
Cross-Platform: project, group, collaborator
Examples:
optly entity list flag --project 12345 # List all flags in project
optly entity get flag my_flag_key --project 12345 # Get specific flag details
optly entity templates flag --project 12345 # Show flag templates
optly entity create flag --template simple --project 12345
`);
// List entities
entity
.command('list <type>')
.description('List entities of a specific type')
.option('-p, --project <id>', 'filter by project')
.option('--archived', 'include archived entities')
.option('--limit <n>', 'limit results per page')
.option('--page <n>', 'page number (for pagination)')
.option('--page-size <n>', 'number of results per page')
.addHelpText('after', `
Examples:
optly entity list flag --project 12345 # List flags in project
optly entity list experiment --project 12345 --limit 10
optly entity list audience --archived # Include archived audiences
optly entity list flag --page 2 --page-size 20 # Paginated results
`)
.action(async (type, options) => {
await this.handleEntityList(type, options);
});
// Get entity details
entity
.command('get <type> <id>')
.description('Get entity details')
.option('-p, --project <id>', 'project context')
.action(async (type, id, options) => {
await this.handleEntityGet(type, id, options);
});
// Create entity
entity
.command('create <type>')
.description('Create new entity from template or file')
.option('-t, --template <name>', 'use template')
.option('-f, --file <path>', 'read from file')
.option('-p, --project <id>', 'project context')
.option('--dry-run', 'preview without creating')
.addHelpText('after', `
Examples:
optly entity create flag --template simple --project 12345
optly entity create experiment --template basic --project 12345
optly entity create flag --file my-flag.json --project 12345 --dry-run
optly entity create audience --template demographic --project 12345
`)
.action(async (type, options) => {
await this.handleEntityCreate(type, options);
});
// Update entity
entity
.command('update <type> <id>')
.description('Update entity')
.option('-f, --file <path>', 'read updates from file')
.option('-s, --set <field=value...>', 'set field values')
.option('-p, --project <id>', 'project context')
.option('--dry-run', 'preview without updating')
.action(async (type, id, options) => {
await this.handleEntityUpdate(type, id, options);
});
// Delete entity
entity
.command('delete <type> <id>')
.description('Delete/archive entity')
.option('-p, --project <id>', 'project context')
.option('--force', 'skip confirmation')
.action(async (type, id, options) => {
await this.handleEntityDelete(type, id, options);
});
// Show entity templates
entity
.command('templates <type>')
.description('Show available templates for entity type')
.option('-p, --project <id>', 'project context')
.option('--complexity <level>', 'filter by complexity (simple/moderate/complex)')
.action(async (type, options) => {
await this.handleEntityTemplates(type, options);
});
}
/**
* Add export/import commands
*/
addExportImportCommands() {
// Export command
this.program
.command('export')
.description('Export data in various formats')
.argument('<entity>', 'entity type or "all"')
.option('-p, --project <id>', 'filter by project')
.option('--format <type>', 'output format (json/csv/yaml)', 'json')
.option('--output <path>', 'output file path')
.option('--filter <conditions>', 'filter conditions')
.option('--transform <script>', 'transformation script path')
.addHelpText('after', `
Examples:
optly export flags --format json --output flags.json --project 12345
optly export experiments --format csv --project 12345 --filter "status=running"
optly export all --format yaml --output complete-backup.yaml --project 12345
optly export audiences --format json --filter "name LIKE '%mobile%'"
optly export flags --output flags.csv --format csv --filter "enabled=true"
Entity Types:
flag, experiment, audience, attribute, event, page, campaign, variation,
environment, ruleset, rule, variable_definition, webhook, extension, all
Output Formats:
json - Structured JSON format (default)
csv - Comma-separated values for spreadsheets
yaml - Human-readable YAML format
`)
.action(async (entity, options) => {
await this.handleExport(entity, options);
});
// Import command
this.program
.command('import')
.description('Import data from file')
.argument('<file>', 'input file path')
.option('--type <entity>', 'entity type (auto-detect if not specified)')
.option('-p, --project <id>', 'target project')
.option('--merge', 'merge with existing data')
.option('--dry-run', 'preview import without applying')
.option('--map <field=field...>', 'field mappings')
.addHelpText('after', `
Examples:
optly import flags.json --project 12345 --dry-run # Preview import
optly import experiments.csv --type experiment --project 12345
optly import backup.yaml --merge --project 12345 # Merge with existing
optly import flags.json --map "name=display_name" --project 12345
optly import audiences.json --project 12345 # Auto-detect type
File Formats:
.json - JSON format (single object or array)
.csv - CSV with headers matching field names
.yaml - YAML format (single document or array)
Import Modes:
--dry-run - Preview changes without applying (recommended first)
--merge - Merge with existing data (update if exists, create if not)
default - Replace/create entities (safer for new data)
Field Mapping:
--map "csv_column=entity_field" - Map CSV columns to entity fields
--map "old_name=new_name" - Rename fields during import
`)
.action(async (file, options) => {
await this.handleImport(file, options);
});
}
/**
* Add database management commands
*/
addDatabaseCommands() {
const db = this.program
.command('db')
.alias('database')
.description('Database management commands');
// Backup
db.command('backup')
.description('Create database backup')
.option('--output <path>', 'backup file path')
.option('--compress', 'compress backup')
.action(async (options) => {
await this.handleBackup(options);
});
// Restore
db.command('restore')
.description('Restore database from backup')
.argument('<file>', 'backup file path')
.option('--force', 'overwrite existing data')
.action(async (file, options) => {
await this.handleRestore(file, options);
});
// Optimize
db.command('optimize')
.description('Optimize database performance')
.option('--vacuum', 'reclaim unused space')
.option('--analyze', 'update statistics')
.option('--reindex', 'rebuild indexes')
.action(async (options) => {
await this.handleOptimize(options);
});
// Stats
db.command('stats')
.description('Show database statistics')
.option('--detailed', 'show detailed statistics')
.option('--tables', 'show table statistics')
.action(async (options) => {
await this.handleDatabaseStats(options);
});
// Status
db.command('status')
.description('Check database connection and health status')
.option('--detailed', 'show detailed health information')
.action(async (options) => {
await this.handleDatabaseStatus(options);
});
// Reset
db.command('reset')
.description('Reset database (WARNING: deletes all data)')
.option('--force', 'skip confirmation')
.option('--backup', 'create backup before reset')
.action(async (options) => {
await this.handleDatabaseReset(options);
});
}
/**
* Add analytics commands
*/
addAnalyticsCommands() {
const analytics = this.program
.command('analytics')
.alias('a')
.description('Analytics and insights');
// Structured analytics engine
analytics
.command('analyze')
.description('š Structured data analysis (PRIMARY ANALYTICS TOOL)')
.option('-q, --query <json>', 'structured query as JSON')
.option('--template <name>', 'use pre-built template (variable_analysis, complexity_analysis, etc.)')
.option('-p, --project <id>', 'project context')
.option('--format <format>', 'output format (table, summary, detailed, json, csv)', 'table')
.option('--limit <n>', 'limit results', '100')
.option('--interactive', 'interactive mode for query refinement')
.option('--no-cache', 'bypass query cache for fresh results')
.option('--cache-ttl <ms>', 'cache TTL in milliseconds', '300000')
.option('--optimize', 'show query optimization details')
.option('--benchmark', 'show performance benchmarks')
.option('--explain', 'explain query interpretation and execution plan')
.addHelpText('after', `
Examples:
optly analytics analyze --template list_flags # List all flags
optly analytics analyze --template list_experiments # List all experiments
optly analytics analyze --template variable_analysis # Analyze feature variables
optly analytics analyze --query '{"from":"flags_unified_view"}' # Custom JSON query
optly analytics analyze --query '{"from":"flags","select":["key","name","enabled"]}'
optly analytics analyze --project 12345 --format json # Project-specific analysis
optly analytics analyze --template list_flags --format csv --limit 50
Available Templates:
list_flags, list_experiments, variable_analysis, complexity_analysis,
performance_summary, entity_relationships, cross_platform_analysis
Query Format:
{"from":"table_name","select":["field1","field2"],"where":{"enabled":true}}
`)
.action(async (options) => {
await this.handleAnalyze(undefined, options);
});
// Get insights
analytics
.command('insights')
.description('Get AI-powered insights and recommendations')
.option('-p, --project <id>', 'project context')
.option('--type <entity>', 'entity type focus')
.option('--period <days>', 'analysis period in days', '30')
.action(async (options) => {
await this.handleInsights(options);
});
// Get results
analytics
.command('results')
.description('Get experiment/campaign results')
.argument('<id>', 'experiment or campaign ID')
.option('--type <type>', 'experiment or campaign', 'experiment')
.option('--metrics <list>', 'specific metrics to include')
.option('--start <date>', 'start date')
.option('--end <date>', 'end date')
.action(async (id, options) => {
await this.handleResults(id, options);
});
// Recent changes
analytics
.command('changes')
.description('Show recent changes')
.option('-p, --project <id>', 'project context')
.option('--since <date>', 'changes since date')
.option('--entity <type>', 'filter by entity type')
.option('--limit <n>', 'limit results', '50')
.action(async (options) => {
await this.handleRecentChanges(options);
});
// Analytics cache management
analytics
.command('cache')
.description('Manage analytics query cache')
.argument('<action>', 'action to perform (clear, stats, warmup)')
.option('-p, --project <id>', 'project context for warmup')
.option('--entity <type>', 'entity type for warmup')
.action(async (action, options) => {
await this.handleAnalyticsCache(action, options);
});
// Query templates
analytics
.command('templates')
.description('List and manage query templates')
.option('--category <name>', 'filter by category')
.option('--show <name>', 'show template details')
.option('--example <name>', 'show example usage')
.addHelpText('after', `
Examples:
optly analytics templates # List all available templates
optly analytics templates --category flags # Show flag-related templates
optly analytics templates --show list_flags # Show template details
optly analytics templates --example variable_analysis # Show usage example
`)
.action(async (options) => {
await this.handleAnalyticsTemplates(options);
});
}
/**
* Add watch mode commands
*/
addWatchCommands() {
const watch = this.program
.command('watch')
.description('Watch for changes and auto-sync');
// Start watching
watch
.command('start')
.description('Start watching for changes')
.option('-p, --project <id>', 'watch specific project')
.option('--entities <list>', 'entities to watch')
.option('--interval <seconds>', 'check interval', '30')
.option('--webhook <url>', 'webhook for notifications')
.action(async (options) => {
await this.handleWatchStart(options);
});
// Stop watching
watch
.command('stop')
.description('Stop watching')
.option('--all', 'stop all watchers')
.option('--id <id>', 'stop specific watcher')
.action(async (options) => {
await this.handleWatchStop(options);
});
// List watchers
watch
.command('list')
.description('List active watchers')
.action(async () => {
await this.handleWatchList();
});
}
/**
* Add diff/comparison commands
*/
addDiffCommands() {
this.program
.command('diff')
.description('Compare data between sources')
.option('--source <type>', 'source (cache/api/file)', 'cache')
.option('--target <type>', 'target (cache/api/file)', 'api')
.option('--entity <type>', 'entity type to compare')
.option('-p, --project <id>', 'project context')
.option('--ids <list>', 'specific IDs to compare')
.option('--output <format>', 'output format (text/json/html)', 'text')
.action(async (options) => {
await this.handleDiff(options);
});
this.program
.command('compare')
.description('Compare environments')
.argument('<env1>', 'first environment')
.argument('<env2>', 'second environment')
.option('-p, --project <id>', 'project context')
.option('--type <entity>', 'entity type to compare')
.action(async (env1, env2, options) => {
await this.handleCompareEnvironments(env1, env2, options);
});
}
/**
* Add configuration commands
*/
addConfigCommands() {
const config = this.program
.command('config')
.description('Configuration management');
// Show config
config
.command('show')
.description('Show current configuration')
.option('--section <name>', 'specific section')
.action(async (options) => {
await this.handleConfigShow(options);
});
// Set config
config
.command('set')
.description('Set configuration value')
.argument('<key>', 'configuration key (dot notation)')
.argument('<value>', 'value to set')
.option('--profile <name>', 'configuration profile')
.action(async (key, value, options) => {
await this.handleConfigSet(key, value, options);
});
// Manage defaults
config
.command('defaults')
.description('Manage entity defaults')
.argument('<action>', 'action (get/set/reset)')
.argument('[entity]', 'entity type')
.option('-p, --project <id>', 'project context')
.option('--data <json>', 'default data (for set)')
.action(async (action, entity, options) => {
await this.handleDefaults(action, entity, options);
});
// Health check
this.program
.command('health')
.description('Check system health')
.option('--detailed', 'detailed health information')
.action(async (options) => {
await this.handleHealthCheck(options);
});
// OpenAPI Reference commands (for entity schemas and fields)
const openapi = this.program
.command('openapi')
.description('Query OpenAPI specifications for Optimizely entities');
openapi
.command('schema <entityType>')
.description('Get OpenAPI schema for an entity type')
.addHelpText('after', `
Examples:
$ optly openapi schema flag # Get flag schema
$ optly openapi schema experiment # Get experiment schema
$ optly openapi schema flag -o json # Output as JSON
Entity types: project, experiment, flag, audience, attribute, event, page, campaign, variation, ruleset, rule, variable_definition, feature, environment, webhook, extension, group`)
.option('-o, --output <format>', 'output format', 'table')
.option('--operation <op>', 'specific operation (create/update/delete/list/get)')
.option('-p, --project <id>', 'project ID for platform-specific differences')
.action(async (entityType, options) => {
await this.handleApiRefSchema(entityType, options);
});
openapi
.command('fields <entityType>')
.description('Get field details for an entity type')
.addHelpText('after', `
Examples:
$ optly openapi fields flag # Get all flag fields
$ optly openapi fields flag -f key # Get details for 'key' field`)
.option('-f, --field <name>', 'specific field name')
.option('-o, --output <format>', 'output format', 'table')
.option('-p, --project <id>', 'project ID for platform-specific differences')
.action(async (entityType, options) => {
await this.handleApiRefFields(entityType, options);
});
openapi
.command('examples <entityType>')
.description('Get usage examples for an entity type')
.addHelpText('after', `
Examples:
$ optly openapi examples flag # Get flag creation examples
$ optly openapi examples experiment # Get experiment examples`)
.option('-o, --output <format>', 'output format', 'json')
.option('--operation <op>', 'examples for specific operation')
.option('-p, --project <id>', 'project ID for platform-specific differences')
.action(async (entityType, options) => {
await this.handleApiRefExamples(entityType, options);
});
openapi
.command('validation <entityType>')
.description('Get validation rules for an entity type')
.addHelpText('after', `
Examples:
$ optly openapi validation flag # Get flag validation rules
$ optly openapi validation experiment # Get experiment validation rules`)
.option('-o, --output <format>', 'output format', 'table')
.option('-p, --project <id>', 'project ID for platform-specific differences')
.action(async (entityType, options) => {
await this.handleApiRefValidation(entityType, options);
});
openapi
.command('dependencies <entityType>')
.description('Get entity dependencies for operations')
.addHelpText('after', `
Examples:
$ optly openapi dependencies flag # Get flag dependencies
$ optly openapi dependencies experiment --operation update # Get update dependencies`)
.option('--operation <type>', 'operation type (create/update/delete)', 'create')
.option('-o, --output <format>', 'output format', 'table')
.option('-p, --project <id>', 'project ID for platform-specific differences')
.action(async (entityType, options) => {
await this.handleApiRefDependencies(entityType, options);
});
// SDK/Platform Documentation commands
const sdkDocs = this.program
.command('sdk-docs')
.description('Query Optimizely SDK and platform documentation');
sdkDocs
.command('search <query>')
.description('Search SDK/API documentation with natural language')
.addHelpText('after', `
Examples:
$ optly sdk-docs search "track events" # Search for event tracking
$ optly sdk-docs search "get visitor id" # Find visitor ID methods
$ optly sdk-docs search "javascript activate" # JavaScript activation methods`)
.option('-o, --output <format>', 'output format', 'table')
.option('--methods-only', 'search only methods')
.option('--concepts-only', 'search only concepts')
.action(async (query, options) => {
await this.handleApiRefSearch(query, options);
});
sdkDocs
.command('method <methodName>')
.description('Get details for a specific SDK method')
.addHelpText('after', `
Examples:
$ optly sdk-docs method window.optimizely.get # Web API method
$ optly sdk-docs method decide --platform javascript # JavaScript SDK method
$ optly sdk-docs method track_event --platform python # Python SDK method`)
.option('--platform <name>', 'platform/SDK (javascript, python, web_experimentation, etc.)')
.option('-o, --output <format>', 'output format', 'json')
.option('--details', 'show detailed information')
.option('--examples', 'show usage examples')
.action(async (methodName, options) => {
await this.handleApiRefMethod(methodName, options);
});
sdkDocs
.command('sdk <sdkName>')
.description('Get SDK information and methods')
.addHelpText('after', `
Examples:
$ optly sdk-docs sdk javascript # Get JavaScript SDK overview
$ optly sdk-docs sdk javascript --methods # List all JS SDK methods
$ optly sdk-docs sdk python --setup # Get Python SDK setup guide
Available SDKs: javascript, python, java, swift, android, php, web_experimentation`)
.option('--info <type>', 'information type (methods, installation, setup)', 'overview')
.option('--methods', 'list all methods', false)
.option('--setup', 'show setup/installation guide', false)
.option('-o, --output <format>', 'output format', 'table')
.action(async (sdkName, options) => {
await this.handleApiRefSdk(sdkName, options);
});
sdkDocs
.command('browse')
.description('Browse all available SDK/platform documentation')
.addHelpText('after', `
Examples:
$ optly sdk-docs browse --methods # Browse all available methods
$ optly sdk-docs browse --sdks # Browse all SDKs
$ optly sdk-docs browse --concepts # Browse all concepts`)
.option('--methods', 'browse all methods', false)
.option('--sdks', 'browse all SDKs', false)
.option('--concepts', 'browse all concepts', false)
.option('-o, --output <format>', 'output format', 'table')
.action(async (options) => {
await this.handleApiRefBrowse(options);
});
// Setup documentation
this.program
.command('setup-docs')
.description('Copy documentation and templates to current directory')
.action(async () => {
// setup-docs doesn't need API/database initialization - run directly
await this.handleSetupDocs();
});
}
/**
* Add interactive REPL command
*/
addInteractiveCommand() {
this.program
.command('interactive')
.alias('i')
.description('Start interactive REPL mode')
.option('--history <file>', 'history file path', '.optly_history')
.action(async (options) => {
await this.startInteractiveMode(options);
});
}
/**
* Add convenient shortcuts for common commands
*/
addShortcuts() {
// Shortcuts for common operations
this.program
.command('flags')
.description('List and analyze flags using the powerful analytics engine')
.action(() => {
console.log('Flag Analysis Commands:\n');
console.log(chalk.cyan(' optly analytics analyze --template list_flags'));
console.log(chalk.cyan(' optly analytics analyze --query \'{"from":"flags_unified_view"}\''));
console.log(chalk.cyan(' optly entity list flag # for raw entity listing'));
console.log('\nThe analytics engine provides powerful querying capabilities!');
});
this.program
.command('experiments')
.description('List and analyze experiments using the powerful analytics engine')
.action(() => {
console.log('Experiment Analysis Commands:\n');
console.log(chalk.cyan(' optly analytics analyze --template list_experiments'));
console.log(chalk.cyan(' optly analytics analyze --query \'{"from":"experiments_unified_view"}\''));
console.log(chalk.cyan(' optly entity list experiment # for raw entity listing'));
console.log('\nThe analytics engine provides powerful querying capabilities!');
});
this.program
.command('list')
.description('List entities using the analytics engine or entity commands')
.action(() => {
console.log('Entity Listing Commands:\n');
console.log('Use one of these powerful commands:');
console.log(chalk.cyan(' optly analytics analyze --template list_flags # Structured analytics'));
console.log(chalk.cyan(' optly entity list <type> # Raw entity listing'));
console.log('\nThe analytics engine provides powerful querying capabilities!');
});
}
// Handler implementations
/**
* Handle setup-docs command - copy documentation to current directory
*/
async handleSetupDocs() {
const fs = await import('fs/promises');
const path = await import('path');
const chalk = (await import('chalk')).default;
const { fileURLToPath } = await import('url');
try {
console.log(chalk.blue('š Setting up Optimizely MCP documentation...\n'));
// Find package root by looking for package.json
// First try to find in node_modules (installed package)
let packagePath;
const nodeModulesPath = path.join(process.cwd(), 'node_modules', '@simonecoelhosfo', 'optimizely-mcp-server');
try {
await fs.access(path.join(nodeModulesPath, 'package.json'));
packagePath = nodeModulesPath;
console.log(chalk.gray(`Found package in node_modules: ${packagePath}`));
}
catch {
// Fall back to finding package root from current execution context
const currentFileUrl = import.meta.url;
const currentFilePath = fileURLToPath(currentFileUrl);
packagePath = path.resolve(path.dirname(currentFilePath), '../..');
while (packagePath !== path.dirname(packagePath)) {
try {
await fs.access(path.join(packagePath, 'package.json'));
// Verify this is actually our package
const packageJsonContent = await fs.readFile(path.join(packagePath, 'package.json'), 'utf-8');
const packageJson = JSON.parse(packageJsonContent);
if (packageJson.name === '@simonecoelhosfo/optimizely-mcp-server') {
console.log(chalk.gray(`Found package root: ${packagePath}`));
break;
}
}
catch {
packagePath = path.dirname(packagePath);
}
}
}
const targetDir = path.join(process.cwd(), 'optimizely-mcp-docs');
// Create target directory
await fs.mkdir(targetDir, { recursive: true });
// Helper function to copy directory
const copyDirectory = async (src, dest) => {
await fs.mkdir(dest, { recursive: true });
const entries = await fs.readdir(src, { withFileTypes: true });
for (const entry of entries) {
const srcPath = path.join(src, entry.name);
const destPath = path.join(dest, entry.name);
if (entry.isDirectory()) {
await copyDirectory(srcPath, destPath);
}
else {
await fs.copyFile(srcPath, destPath);
}
}
};
// Files to copy from NPM package
const filesToCopy = [
{ src: 'README.md', dest: 'README.md' }, // Main package README
{ src: 'docs', dest: 'docs' }, // Documentation
{ src: 'templates', dest: 'templates' }, // Configuration templates
{ src: 'agent-rules', dest: 'agent-rules' } // Agent rules
];
let copiedCount = 0;
for (const { src, dest } of filesToCopy) {
const sourcePath = path.join(packagePath, src);
const destPath = path.join(targetDir, dest);
try {
const stats = await fs.stat(sourcePath);
if (stats.isDirectory()) {
await copyDirectory(sourcePath, destPath);
console.log(chalk.green(`ā Copied ${dest}/`));
}
else {
await fs.copyFile(sourcePath, destPath);
console.log(chalk.green(`ā Copied ${dest}`));
}
copiedCount++;
}
catch (error) {
console.warn(chalk.yellow(`ā Could not copy ${src}: ${error.message}`));
}
}
// Create quick start guide
const quickStartContent = `# Optimizely MCP Server - Quick Start
Welcome to Optimizely MCP Server! This folder contains all the documentation you need to get started.
## š For Claude Desktop
Add to your Claude Desktop config (\`%APPDATA%\\\\Claude\\\\claude_desktop_config.json\`):
\`\`\`json
{
"mcpServers": {
"optimizely": {
"command": "npx",
"args": ["@simonecoelhosfo/optimizely-mcp-server"],
"env": {
"OPTIMIZELY_API_TOKEN": "your-api-token-here"
}
}
}
}
\`\`\`
## š For Cursor
1. Open Settings (Ctrl+,)
2. Search for "MCP"
3. Add the Optimizely server configuration
## š Get Your API Token
1. Log in to [app.optimizely.com](https://app.optimizely.com)
2. Go to Account Settings ā Personal Settings ā API Access
3. Generate a new Personal Access Token
## š Next Steps
- Check the \`docs/\` folder for detailed guides
- Run \`optly --help\` to see all commands
- Run \`optly health\` to test your connection
`;
await fs.writeFile(path.join(targetDir, 'QUICK-START.md'), quickStartContent);
console.log(chalk.green('ā Created QUICK-START.md'));
// Create NPX fix script in the project root (not inside optimizely-mcp-docs)
const npxFixScriptContent = `#!/usr/bin/env node
// NPX Fix for Optimizely MCP Server
// Auto-generated by setup-docs command
// Solves Windows NPX spawn issues while maintaining cross-platform compatibility
const { execSync } = require('child_process');
const fs = require('fs');
const args = process.argv.slice(2);
if (args.length === 0) {
console.log('š Optimizely MCP Server - NPX Fix');
console.log('');
console.log('Usage:');
console.log(' node optly-npx-fix.cjs optly setup-docs');
console.log(' node optly-npx-fix.cjs optly health');
console.log(' node optly-npx-fix.cjs optly sync status');
console.log(' node optly-npx-fix.cjs optly analytics templates');
console.log('');
console.log('š” This fixes Windows NPX spawn issues.');
console.log(' Works on Windows, Linux, and macOS.');
console.log('');
console.log('š¦ Package.json integration:');
console.log(' "setup-docs": "node optly-npx-fix.cjs optly setup-docs"');
process.exit(0);
}
if (args[0] === 'optly') {
const optlyPath = './node_modules/@simonecoelhosfo/optimizely-mcp-server/bin/optly';
if (fs.existsSync(optlyPath)) {
try {
const command = \`node "\${optlyPath}" \${args.slice(1).join(' ')}\`;
execSync(command, {
stdio: 'inherit',
timeout: 30000
});
} catch (error) {
console.error('ā Error executing optly:', error.message);
process.exit(1);
}
} else {
console.error('ā Optimizely MCP Server not found.');
console.error('š¦ Install with: npm install @simonecoelhosfo/optimizely-mcp-server');
process.exit(1);
}
} else {
console.log('š” This tool currently supports optly commands only.');
console.log(' For other packages, use standard npm/npx commands.');
process.exit(1);
}
`;
const npxFixPath = path.join(process.cwd(), 'optly-npx-fix.cjs');
await fs.writeFile(npxFixPath, npxFixScriptContent);
console.log(chalk.green('ā Created optly-npx-fix.cjs (in project root)'));
// Update package.json with optly scripts
try {
const packageJsonPath = path.join(process.cwd(), 'package.json');
let packageJson = {};
try {
const packageJsonContent = await fs.readFile(packageJsonPath, 'utf-8');
packageJson = JSON.parse(packageJsonContent);
}
catch {
// Create minimal package.json if it doesn't exist
packageJson = {
name: "optimizely-project",
version: "1.0.0",
description: "Project using Optimizely MCP Server"
};
}
// Ensure scripts section exists
if (!packageJson.scripts) {
packageJson.scripts = {};
}
// Add optly scripts
const optlyScripts = {
"optly": "node optly-npx-fix.cjs optly",
"optly-help": "node optly-npx-fix.cjs optly --help",
"optly-version": "node optly-npx-fix.cjs optly --version",
"optly-health": "node optly-npx-fix.cjs optly health",
"optly-setup": "node optly-npx-fix.cjs optly setup-docs",
"optly-sync": "node optly-npx-fix.cjs optly sync status",
"optly-s