UNPKG

@simonecoelhosfo/optimizely-mcp-server

Version:

Optimizely MCP Server for AI assistants with integrated CLI tools

1,096 lines (1,069 loc) • 65 kB
#!/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