Bash MCP (Master Control Program)

by yannbam
Verified
import { spawn } from 'child_process'; import { MCPConfig, ExecutionOptions, ExecutionResult } from '../types'; import { logger } from '../utils/logger'; import { validateCommand, sanitizeOutput, isDirectoryAllowed } from '../utils/validator'; export class CommandExecutor { private config: MCPConfig; constructor(config: MCPConfig) { this.config = config; } /** * Execute a command in non-interactive mode */ public async executeCommand( command: string, options: ExecutionOptions = {} ): Promise<ExecutionResult> { // Use default options if not provided const timeout = options.timeout || this.config.security.commandTimeout; const cwd = options.cwd || process.cwd(); // Handle environment variables - ensure all values are strings let env = options.env; if (!env) { env = {}; Object.entries(process.env).forEach(([key, value]) => { if (value !== undefined) { env![key] = value; } }); } // Validate the command const validation = validateCommand(command, this.config); if (!validation.isValid) { logger.warn(`Command validation failed: ${validation.reason}`); return { success: false, output: '', error: validation.reason, command, }; } // Validate the directory if (!isDirectoryAllowed(cwd, this.config)) { logger.warn(`Directory not allowed: ${cwd}`); return { success: false, output: '', error: `Directory not allowed: ${cwd}`, command, }; } // Log the command execution logger.info(`Executing command: ${command} in directory: ${cwd}`); try { // Execute the command const result = await this.spawnCommand(command, cwd, env, timeout); // Sanitize the output const sanitizedOutput = sanitizeOutput(result.output, this.config); return { ...result, output: sanitizedOutput, command, }; } catch (error) { logger.error( `Command execution error: ${error instanceof Error ? error.message : String(error)}` ); return { success: false, output: '', error: error instanceof Error ? error.message : String(error), command, }; } } /** * Spawn a child process to execute the command */ private spawnCommand( command: string, cwd: string, env: NodeJS.ProcessEnv, timeoutSeconds: number ): Promise<ExecutionResult> { return new Promise((resolve) => { // Split the command into the executable and arguments const [cmd, ...args] = command.split(' '); // Create the child process const childProcess = spawn(cmd, args, { cwd, env, shell: true, }); let stdout = ''; let stderr = ''; let killed = false; // Collect stdout childProcess.stdout.on('data', (data) => { stdout += data.toString(); }); // Collect stderr childProcess.stderr.on('data', (data) => { stderr += data.toString(); }); // Handle process completion childProcess.on('close', (code) => { if (killed) { resolve({ success: false, output: stdout + stderr, exitCode: code || undefined, error: 'Command execution timed out', command, }); } else { resolve({ success: code === 0, output: stdout + stderr, exitCode: code || undefined, error: code !== 0 ? `Command exited with code ${code}` : undefined, command, }); } }); // Handle process errors childProcess.on('error', (error) => { resolve({ success: false, output: stdout + stderr, error: error.message, command, }); }); // Set a timeout to kill the process if it runs too long const timeoutId = setTimeout(() => { logger.warn(`Command timed out after ${timeoutSeconds} seconds: ${command}`); killed = true; childProcess.kill(); }, timeoutSeconds * 1000); // Clear the timeout if the process completes before the timeout childProcess.on('close', () => { clearTimeout(timeoutId); }); }); } }