UNPKG

bigparse

Version:

MCP server that gives Claude instant, intelligent access to your codebase using Language Server Protocol

257 lines (225 loc) 6.6 kB
import { spawn, SpawnOptions } from 'child_process'; import { EventEmitter } from 'events'; import { LANGUAGE_SERVERS } from '../resources/language-servers.js'; export interface InstallProgress { status: 'starting' | 'running' | 'success' | 'error'; message: string; output?: string; error?: string; } export class LanguageServerInstaller extends EventEmitter { private allowedCommands = new Set([ 'npm', 'pip', 'pip3', 'go', 'rustup', 'gem', 'dotnet', 'dart', 'flutter' ]); async installLanguageServer(language: string): Promise<InstallProgress> { const lsInfo = LANGUAGE_SERVERS[language]; if (!lsInfo) { return { status: 'error', message: `Unknown language: ${language}`, error: 'Language not supported' }; } // Validate the install command for security const validation = this.validateCommand(lsInfo.installCommand); if (!validation.safe) { return { status: 'error', message: 'Install command validation failed', error: validation.reason }; } this.emit('progress', { status: 'starting', message: `Starting installation of ${lsInfo.name}...` }); try { const result = await this.runInstallCommand(lsInfo.installCommand, lsInfo.name); if (result.success) { // Verify installation const verified = await this.verifyInstallation(language); if (verified) { this.emit('progress', { status: 'success', message: `✅ ${lsInfo.name} installed successfully!`, output: result.output }); return { status: 'success', message: `${lsInfo.name} installed successfully`, output: result.output }; } else { return { status: 'error', message: `Installation appeared to succeed but ${lsInfo.name} is not available`, error: 'Verification failed', output: result.output }; } } else { return { status: 'error', message: `Failed to install ${lsInfo.name}`, error: result.error, output: result.output }; } } catch (error) { return { status: 'error', message: `Installation error: ${error}`, error: String(error) }; } } private validateCommand(command: string): { safe: boolean; reason?: string } { // Basic security validation const parts = command.split(' '); const mainCommand = parts[0]; if (!this.allowedCommands.has(mainCommand)) { return { safe: false, reason: `Command '${mainCommand}' is not in the allowed list` }; } // Check for dangerous patterns const dangerousPatterns = [ /[;&|]/, // Command chaining /[<>]/, // Redirects /\$\(/, // Command substitution /`/, // Backticks /\.\./, // Path traversal ]; for (const pattern of dangerousPatterns) { if (pattern.test(command)) { return { safe: false, reason: 'Command contains potentially dangerous characters' }; } } return { safe: true }; } private async runInstallCommand(command: string, serverName: string): Promise<{ success: boolean; output: string; error?: string; }> { return new Promise((resolve) => { const parts = command.split(' '); const cmd = parts[0]; const args = parts.slice(1); let output = ''; let errorOutput = ''; const options: SpawnOptions = { shell: false, // Safer - no shell interpretation env: { ...process.env, FORCE_COLOR: '0', // Disable color output for cleaner logs } }; const proc = spawn(cmd, args, options); proc.stdout?.on('data', (data) => { const text = data.toString(); output += text; this.emit('progress', { status: 'running', message: `Installing ${serverName}...`, output: text.trim() }); }); proc.stderr?.on('data', (data) => { const text = data.toString(); errorOutput += text; // Some installers write normal output to stderr this.emit('progress', { status: 'running', message: `Installing ${serverName}...`, output: text.trim() }); }); proc.on('error', (error) => { resolve({ success: false, output: output + errorOutput, error: error.message }); }); proc.on('close', (code) => { if (code === 0) { resolve({ success: true, output: output + errorOutput }); } else { resolve({ success: false, output: output + errorOutput, error: `Process exited with code ${code}` }); } }); }); } private async verifyInstallation(language: string): Promise<boolean> { const lsInfo = LANGUAGE_SERVERS[language]; if (!lsInfo) return false; try { return new Promise((resolve) => { const parts = lsInfo.checkCommand.split(' '); const cmd = parts[0]; const args = parts.slice(1); const proc = spawn(cmd, args, { shell: false }); proc.on('error', () => { resolve(false); }); proc.on('close', (code) => { resolve(code === 0); }); }); } catch { return false; } } async getInstallScript(language: string): Promise<string | null> { const lsInfo = LANGUAGE_SERVERS[language]; if (!lsInfo) return null; return `#!/bin/bash # BigParse Language Server Installer # Installing: ${lsInfo.name} echo "🚀 BigParse Language Server Installer" echo "Installing ${lsInfo.name}..." echo "" # Check if already installed if command -v ${lsInfo.command} &> /dev/null; then echo "✅ ${lsInfo.name} is already installed" ${lsInfo.checkCommand} exit 0 fi # Install echo "Running: ${lsInfo.installCommand}" ${lsInfo.installCommand} # Verify installation echo "" echo "Verifying installation..." if command -v ${lsInfo.command} &> /dev/null; then echo "✅ ${lsInfo.name} installed successfully!" ${lsInfo.checkCommand} else echo "❌ Installation may have failed" echo "" echo "Manual installation instructions:" echo "${lsInfo.installInstructions}" exit 1 fi`; } }