@gabrielmaialva33/mcp-filesystem
Version:
MCP server for secure filesystem access
146 lines • 4.48 kB
JavaScript
import { exec } 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';
import { validatePath } from '../path.js';
const execPromise = promisify(exec);
const SAFE_COMMAND_REGEX = /^[a-zA-Z0-9_\-./\s:;,|><&{}()\[\]'"$%+*!?~=]+$/;
const DEV_COMMANDS = [
'npm',
'pnpm',
'yarn',
'npx',
'node',
'ts-node',
'tsc',
'eslint',
'prettier',
'jest',
'vitest',
'mocha',
'git',
'find',
'grep',
'sed',
'awk',
'cat',
'ls',
'cd',
'cp',
'mv',
];
const FORBIDDEN_COMMANDS = [
'rm -rf',
'rm -rf /',
'rm -rf /*',
'rm -r /',
'rmdir',
'dd',
'mkfs',
'format',
'wget',
'curl -O',
'curl --output',
'chmod 777',
'chmod -R 777',
'sudo',
'su',
'doas',
':(){:|:&};:',
];
export const BashExecuteArgsSchema = z.object({
command: z.string().describe('The bash command to execute'),
workingDir: z.string().optional().describe('Working directory for command execution'),
timeout: z
.number()
.int()
.positive()
.max(60000)
.optional()
.default(30000)
.describe('Maximum execution time in milliseconds (max 60s)'),
env: z.record(z.string()).optional().describe('Additional environment variables for the command'),
});
export const BashPipeArgsSchema = z.object({
commands: z.array(z.string()).min(1).describe('Array of commands to pipe together'),
workingDir: z.string().optional().describe('Working directory for command execution'),
timeout: z
.number()
.int()
.positive()
.max(60000)
.optional()
.default(30000)
.describe('Maximum execution time in milliseconds (max 60s)'),
env: z.record(z.string()).optional().describe('Additional environment variables for the command'),
});
function validateCommand(command) {
if (FORBIDDEN_COMMANDS.some((forbidden) => command.includes(forbidden))) {
throw new FileSystemError(`Command contains forbidden operations`, 'FORBIDDEN_COMMAND', undefined, { command });
}
const baseCommand = command.split(' ')[0].trim();
const isDevCommand = DEV_COMMANDS.some((cmd) => baseCommand === cmd || baseCommand.endsWith(`/${cmd}`));
if (isDevCommand) {
return true;
}
if (!SAFE_COMMAND_REGEX.test(command)) {
throw new FileSystemError(`Command contains potentially unsafe characters`, 'UNSAFE_COMMAND', undefined, { command });
}
return true;
}
export async function bashExecute(args, config) {
validateCommand(args.command);
const cwd = args.workingDir ? await validatePath(args.workingDir, config) : process.cwd();
const options = {
cwd,
timeout: args.timeout || 30000,
env: args.env ? { ...process.env, ...args.env } : process.env,
encoding: 'utf8',
maxBuffer: 10 * 1024 * 1024,
};
try {
await logger.debug(`Executing bash command: ${args.command}`, {
workingDir: cwd,
timeout: options.timeout,
});
const { stdout, stderr } = await execPromise(args.command, options);
await logger.debug(`Command executed successfully: ${args.command}`, {
exitCode: 0,
stdoutPreview: stdout.substring(0, 100) + (stdout.length > 100 ? '...' : ''),
});
return {
stdout,
stderr,
exitCode: 0,
};
}
catch (error) {
const stderr = error.stderr || '';
const stdout = error.stdout || '';
const exitCode = error.code || 1;
await logger.warn(`Command execution failed: ${args.command}`, {
exitCode,
stderr: stderr.substring(0, 100) + (stderr.length > 100 ? '...' : ''),
});
return {
stdout,
stderr,
exitCode,
};
}
}
export async function bashPipe(args, config) {
for (const command of args.commands) {
validateCommand(command);
}
args.workingDir ? await validatePath(args.workingDir, config) : process.cwd();
const pipedCommand = args.commands.join(' | ');
return bashExecute({
command: pipedCommand,
workingDir: args.workingDir,
timeout: args.timeout,
env: args.env,
}, config);
}
//# sourceMappingURL=bash_tools.js.map