UNPKG

create-sparc

Version:

NPX package to scaffold new projects with SPARC methodology structure

807 lines (694 loc) 26 kB
/** * MCP Configuration Command * Provides an integrated CLI interface for the MCP Configuration Wizard */ const chalk = require('chalk'); const inquirer = require('inquirer'); const { wizardCore, mcpSecurity } = require('../../core/mcp-wizard'); const { validateServerId, validateApiKey, validatePermissions } = require('../../core/mcp-wizard/validation'); const { logger } = require('../../utils'); /** * Register the configure-mcp command with the CLI program * @param {import('commander').Command} program - Commander program instance */ function configureMcpCommand(program) { program .command('configure-mcp') .description('Integrated MCP configuration wizard with server discovery') .option('-l, --list', 'List configured MCP servers') .option('-d, --discover', 'Discover available 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('-v, --validate', 'Validate current MCP configuration') .option('-b, --backup', 'Create a backup of current configuration') .option('-s, --security-audit', 'Perform security audit on MCP configuration') .option('--auto-fix', 'Automatically fix security issues (with --security-audit)') .option('-e, --validate-env', 'Validate environment variable references') .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') .action(async (options) => { try { // Initialize the wizard core await wizardCore.initialize({ projectPath: process.cwd(), mcpConfigPath: options.configPath, roomodesPath: options.roomodesPath, registryUrl: options.registry }); // Handle list command if (options.list) { await listConfiguredServers(); return; } // Handle discover command if (options.discover) { await discoverServers(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; } // Handle validate command if (options.validate) { await validateConfiguration(); return; } // Handle backup command if (options.backup) { await backupConfiguration(); return; } // Handle security audit command if (options.securityAudit) { await auditSecurity(options.autoFix); return; } // Handle environment variable validation command if (options.validateEnv) { await validateEnvironmentVariables(); return; } // Default: run the interactive wizard await runInteractiveWizard(options); } catch (error) { logger.error(`Configuration error: ${error.message}`); if (process.env.DEBUG) { console.error(error); } process.exit(1); } }); } /** * List all configured MCP servers */ async function listConfiguredServers() { logger.info('Listing configured MCP servers...'); const result = await wizardCore.listConfiguredServers(); 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 } /** * Discover available MCP servers * @param {Object} options - Command options */ async function discoverServers(options) { logger.info('Discovering available MCP servers...'); // Get search parameters let searchParams = {}; if (options.interactive) { const answers = await inquirer.prompt([ { type: 'input', name: 'search', message: 'Enter search term (optional):', }, { type: 'input', name: 'tags', message: 'Filter by tags (comma-separated, optional):', } ]); if (answers.search) { searchParams.search = answers.search; } if (answers.tags) { searchParams.tags = answers.tags; } } const result = await wizardCore.discoverServers(searchParams); if (!result.success) { logger.error(`Failed to discover servers: ${result.error}`); return; } const servers = result.servers; if (servers.length === 0) { logger.info('No MCP servers found matching your criteria.'); return; } console.log('\n' + chalk.bold('Available MCP Servers:')); for (const server of servers) { console.log(`\n${chalk.cyan(server.id)} - ${server.name}`); console.log(` Description: ${server.description || 'No description available'}`); console.log(` Tags: ${server.tags?.join(', ') || 'None'}`); } console.log(''); // Empty line for better readability // Ask if user wants to configure a server if (options.interactive) { const answer = await inquirer.prompt([ { type: 'confirm', name: 'configure', message: 'Would you like to configure one of these servers?', default: false } ]); if (answer.configure) { const serverAnswer = await inquirer.prompt([ { type: 'list', name: 'serverId', message: 'Select a server to configure:', choices: servers.map(server => ({ name: `${server.id} - ${server.name}`, value: server.id })) } ]); await configureServer(serverAnswer.serverId, false, options); } } } /** * Remove a configured MCP server * @param {string} serverId - Server ID to remove * @param {Object} options - Command options */ async function removeServer(serverId, options) { if (!options.interactive) { await confirmAndRemoveServer(serverId); 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); } else { logger.info('Server removal cancelled.'); } } /** * Confirm and remove a server after validation * @param {string} serverId - Server ID to remove */ async function confirmAndRemoveServer(serverId) { logger.info(`Removing MCP server: ${chalk.cyan(serverId)}...`); const result = await wizardCore.removeServer(serverId); if (result.success) { logger.success(`MCP server ${chalk.cyan(serverId)} removed successfully.`); } else { logger.error(`Failed to remove server: ${result.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)}...`); // If non-interactive mode, we need all required parameters if (!options.interactive) { logger.error('Non-interactive mode requires all parameters (not implemented yet)'); return; } // Get server details if adding a new server let serverDetails = null; if (!isUpdate) { try { const detailsResult = await wizardCore.getServerDetails(serverId); if (detailsResult.success) { serverDetails = detailsResult.server; } else { logger.warn(`Could not fetch server details: ${detailsResult.error}`); logger.info('Continuing with manual configuration...'); } } catch (error) { logger.warn(`Could not fetch server details: ${error.message}`); logger.info('Continuing with manual configuration...'); } } // Collect server parameters const serverParams = await collectServerParameters(serverId, isUpdate, serverDetails); // Call the appropriate wizard method let result; if (isUpdate) { result = await wizardCore.updateServerConfig(serverId, serverParams); } else { result = await wizardCore.configureServerWorkflow(serverId, serverParams); } if (result.success) { logger.success(`MCP server ${chalk.cyan(serverId)} ${isUpdate ? 'updated' : 'added'} successfully.`); // Show environment variable setup instructions if needed if (serverParams.apiKey && serverParams.apiKey.startsWith('${env:')) { const envVarName = serverParams.apiKey.match(/\${env:([^}]+)}/)[1]; logger.info(chalk.yellow(`\nIMPORTANT: Set up the following environment variable:`)); logger.info(`export ${envVarName}="your-api-key-here"`); } } else { logger.error(`Failed to ${isUpdate ? 'update' : 'add'} server: ${result.error}`); } } /** * Collect server parameters through interactive prompts * @param {string} serverId - Server ID * @param {boolean} isUpdate - Whether this is an update operation * @param {Object} serverDetails - Server details from registry (optional) * @returns {Promise<Object>} Collected parameters */ async function collectServerParameters(serverId, isUpdate, serverDetails = null) { // Validate server ID const serverIdValidation = validateServerId(serverId); if (!serverIdValidation.valid) { throw new Error(`Invalid server ID: ${serverIdValidation.error}`); } // Define recommended permissions based on server details or defaults const recommendedPermissions = serverDetails?.recommendedPermissions || ['read', 'write']; // Define required parameters based on server details or defaults const requiredParams = serverDetails?.requiredParams || ['apiKey']; // Build questions dynamically const questions = []; // Always ask for API key questions.push({ type: 'input', name: 'apiKey', message: `Enter API key for ${serverId}:`, validate: input => { const validation = validateApiKey(input); return validation.valid ? true : validation.error; } }); // Add region question if needed if (serverDetails?.supportsRegions || !serverDetails) { questions.push({ type: 'input', name: 'region', message: `Enter region for ${serverId} (optional):`, default: serverDetails?.defaultRegion || 'us-east-1' }); } // Add custom parameters from server details if (serverDetails?.optionalParams) { for (const param of serverDetails.optionalParams) { questions.push({ type: param.type || 'input', name: param.name, message: `${param.description || param.name} (optional):`, default: param.default, choices: param.choices }); } } // Add permissions question questions.push({ 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`; const params = { ...answers, apiKey: `\${env:${envVarName}}` // Use environment variable reference }; return params; } /** * Validate current MCP configuration */ async function validateConfiguration() { logger.info('Validating MCP configuration...'); const result = await wizardCore.validateConfiguration(); if (result.success) { logger.success('MCP configuration is valid.'); // Show configuration summary const serverCount = Object.keys(result.config.mcpServers || {}).length; console.log(`\nConfiguration contains ${serverCount} server(s).`); if (serverCount > 0) { console.log('\nConfigured servers:'); for (const serverId of Object.keys(result.config.mcpServers)) { console.log(`- ${chalk.cyan(serverId)}`); } } } else { logger.error('MCP configuration is invalid:'); for (const error of result.errors) { console.log(`- ${error.property}: ${error.message}`); } } } /** * Create a backup of current configuration */ async function backupConfiguration() { logger.info('Creating backup of MCP configuration...'); const result = await wizardCore.backupConfiguration(); if (result.success) { logger.success('Backup created successfully.'); if (result.backupPaths.mcpConfig) { console.log(`MCP config backup: ${result.backupPaths.mcpConfig}`); } if (result.backupPaths.roomodes) { console.log(`Roomodes backup: ${result.backupPaths.roomodes}`); } } else { logger.error(`Failed to create backup: ${result.error}`); } } /** * Perform security audit on MCP configuration * @param {boolean} autoFix - Whether to automatically fix security issues */ async function auditSecurity(autoFix = false) { logger.info('Performing security audit on MCP configuration...'); const result = await wizardCore.auditSecurity({ autoFix }); if (result.success) { if (result.secure) { logger.success('MCP configuration passed security audit.'); } else { logger.warn(`Security issues detected: ${result.issues.length} issues found`); // Group issues by severity const criticalIssues = result.issues.filter(issue => issue.severity === 'critical'); const warningIssues = result.issues.filter(issue => issue.severity === 'warning'); const infoIssues = result.issues.filter(issue => issue.severity === 'info'); // Display critical issues if (criticalIssues.length > 0) { console.log(`\n${chalk.red.bold('Critical Issues:')} ${criticalIssues.length}`); criticalIssues.forEach(issue => { console.log(`- ${chalk.red(issue.message)}`); if (issue.recommendation) { console.log(` ${chalk.dim(issue.recommendation)}`); } }); } // Display warning issues if (warningIssues.length > 0) { console.log(`\n${chalk.yellow.bold('Warnings:')} ${warningIssues.length}`); warningIssues.forEach(issue => { console.log(`- ${chalk.yellow(issue.message)}`); if (issue.recommendation) { console.log(` ${chalk.dim(issue.recommendation)}`); } }); } // Display info issues if (infoIssues.length > 0) { console.log(`\n${chalk.blue.bold('Information:')} ${infoIssues.length}`); infoIssues.forEach(issue => { console.log(`- ${chalk.blue(issue.message)}`); if (issue.recommendation) { console.log(` ${chalk.dim(issue.recommendation)}`); } }); } // Display recommendations if (result.recommendations && result.recommendations.length > 0) { console.log(`\n${chalk.cyan.bold('Recommendations:')}`); result.recommendations.forEach(recommendation => { console.log(`\n${chalk.cyan(recommendation.title)}`); recommendation.steps.forEach(step => { console.log(`- ${step}`); }); }); } // Display auto-fix results if applied if (autoFix && result.fixes) { console.log(`\n${chalk.green.bold('Applied Fixes:')} ${result.fixes.appliedFixes.length}`); result.fixes.appliedFixes.forEach(fix => { console.log(`- ${fix.message}`); }); } else if (!autoFix) { console.log(`\n${chalk.yellow('To automatically fix these issues, run with --auto-fix option')}`); } } } else { logger.error(`Failed to perform security audit: ${result.error}`); } } /** * Validate environment variable references in MCP configuration */ async function validateEnvironmentVariables() { logger.info('Validating environment variable references...'); const result = await wizardCore.validateEnvVarReferences(); if (result.success) { if (result.valid) { logger.success('All environment variable references are valid.'); // Display environment variables in use if (result.references.length > 0) { console.log(`\n${chalk.cyan.bold('Environment Variables in Use:')}`); result.references.forEach(ref => { const status = ref.isSet ? chalk.green('✓ Set') : chalk.red('✗ Not Set'); console.log(`- ${ref.name}: ${status} (used by ${ref.serverId})`); }); } } else { logger.warn('Missing environment variables detected.'); // Display missing variables console.log(`\n${chalk.yellow.bold('Missing Environment Variables:')}`); result.missingVariables.forEach(varName => { console.log(`- ${chalk.yellow(varName)}`); }); // Display setup instructions console.log(`\n${chalk.cyan.bold('Setup Instructions:')}`); console.log('Add these variables to your environment:'); result.missingVariables.forEach(varName => { console.log(`export ${varName}="your-value-here"`); }); } } else { logger.error(`Failed to validate environment variables: ${result.error}`); } } /** * Run the interactive wizard for MCP server configuration * @param {Object} options - Command options */ async function runInteractiveWizard(options) { logger.info(chalk.cyan('Starting Integrated 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: 'Discover available MCP servers', value: 'discover' }, { 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' }, { name: 'Validate configuration', value: 'validate' }, { name: 'Perform security audit', value: 'security-audit' }, { name: 'Validate environment variables', value: 'validate-env' }, { name: 'Create configuration backup', value: 'backup' } ] } ]); // Handle the selected operation switch (operationAnswers.operation) { case 'discover': await discoverServers(options); break; case 'list': await listConfiguredServers(); break; case 'add': // Step 2: Enter server ID or discover const addMethodAnswers = await inquirer.prompt([ { type: 'list', name: 'method', message: 'How would you like to add a server?', choices: [ { name: 'Enter server ID manually', value: 'manual' }, { name: 'Discover available servers', value: 'discover' } ] } ]); if (addMethodAnswers.method === 'discover') { await discoverServers(options); } else { 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); } break; case 'update': // Get list of configured servers const serverList = await wizardCore.listConfiguredServers(); 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) { 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); } break; } // 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) } ]); await configureServer(updateAnswers.serverId, true, options); break; case 'remove': // Get list of configured servers const removeServerList = await wizardCore.listConfiguredServers(); if (!removeServerList.success || Object.keys(removeServerList.servers).length === 0) { logger.info(chalk.yellow('No MCP servers configured to remove.')); break; } // 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) } ]); await removeServer(removeAnswers.serverId, options); break; case 'validate': await validateConfiguration(); break; case 'security-audit': // Ask if user wants to auto-fix issues const securityAnswers = await inquirer.prompt([ { type: 'confirm', name: 'autoFix', message: 'Would you like to automatically fix security issues?', default: false } ]); await auditSecurity(securityAnswers.autoFix); break; case 'validate-env': await validateEnvironmentVariables(); break; case 'backup': await backupConfiguration(); 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); } } } module.exports = { configureMcpCommand };