UNPKG

christmas-mcp-copilot-runner

Version:

A Model Copilot Provider (MCP) for safely executing whitelisted system commands across platforms with automatic VS Code setup

564 lines (475 loc) โ€ข 19.4 kB
#!/usr/bin/env node const fs = require('fs'); const path = require('path'); const os = require('os'); const readline = require('readline'); console.log('๐ŸŽ„ Christmas MCP Copilot Runner - VS Code Auto Setup'); console.log('==================================================='); // Create readline interface for user input const rl = readline.createInterface({ input: process.stdin, output: process.stdout }); // Promisify readline question function question(query) { return new Promise(resolve => rl.question(query, resolve)); } // Enhanced OS detection with user confirmation async function detectAndConfirmOS() { const detectedOS = os.platform(); let osName; switch (detectedOS) { case 'win32': osName = 'Windows'; break; case 'darwin': osName = 'macOS'; break; case 'linux': osName = 'Linux'; break; default: osName = 'Unknown'; } console.log(`๐Ÿ” Detected OS: ${osName}`); if (osName === 'Unknown') { console.log('โš ๏ธ Could not detect your operating system automatically.'); const osChoice = await question('Please select your OS:\n1. Windows\n2. macOS\n3. Linux\nEnter choice (1-3): '); switch (osChoice.trim()) { case '1': return 'win32'; case '2': return 'darwin'; case '3': return 'linux'; default: console.log('โŒ Invalid choice. Defaulting to Linux.'); return 'linux'; } } const confirm = await question(`โœ… Is this correct? (y/n): `); if (confirm.toLowerCase().startsWith('y')) { return detectedOS; } const osChoice = await question('\nPlease select your OS:\n1. Windows\n2. macOS\n3. Linux\nEnter choice (1-3): '); switch (osChoice.trim()) { case '1': return 'win32'; case '2': return 'darwin'; case '3': return 'linux'; default: console.log('โŒ Invalid choice. Using detected OS.'); return detectedOS; } } // Get project working directory from user async function getProjectWorkingDirectory() { console.log('\n๐Ÿ“‚ Project Working Directory Setup'); console.log('This is where MCP commands will be executed from.'); const useCurrentDir = await question(`\nUse current directory? (${process.cwd()})\n(y/n): `); if (useCurrentDir.toLowerCase().startsWith('y')) { return process.cwd(); } const projectPath = await question('\nEnter the full path to your project directory: '); const trimmedPath = projectPath.trim().replace(/['"]/g, ''); // Remove quotes if (!fs.existsSync(trimmedPath)) { console.log('โš ๏ธ Directory does not exist. Using current directory as fallback.'); return process.cwd(); } if (!fs.statSync(trimmedPath).isDirectory()) { console.log('โš ๏ธ Path is not a directory. Using current directory as fallback.'); return process.cwd(); } return path.resolve(trimmedPath); } // Get user preference for command type async function getCommandPreference() { console.log('\nโš™๏ธ Command Configuration'); console.log('The recommended configuration uses "node" for optimal performance.'); console.log(''); console.log('๐ŸŽฏ Default: node (Recommended)'); console.log(' โœ… Faster startup'); console.log(' โœ… More reliable'); console.log(' โœ… Better performance'); console.log(' โœ… Direct path execution'); console.log(''); const useDefault = await question('Use recommended "node" configuration? (Y/n): '); const trimmedChoice = useDefault.trim().toLowerCase(); // If user says no, offer npx alternative if (trimmedChoice === 'n' || trimmedChoice === 'no') { console.log(''); console.log('๐Ÿ“ฆ Alternative: npx'); console.log(' โš ๏ธ Slower startup (package resolution)'); console.log(' โš ๏ธ Potential version conflicts'); console.log(' โœ… Always uses latest version'); console.log(' โœ… No path configuration needed'); console.log(''); const confirmNpx = await question('Confirm using npx instead of node? (y/N): '); if (confirmNpx.trim().toLowerCase().startsWith('y')) { return { type: 'npx', description: 'NPX (package resolution)' }; } } // Default to node for all other cases return { type: 'node', description: 'Node (direct execution)' }; } // Detect VS Code settings directory based on platform function getVSCodeSettingsPath(targetOS) { const homeDir = os.homedir(); switch (targetOS) { case 'win32': return path.join(homeDir, 'AppData', 'Roaming', 'Code', 'User'); case 'darwin': return path.join(homeDir, 'Library', 'Application Support', 'Code', 'User'); case 'linux': return path.join(homeDir, '.config', 'Code', 'User'); default: return path.join(homeDir, '.config', 'Code', 'User'); } } // Get npm global package installation paths async function detectNpmPackagePaths(targetOS) { const paths = []; try { // Method 1: Try to get npm global root const { execSync } = require('child_process'); const npmRoot = execSync('npm root -g', { encoding: 'utf8', timeout: 5000 }).trim(); const globalPath = path.join(npmRoot, 'christmas-mcp-copilot-runner', 'src', 'index.js'); if (fs.existsSync(globalPath)) { paths.push({ description: 'NPM Global Installation', path: globalPath, type: 'global' }); } } catch (error) { console.log('โš ๏ธ Could not detect npm global root'); } // Method 2: Common Windows global npm paths if (targetOS === 'win32') { const homeDir = os.homedir(); const commonWinPaths = [ path.join(homeDir, 'AppData', 'Roaming', 'npm', 'node_modules', 'christmas-mcp-copilot-runner', 'src', 'index.js'), path.join('C:', 'Users', os.userInfo().username, 'AppData', 'Roaming', 'npm', 'node_modules', 'christmas-mcp-copilot-runner', 'src', 'index.js'), path.join('C:', 'Program Files', 'nodejs', 'node_modules', 'christmas-mcp-copilot-runner', 'src', 'index.js'), path.join('C:', 'Program Files (x86)', 'nodejs', 'node_modules', 'christmas-mcp-copilot-runner', 'src', 'index.js') ]; for (const winPath of commonWinPaths) { if (fs.existsSync(winPath)) { paths.push({ description: `Windows Global (${winPath})`, path: winPath.replace(/\//g, '\\'), type: 'global' }); } } } // Method 3: Common macOS/Linux global npm paths if (targetOS === 'darwin' || targetOS === 'linux') { const homeDir = os.homedir(); const commonUnixPaths = [ '/usr/local/lib/node_modules/christmas-mcp-copilot-runner/src/index.js', path.join(homeDir, '.npm-global', 'lib', 'node_modules', 'christmas-mcp-copilot-runner', 'src', 'index.js'), '/opt/homebrew/lib/node_modules/christmas-mcp-copilot-runner/src/index.js' ]; for (const unixPath of commonUnixPaths) { if (fs.existsSync(unixPath)) { paths.push({ description: `${targetOS === 'darwin' ? 'macOS' : 'Linux'} Global (${unixPath})`, path: unixPath, type: 'global' }); } } } // Method 4: Try require.resolve as fallback try { const packagePath = require.resolve('christmas-mcp-copilot-runner/package.json'); const localPath = path.join(path.dirname(packagePath), 'src', 'index.js'); if (fs.existsSync(localPath)) { paths.push({ description: 'Local/Development Installation', path: localPath, type: 'local' }); } } catch (error) { // Ignore } // Method 5: Current directory fallback (development mode) const currentDir = __dirname; const packageDir = path.dirname(currentDir); const devPath = path.join(packageDir, 'src', 'index.js'); if (fs.existsSync(devPath)) { paths.push({ description: 'Development Mode (Current Directory)', path: devPath, type: 'dev' }); } return paths; } // Let user select the correct npm package path async function selectNpmPackagePath(targetOS) { console.log('\n๐Ÿ” Detecting npm package installations...'); const availablePaths = await detectNpmPackagePaths(targetOS); if (availablePaths.length === 0) { console.log('โŒ Could not find christmas-mcp-copilot-runner installation'); const manualPath = await question('Please enter the full path to christmas-mcp-copilot-runner/src/index.js: '); const trimmedPath = manualPath.trim().replace(/['"]/g, ''); if (fs.existsSync(trimmedPath)) { return targetOS === 'win32' ? trimmedPath.replace(/\//g, '\\') : trimmedPath; } else { throw new Error('Invalid path provided'); } } console.log('\n๐Ÿ“ฆ Found the following installations:'); availablePaths.forEach((item, index) => { console.log(`${index + 1}. ${item.description}`); console.log(` Path: ${item.path}`); }); if (availablePaths.length === 1) { const useAuto = await question('\nUse this installation automatically? (y/n): '); if (useAuto.toLowerCase().startsWith('y')) { return availablePaths[0].path; } } const selection = await question(`\nSelect installation (1-${availablePaths.length}) or 0 for manual path: `); const selectionNum = parseInt(selection.trim()); if (selectionNum === 0) { const manualPath = await question('Enter full path to christmas-mcp-copilot-runner/src/index.js: '); const trimmedPath = manualPath.trim().replace(/['"]/g, ''); if (fs.existsSync(trimmedPath)) { return targetOS === 'win32' ? trimmedPath.replace(/\//g, '\\') : trimmedPath; } else { throw new Error('Invalid manual path provided'); } } if (selectionNum >= 1 && selectionNum <= availablePaths.length) { return availablePaths[selectionNum - 1].path; } throw new Error('Invalid selection'); } // Create VS Code MCP configuration function createMCPConfig(commandType, mcpServerPath, projectCwd) { const config = { "servers": [ { "name": "christmas-mcp-copilot-runner", "command": commandType.type, "env": {}, "disabled": false, "alwaysAllow": true } ] }; if (commandType.type === 'node') { config.servers[0].args = [mcpServerPath, "--mcp"]; config.servers[0].cwd = projectCwd; } else if (commandType.type === 'npx') { config.servers[0].args = ["christmas-mcp-copilot-runner", "--mcp"]; config.servers[0].cwd = projectCwd; } return config; } // Create VS Code settings configuration function createVSCodeSettings(commandType, mcpServerPath, projectCwd) { const mcpServerConfig = { "command": commandType.type, "env": {}, "disabled": false, "alwaysAllow": true }; if (commandType.type === 'node') { mcpServerConfig.args = [mcpServerPath, "--mcp"]; mcpServerConfig.cwd = projectCwd; } else if (commandType.type === 'npx') { mcpServerConfig.args = ["christmas-mcp-copilot-runner", "--mcp"]; mcpServerConfig.cwd = projectCwd; } return { "github.copilot.enable": { "*": true }, "workbench.trust.enabled": false, "languageModels.experimental": { "tools": { "enabled": true, "autoApprove": ["christmas-mcp-copilot-runner"] } }, "mcp.servers": { "christmas-mcp-copilot-runner": mcpServerConfig } }; } // Setup VS Code configuration async function setupVSCode(commandType, mcpServerPath, projectCwd) { const targetOS = os.platform(); const vscodeDir = getVSCodeSettingsPath(targetOS); const settingsPath = path.join(vscodeDir, 'settings.json'); console.log(`๐Ÿ“ VS Code settings directory: ${vscodeDir}`); console.log(`๐Ÿ“ Project working directory: ${projectCwd}`); console.log(`โš™๏ธ Command type: ${commandType.description}`); if (commandType.type === 'node') { console.log(`๐ŸŽฏ MCP Server path: ${mcpServerPath}`); } // Create VS Code directory if it doesn't exist if (!fs.existsSync(vscodeDir)) { console.log('๐Ÿ“ Creating VS Code settings directory...'); fs.mkdirSync(vscodeDir, { recursive: true }); } // Read existing settings or create new ones let existingSettings = {}; if (fs.existsSync(settingsPath)) { try { const settingsContent = fs.readFileSync(settingsPath, 'utf8'); existingSettings = JSON.parse(settingsContent); console.log('๐Ÿ“– Found existing VS Code settings'); } catch (error) { console.log('โš ๏ธ Warning: Could not parse existing settings, creating new ones'); } } // Merge with new MCP settings const newSettings = createVSCodeSettings(commandType, mcpServerPath, projectCwd); const mergedSettings = { ...existingSettings, ...newSettings }; // Write updated settings fs.writeFileSync(settingsPath, JSON.stringify(mergedSettings, null, 2)); console.log('โœ… VS Code settings updated with MCP configuration'); return true; } // Setup workspace-specific configuration async function setupWorkspaceConfig(commandType, mcpServerPath, projectCwd) { const vscodeWorkspaceDir = path.join(projectCwd, '.vscode'); const mcpConfigPath = path.join(vscodeWorkspaceDir, 'mcp.json'); const workspaceSettingsPath = path.join(vscodeWorkspaceDir, 'settings.json'); // Create .vscode directory if it doesn't exist if (!fs.existsSync(vscodeWorkspaceDir)) { fs.mkdirSync(vscodeWorkspaceDir, { recursive: true }); console.log('๐Ÿ“ Created .vscode directory in project workspace'); } // Create MCP configuration const mcpConfig = createMCPConfig(commandType, mcpServerPath, projectCwd); fs.writeFileSync(mcpConfigPath, JSON.stringify(mcpConfig, null, 2)); console.log('โœ… Created workspace MCP configuration'); // Create workspace settings const workspaceSettings = createVSCodeSettings(commandType, mcpServerPath, projectCwd); fs.writeFileSync(workspaceSettingsPath, JSON.stringify(workspaceSettings, null, 2)); console.log('โœ… Created workspace VS Code settings'); return true; } // Test MCP server functionality function testMCPServer(mcpServerPath) { console.log('๐Ÿงช Testing MCP server functionality...'); try { // Check if the MCP server file exists if (!fs.existsSync(mcpServerPath)) { throw new Error('MCP server file not found'); } console.log('โœ… MCP server file found'); console.log(`๐Ÿ“ Server path: ${mcpServerPath}`); return true; } catch (error) { console.error('โŒ MCP server test failed:', error.message); return false; } } // Main setup function with interactive configuration async function main() { try { console.log('๐Ÿš€ Starting interactive VS Code MCP setup...\n'); // Get OS confirmation const targetOS = await detectAndConfirmOS(); console.log(`โœ… Target OS: ${targetOS}`); // Get command preference const commandType = await getCommandPreference(); console.log(`โœ… Command type: ${commandType.description}`); // Get npm package path selection (only if using node command) let mcpServerPath = null; if (commandType.type === 'node') { mcpServerPath = await selectNpmPackagePath(targetOS); console.log(`โœ… MCP Server path: ${mcpServerPath}`); // Test MCP server if (!testMCPServer(mcpServerPath)) { console.error('โŒ Setup failed: MCP server test failed'); rl.close(); process.exit(1); } } else { console.log('โœ… Using npx - no path configuration needed'); } // Get project working directory const projectCwd = await getProjectWorkingDirectory(); console.log(`โœ… Project directory: ${projectCwd}`); // Format project path for target OS const formattedCwd = targetOS === 'win32' ? projectCwd.replace(/\//g, '\\') : projectCwd.replace(/\\/g, '/'); // Setup global VS Code configuration console.log('\n๐Ÿ“ Setting up global VS Code configuration...'); await setupVSCode(commandType, mcpServerPath, formattedCwd); // Setup workspace configuration console.log('\n๐Ÿ“ Setting up workspace configuration...'); await setupWorkspaceConfig(commandType, mcpServerPath, formattedCwd); console.log('\n๐ŸŽ‰ Setup completed successfully!'); console.log('====================================='); console.log('โœ… Christmas MCP Copilot Runner is now configured for VS Code'); console.log('โœ… Auto-approval is enabled for seamless command execution'); console.log('โœ… MCP server is ready to use'); console.log(`โœ… Command type: ${commandType.description}`); if (commandType.type === 'node') { console.log(`โœ… Server path: ${mcpServerPath}`); } console.log(`โœ… Commands will execute in: ${formattedCwd}`); console.log(''); console.log('๐Ÿ“‹ Next Steps:'); console.log('1. Restart VS Code completely (Cmd+Q/Ctrl+Q then reopen)'); console.log('2. Open your project workspace in VS Code'); console.log('3. Ask GitHub Copilot: "Use christmas-mcp-copilot-runner to run [your command]"'); console.log('4. Commands will execute automatically without approval prompts'); console.log(''); if (commandType.type === 'node') { console.log('๐Ÿšจ CRITICAL - VS Code UI Warning:'); console.log('โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”'); console.log('โš ๏ธ VS Code "Add Server..." UI has a BUG - it defaults to npx!'); console.log('โŒ NEVER use VS Code UI "Add Server..." button'); console.log('โŒ It will create: "command": "npx" (WRONG!)'); console.log('โœ… This setup uses: "command": "node" (CORRECT!)'); console.log(''); console.log('๐Ÿ”ง If you see npx in your configuration:'); console.log('1. Delete the server from VS Code MCP settings'); console.log('2. Re-run this setup script'); console.log('3. Or manually edit to: "command": "node"'); console.log('โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”'); console.log(''); } else { console.log('โ„น๏ธ NPX Configuration:'); console.log('You chose npx mode. VS Code will use: "command": "npx", "args": ["christmas-mcp-copilot-runner", "--mcp"]'); console.log('This will always use the latest version but may have slower startup times.'); console.log(''); } console.log('๐Ÿ”ง Manual start: christmas-mcp-copilot-runner --mcp'); console.log('๐Ÿ“š Documentation: https://github.com/Chr1stm4s/christmas-mcp-copilot-bypass'); rl.close(); } catch (error) { console.error('โŒ Setup failed:', error.message); console.log('\n๐Ÿ”ง Manual Setup Instructions:'); console.log('1. Create .vscode/mcp.json with MCP server configuration'); console.log('2. Update VS Code settings.json with MCP and auto-approval settings'); console.log('3. Install GitHub Copilot and MCP extensions'); rl.close(); process.exit(1); } } // Run setup if called directly if (require.main === module) { main(); } module.exports = { main, setupVSCode, setupWorkspaceConfig, testMCPServer };