@gabrielmaialva33/mcp-filesystem
Version:
MCP server for secure filesystem access
218 lines • 7.5 kB
JavaScript
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