UNPKG

create-sparc

Version:

NPX package to scaffold new projects with SPARC methodology structure

662 lines (579 loc) 21.7 kB
/** * MCP Configuration Wizard command * Provides CLI interface for configuring MCP servers */ const chalk = require('chalk'); const inquirer = require('inquirer'); const { mcpWizard } = require('../../core/mcp-wizard'); const { wizardCore } = require('../../core/mcp-wizard/wizard-core'); const { validateServerId, validateApiKey, validatePermissions } = require('../../core/mcp-wizard/validation'); const { logger } = require('../../utils'); /** * Register the wizard command with the CLI program * @param {import('commander').Command} program - Commander program instance */ function wizardCommand(program) { program .command('wizard') .description('Interactive MCP server configuration wizard') .option('-l, --list', 'List configured MCP servers') .option('-a, --add <server-id>', 'Add a specific MCP server') .option('-r, --remove <server-id>', 'Remove a configured MCP server') .option('-u, --update <server-id>', 'Update a configured MCP server') .option('--registry <url>', 'Custom registry URL') .option('--no-interactive', 'Run in non-interactive mode (requires all parameters)') .option('--config-path <path>', 'Custom path to MCP configuration file', '.roo/mcp.json') .option('--roomodes-path <path>', 'Custom path to roomodes file', '.roomodes') .option('--api-key <key>', 'API key for the server (use ${env:VAR_NAME} for environment variables)') .option('--region <region>', 'Region for the server', 'us-east-1') .option('--permissions <list>', 'Comma-separated list of permissions to grant', 'read,write') .option('--model <model>', 'Model to use (for AI services)') .option('--timeout <seconds>', 'Timeout in seconds', '10') .option('--debug', 'Enable debug output') .option('--validate', 'Validate the MCP configuration') .action(async (options) => { try { // Set debug mode if requested if (options.debug) { process.env.DEBUG = 'true'; logger.setLevel('debug'); } // Handle validate command if (options.validate) { await validateConfiguration(options); return; } // Handle list command if (options.list) { await listServers(options); return; } // Handle remove command if (options.remove) { await removeServer(options.remove, options); return; } // Handle add or update command if (options.add || options.update) { const serverId = options.add || options.update; const isUpdate = Boolean(options.update); await configureServer(serverId, isUpdate, options); return; } // Default: run the interactive wizard await runInteractiveWizard(options); } catch (error) { logger.error(`Wizard error: ${error.message}`); if (process.env.DEBUG) { console.error(error); } process.exit(1); } }); } /** * List all configured MCP servers * @param {Object} options - Command options */ async function listServers(options) { logger.info('Listing configured MCP servers...'); const result = await mcpWizard.listServers({ projectPath: process.cwd(), mcpConfigPath: options.configPath }); if (!result.success) { logger.error(`Failed to list servers: ${result.error}`); return; } const servers = result.servers; if (Object.keys(servers).length === 0) { logger.info('No MCP servers configured.'); return; } console.log('\n' + chalk.bold('Configured MCP Servers:')); for (const [serverId, serverConfig] of Object.entries(servers)) { console.log(`\n${chalk.cyan(serverId)}`); console.log(` Command: ${serverConfig.command} ${serverConfig.args.join(' ')}`); console.log(` Permissions: ${serverConfig.permissions.join(', ') || 'None'}`); } console.log(''); // Empty line for better readability } /** * Remove a configured MCP server * @param {string} serverId - Server ID to remove * @param {Object} options - Command options */ async function removeServer(serverId, options) { // Validate server ID const serverIdValidation = validateServerId(serverId); if (!serverIdValidation.valid) { throw new Error(`Invalid server ID: ${serverIdValidation.error}`); } // Check if server exists before attempting removal const listResult = await mcpWizard.listServers({ projectPath: process.cwd(), mcpConfigPath: options.configPath }); if (!listResult.success) { throw new Error(`Failed to check if server exists: ${listResult.error}`); } if (!listResult.servers[serverId]) { throw new Error(`Server not found: ${serverId}`); } if (!options.interactive) { await confirmAndRemoveServer(serverId, options); return; } const answers = await inquirer.prompt([ { type: 'confirm', name: 'confirm', message: `Are you sure you want to remove the MCP server "${serverId}"?`, default: false } ]); if (answers.confirm) { await confirmAndRemoveServer(serverId, options); } else { logger.info('Server removal cancelled.'); } } /** * Confirm and remove a server after validation * @param {string} serverId - Server ID to remove * @param {Object} options - Command options */ async function confirmAndRemoveServer(serverId, options) { logger.info(`Removing MCP server: ${chalk.cyan(serverId)}...`); try { // Initialize wizard core await wizardCore.initialize({ projectPath: process.cwd(), mcpConfigPath: options.configPath, roomodesPath: options.roomodesPath }); // Create a backup before removing const backupResult = await wizardCore.backupConfiguration(); if (!backupResult.success) { logger.warn(`Failed to create backup: ${backupResult.error}`); logger.warn('Proceeding without backup...'); } else { logger.debug(`Backup created successfully: ${JSON.stringify(backupResult.backupPaths)}`); } try { // Remove the server const result = await mcpWizard.removeServer(serverId, { projectPath: process.cwd(), mcpConfigPath: options.configPath, roomodesPath: options.roomodesPath }); if (result.success) { logger.success(`MCP server ${chalk.cyan(serverId)} removed successfully.`); } else { logger.error(`Failed to remove server: ${result.error}`); // Restore from backup if available if (backupResult.success) { logger.info('Restoring from backup...'); await wizardCore.restoreConfiguration(backupResult.backupPaths); } } } catch (error) { logger.error(`Error removing server: ${error.message}`); // Restore from backup if available if (backupResult.success) { logger.info('Restoring from backup after error...'); await wizardCore.restoreConfiguration(backupResult.backupPaths); } if (process.env.DEBUG) { console.error(error); } } } catch (error) { logger.error(`Error in backup/restore process: ${error.message}`); if (process.env.DEBUG) { console.error(error); } } } /** * Configure a specific MCP server * @param {string} serverId - Server ID to configure * @param {boolean} isUpdate - Whether this is an update operation * @param {Object} options - Command options */ async function configureServer(serverId, isUpdate, options) { const action = isUpdate ? 'Updating' : 'Adding'; logger.info(`${action} MCP server: ${chalk.cyan(serverId)}...`); // Validate server ID const serverIdValidation = validateServerId(serverId); if (!serverIdValidation.valid) { throw new Error(`Invalid server ID: ${serverIdValidation.error}`); } let serverParams; let serverMetadata; // If non-interactive mode, use provided parameters if (!options.interactive) { if (!options.apiKey) { throw new Error('API key is required in non-interactive mode. Use --api-key option.'); } // Parse permissions from comma-separated list const permissions = options.permissions ? options.permissions.split(',') : ['read', 'write']; // Build server parameters from options serverParams = { apiKey: options.apiKey.replace(/\${env:([^}]+)}/g, (match, envVar) => { return process.env[envVar] || match; }), region: options.region || 'us-east-1', permissions: permissions }; // Add optional parameters if provided if (options.model) { serverParams.model = options.model; } if (options.timeout) { serverParams.timeout = options.timeout; } // Create a server metadata object serverMetadata = { id: serverId, name: serverId, command: 'npx', args: ['-y', `@${serverId}/mcp-server@latest`], recommendedPermissions: permissions }; // If registry URL is provided, attempt to fetch server metadata if (options.registry) { try { logger.debug(`Fetching server metadata from registry: ${options.registry}`); // This would be implemented to fetch from the registry // For now, we'll use the default metadata } catch (error) { logger.warn(`Could not fetch server metadata: ${error.message}`); logger.warn('Using default server metadata'); } } } else { // Interactive mode - collect parameters through prompts serverParams = await collectServerParameters(serverId, isUpdate); // Create a simple server metadata object serverMetadata = { id: serverId, name: serverId, command: 'npx', args: ['-y', `@${serverId}/mcp-server@latest`], recommendedPermissions: ['read', 'write'] }; } // Call the appropriate wizard method let result; if (isUpdate) { result = await mcpWizard.updateServer(serverId, serverParams, { projectPath: process.cwd(), mcpConfigPath: options.configPath, roomodesPath: options.roomodesPath }); } else { result = await mcpWizard.addServer(serverMetadata, serverParams, { projectPath: process.cwd(), mcpConfigPath: options.configPath, roomodesPath: options.roomodesPath }); } if (result.success) { logger.success(`MCP server ${chalk.cyan(serverId)} ${isUpdate ? 'updated' : 'added'} successfully.`); return result; } else { const errorMsg = `Failed to ${isUpdate ? 'update' : 'add'} server: ${result.error}`; logger.error(errorMsg); throw new Error(errorMsg); } } /** * Collect server parameters through interactive prompts * @param {string} serverId - Server ID * @param {boolean} isUpdate - Whether this is an update operation * @returns {Promise<Object>} Collected parameters */ async function collectServerParameters(serverId, isUpdate) { // In a real implementation, we would fetch required parameters from registry // For now, we'll use a simplified approach with common parameters // Validate server ID const serverIdValidation = validateServerId(serverId); if (!serverIdValidation.valid) { throw new Error(`Invalid server ID: ${serverIdValidation.error}`); } // Define recommended permissions for this server type // In a real implementation, this would come from the registry const recommendedPermissions = ['read', 'write']; const questions = [ { type: 'input', name: 'apiKey', message: `Enter API key for ${serverId}:`, validate: input => { const validation = validateApiKey(input); return validation.valid ? true : validation.error; } }, { type: 'input', name: 'region', message: `Enter region for ${serverId} (optional):`, default: 'us-east-1' }, { type: 'checkbox', name: 'permissions', message: 'Select permissions to grant:', choices: [ { name: 'Read data', value: 'read' }, { name: 'Write data', value: 'write' }, { name: 'Delete data', value: 'delete' }, { name: 'Admin access', value: 'admin' } ], default: recommendedPermissions } ]; const answers = await inquirer.prompt(questions); // Validate permissions const permissionsValidation = validatePermissions(answers.permissions, recommendedPermissions); if (permissionsValidation.warning) { logger.warn(permissionsValidation.warning); // Ask for confirmation if permissions exceed recommended ones const confirmAnswer = await inquirer.prompt([ { type: 'confirm', name: 'confirm', message: 'Do you want to continue with these permissions?', default: false } ]); if (!confirmAnswer.confirm) { // Ask again for permissions const permissionsAnswer = await inquirer.prompt([ { type: 'checkbox', name: 'permissions', message: 'Select permissions to grant (recommended permissions highlighted):', choices: [ { name: 'Read data (recommended)', value: 'read', checked: recommendedPermissions.includes('read') }, { name: 'Write data (recommended)', value: 'write', checked: recommendedPermissions.includes('write') }, { name: 'Delete data', value: 'delete', checked: recommendedPermissions.includes('delete') }, { name: 'Admin access', value: 'admin', checked: recommendedPermissions.includes('admin') } ] } ]); answers.permissions = permissionsAnswer.permissions; } } // Transform answers into server parameters const envVarName = `${serverId.toUpperCase()}_API_KEY`; // Show environment variable setup instructions logger.info(chalk.yellow(`\nIMPORTANT: Set up the following environment variable:`)); logger.info(`export ${envVarName}="your-api-key-here"`); return { apiKey: `\${env:${envVarName}}`, // Use environment variable reference region: answers.region, permissions: answers.permissions }; } /** * Run the interactive wizard for MCP server configuration * @param {Object} options - Command options */ async function runInteractiveWizard(options) { logger.info(chalk.cyan('Starting MCP Configuration Wizard...')); console.log(chalk.dim('This wizard will help you configure MCP servers for your project.\n')); try { // Step 1: Choose operation const operationAnswers = await inquirer.prompt([ { type: 'list', name: 'operation', message: 'What would you like to do?', choices: [ { name: 'Add a new MCP server', value: 'add' }, { name: 'Update an existing MCP server', value: 'update' }, { name: 'Remove an MCP server', value: 'remove' }, { name: 'List configured MCP servers', value: 'list' } ] } ]); // Handle the selected operation switch (operationAnswers.operation) { case 'list': await listServers(options); break; case 'add': // Check if registry option is provided if (options.registry) { logger.info(`Using custom registry: ${options.registry}`); } // Step 2: Select server to add const addAnswers = await inquirer.prompt([ { type: 'input', name: 'serverId', message: 'Enter the MCP server ID to add:', validate: input => { const validation = validateServerId(input); return validation.valid ? true : validation.error; } } ]); try { await configureServer(addAnswers.serverId, false, options); // Show success message with next steps console.log(chalk.green('\nServer added successfully!')); console.log(chalk.dim('\nNext steps:')); console.log(chalk.dim('1. Set up the required environment variables')); console.log(chalk.dim('2. Test the server connection')); console.log(chalk.dim('3. Use the server in your project with the MCP roomode\n')); } catch (error) { logger.error(`Failed to add server: ${error.message}`); } break; case 'update': // Get list of configured servers const serverList = await mcpWizard.listServers({ projectPath: process.cwd(), mcpConfigPath: options.configPath }); if (!serverList.success || Object.keys(serverList.servers).length === 0) { logger.info(chalk.yellow('No MCP servers configured to update.')); // Ask if user wants to add a server instead const addServerAnswer = await inquirer.prompt([ { type: 'confirm', name: 'addServer', message: 'Would you like to add a new server instead?', default: true } ]); if (addServerAnswer.addServer) { // Recursively call the wizard with 'add' operation const addAnswers = await inquirer.prompt([ { type: 'input', name: 'serverId', message: 'Enter the MCP server ID to add:', validate: input => { const validation = validateServerId(input); return validation.valid ? true : validation.error; } } ]); await configureServer(addAnswers.serverId, false, options); } return; } // Step 2: Select server to update const updateAnswers = await inquirer.prompt([ { type: 'list', name: 'serverId', message: 'Select the MCP server to update:', choices: Object.keys(serverList.servers) } ]); try { await configureServer(updateAnswers.serverId, true, options); // Show success message console.log(chalk.green('\nServer updated successfully!')); } catch (error) { logger.error(`Failed to update server: ${error.message}`); } break; case 'remove': // Get list of configured servers const removeServerList = await mcpWizard.listServers({ projectPath: process.cwd(), mcpConfigPath: options.configPath }); if (!removeServerList.success || Object.keys(removeServerList.servers).length === 0) { logger.info(chalk.yellow('No MCP servers configured to remove.')); return; } // Step 2: Select server to remove const removeAnswers = await inquirer.prompt([ { type: 'list', name: 'serverId', message: 'Select the MCP server to remove:', choices: Object.keys(removeServerList.servers) } ]); try { await removeServer(removeAnswers.serverId, options); } catch (error) { logger.error(`Failed to remove server: ${error.message}`); } break; } // Ask if user wants to perform another operation const continueAnswer = await inquirer.prompt([ { type: 'confirm', name: 'continue', message: 'Would you like to perform another operation?', default: false } ]); if (continueAnswer.continue) { // Recursively call the wizard await runInteractiveWizard(options); } else { logger.success('MCP Configuration Wizard completed successfully.'); } } catch (error) { logger.error(`Wizard error: ${error.message}`); if (process.env.DEBUG) { console.error(error); } } } /** * Validate MCP configuration * @param {Object} options - Command options */ async function validateConfiguration(options) { logger.info('Validating MCP configuration...'); try { // Initialize wizard core with options await wizardCore.initialize({ projectPath: process.cwd(), mcpConfigPath: options.configPath, roomodesPath: options.roomodesPath }); // Validate configuration const result = await wizardCore.validateConfiguration(); if (result.success) { logger.success('MCP configuration is valid.'); // Display configuration summary const serverCount = Object.keys(result.config.mcpServers || {}).length; logger.info(`Configuration contains ${serverCount} server(s).`); if (serverCount > 0) { console.log('\n' + chalk.bold('Configured MCP Servers:')); for (const [serverId, serverConfig] of Object.entries(result.config.mcpServers)) { console.log(`\n${chalk.cyan(serverId)}`); console.log(` Command: ${serverConfig.command} ${serverConfig.args.join(' ')}`); console.log(` Permissions: ${serverConfig.alwaysAllow?.join(', ') || 'None'}`); } console.log(''); // Empty line for better readability } } else { logger.error('MCP configuration is invalid:'); result.errors.forEach(error => { logger.error(`- ${error.message}`); }); process.exit(1); } } catch (error) { logger.error(`Validation error: ${error.message}`); if (process.env.DEBUG) { console.error(error); } process.exit(1); } } module.exports = { wizardCommand };