UNPKG

@gabrielmaialva33/mcp-filesystem

Version:
218 lines 7.5 kB
import { exec, spawn, execSync } from 'node:child_process'; import { promisify } from 'node:util'; import { z } from 'zod'; import { logger } from '../logger/index.js'; import { FileSystemError } from '../errors/index.js'; const execAsync = promisify(exec); const SAFE_COMMAND_REGEX = /^[a-zA-Z0-9_\-./\s:;,|><&{}()[\]'"$%+*!?~=]+$/; const FORBIDDEN_COMMANDS = [ 'rm -rf /', 'rm -rf /*', 'mkfs', 'dd if=/dev/zero', 'chmod -R 777', ':(){:|:&};:', '> /dev/sda', 'cat /dev/port', 'cat /dev/mem', ]; export const BashCommandArgsSchema = z.object({ command: z.string().describe('Bash command to execute'), workingDir: z.string().optional().describe('Working directory for command execution'), timeout: z .number() .int() .positive() .max(60000) .default(10000) .describe('Maximum execution time in milliseconds (max 60s)'), env: z.record(z.string(), z.string()).optional().describe('Additional environment variables'), interactive: z .boolean() .default(false) .describe('Whether command is interactive (uses spawn instead of exec)'), }); function validateCommand(command) { if (FORBIDDEN_COMMANDS.some((forbidden) => command.includes(forbidden) || command.replace(/\s+/g, '') === forbidden.replace(/\s+/g, ''))) { throw new FileSystemError(`Command contains forbidden operations`, 'FORBIDDEN_COMMAND', undefined, { command }); } if (!SAFE_COMMAND_REGEX.test(command)) { throw new FileSystemError(`Command contains potentially unsafe characters`, 'UNSAFE_COMMAND', undefined, { command }); } return true; } export async function executeBashCommand(args) { const startTime = Date.now(); try { await logger.debug(`Executing bash command: ${args.command}`, { args }); validateCommand(args.command); const env = { ...process.env, ...args.env, }; const options = { cwd: args.workingDir || process.cwd(), timeout: args.timeout, env, shell: '/bin/bash', windowsHide: true, }; if (args.interactive) { return await executeInteractiveCommand(args.command, options, startTime); } else { return await executeNonInteractiveCommand(args.command, options, startTime); } } catch (error) { const endTime = Date.now(); const executionTime = endTime - startTime; if (error instanceof FileSystemError) { throw error; } if (typeof error === 'object' && error !== null && 'stdout' in error && 'stderr' in error) { const execError = error; return { stdout: execError.stdout || '', stderr: execError.stderr || 'Command failed to execute', exitCode: execError.code || 1, executionTime, command: args.command, }; } return { stdout: '', stderr: error instanceof Error ? error.message : String(error), exitCode: 1, executionTime, command: args.command, }; } } async function executeNonInteractiveCommand(command, options, startTime) { try { const { stdout, stderr } = await execAsync(command, options); const endTime = Date.now(); const executionTime = endTime - startTime; await logger.debug(`Command executed successfully: ${command}`, { executionTime, stdout: Buffer.isBuffer(stdout) ? stdout.toString('utf8').substring(0, 100) + (stdout.toString('utf8').length > 100 ? '...' : '') : String(stdout).substring(0, 100) + (String(stdout).length > 100 ? '...' : ''), }); return { stdout: Buffer.isBuffer(stdout) ? stdout.toString('utf8') : String(stdout), stderr: Buffer.isBuffer(stderr) ? stderr.toString('utf8') : String(stderr), exitCode: 0, executionTime, command, }; } catch (error) { const endTime = Date.now(); const executionTime = endTime - startTime; return { stdout: error.stdout || '', stderr: error.stderr || 'Command failed', exitCode: error.code || 1, executionTime, command, }; } } async function executeInteractiveCommand(command, options, startTime) { return new Promise((resolve) => { const parts = command.split(' '); const cmd = parts[0]; const args = parts.slice(1); let stdout = ''; let stderr = ''; let exitCode = 0; const childProcess = spawn(cmd, args, options); childProcess.stdout?.on('data', (data) => { stdout += data.toString(); }); childProcess.stderr?.on('data', (data) => { stderr += data.toString(); }); childProcess.on('close', (code) => { const endTime = Date.now(); const executionTime = endTime - startTime; exitCode = code !== null ? code : 1; logger.debug(`Interactive command completed: ${command}`, { executionTime, exitCode, }); resolve({ stdout, stderr, exitCode, executionTime, command, }); }); childProcess.on('error', (error) => { const endTime = Date.now(); const executionTime = endTime - startTime; stderr = error.message; exitCode = 1; logger.error(`Error executing interactive command: ${command}`, { error, executionTime, }); resolve({ stdout, stderr, exitCode, executionTime, command, }); }); if (options.timeout) { setTimeout(() => { if (!childProcess.killed) { childProcess.kill(); const endTime = Date.now(); const executionTime = endTime - startTime; stderr += '\nCommand timed out'; exitCode = 124; logger.warn(`Command timed out: ${command}`, { timeout: options.timeout, executionTime, }); resolve({ stdout, stderr, exitCode, executionTime, command, }); } }, options.timeout); } }); } export function executeBashSync(command, options = {}) { validateCommand(command); try { const result = execSync(command, { encoding: 'utf-8', cwd: options.cwd || process.cwd(), env: { ...process.env, ...options.env, }, shell: '/bin/bash', ...options, }); return result.toString(); } catch (error) { if (error.stdout) { return error.stdout.toString(); } throw error; } } //# sourceMappingURL=index.js.map