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
JavaScript
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 };