mcp-extended-tools
Version:
Extended tools for command execution
240 lines (204 loc) • 7.15 kB
JavaScript
import { spawn } from 'child_process';
import { EventEmitter } from 'events';
import path from 'path';
import fs from 'fs/promises';
import winston from 'winston';
// Simple debug logging function
function debug(message) {
console.log(`[DEBUG] ${message}`);
}
class CommandExecutor extends EventEmitter {
constructor(options = {}) {
super();
this.activeProcesses = new Map();
this.watchers = new Map();
// Configure logging
this.logger = winston.createLogger({
level: options.logLevel || 'info',
format: winston.format.combine(
winston.format.timestamp(),
winston.format.json()
),
defaultMeta: { service: 'command-executor' },
transports: [
new winston.transports.File({
filename: path.join(options.logDir || 'logs', 'error.log'),
level: 'error'
}),
new winston.transports.File({
filename: path.join(options.logDir || 'logs', 'combined.log')
})
]
});
// Allowed commands whitelist
this.allowedCommands = [
'npm', 'yarn', 'pnpm', 'git', 'node', 'jest',
'vitest', 'vite', 'webpack', 'python', 'pip'
];
// Initialize log directory
this.initializeLogDirectory(options.logDir || 'logs');
}
async initializeLogDirectory(logDir) {
try {
await fs.mkdir(logDir, { recursive: true });
debug(`Log directory initialized: ${logDir}`);
} catch (error) {
debug(`Error creating log directory: ${error.message}`);
throw new Error(`Cannot create log directory: ${error.message}`);
}
}
isCommandAllowed(command) {
const firstArg = command.split(' ')[0];
return this.allowedCommands.includes(firstArg);
}
async execute(command, args = [], options = {}) {
if (!this.isCommandAllowed(command)) {
const error = new Error('Command not allowed');
this.logger.error('Command execution blocked', { command, error: error.message });
throw error;
}
return new Promise((resolve, reject) => {
const processId = Date.now().toString();
const startTime = new Date();
debug(`Executing command: ${command} ${args.join(' ')}`);
const process = spawn(command, args, {
shell: true,
...options
});
this.activeProcesses.set(processId, {
process,
command,
args,
startTime,
options
});
let stdout = '';
let stderr = '';
process.stdout.on('data', (data) => {
stdout += data;
this.emit('output', {
processId,
type: 'stdout',
data: data.toString()
});
});
process.stderr.on('data', (data) => {
stderr += data;
this.emit('output', {
processId,
type: 'stderr',
data: data.toString()
});
});
process.on('close', (code) => {
const endTime = new Date();
const runtime = endTime - startTime;
this.logger.info('Command execution completed', {
processId,
command,
code,
runtime
});
this.activeProcesses.delete(processId);
if (code === 0) {
resolve({
processId,
stdout,
stderr,
code,
runtime
});
} else {
reject({
processId,
stdout,
stderr,
code,
runtime
});
}
});
process.on('error', (error) => {
this.logger.error('Command execution failed', {
processId,
command,
error: error.message
});
this.activeProcesses.delete(processId);
reject({
processId,
error: error.message,
code: 1
});
});
});
}
async stop(processId, options = { force: false }) {
const processInfo = this.activeProcesses.get(processId);
if (!processInfo) {
debug(`Process not found: ${processId}`);
return false;
}
const { process } = processInfo;
debug(`Stopping process: ${processId}, force: ${options.force}`);
if (options.force) {
process.kill('SIGKILL');
} else {
process.kill('SIGTERM');
await new Promise(resolve => {
const timeout = setTimeout(() => {
process.kill('SIGKILL');
resolve();
}, 5000);
process.on('close', () => {
clearTimeout(timeout);
resolve();
});
});
}
this.activeProcesses.delete(processId);
this.logger.info('Process stopped', { processId, force: options.force });
return true;
}
listActive() {
return Array.from(this.activeProcesses.entries()).map(([id, info]) => ({
processId: id,
command: info.command,
args: info.args,
startTime: info.startTime,
runtime: new Date() - info.startTime,
pid: info.process.pid
}));
}
async cleanup() {
debug('Cleaning up all processes and watchers');
// Stop all watchers
for (const [processId, watcher] of this.watchers) {
watcher.close();
this.watchers.delete(processId);
}
// Stop all processes
const processes = this.listActive();
await Promise.all(
processes.map(proc => this.stop(proc.processId, { force: true }))
);
this.logger.info('Cleanup completed', {
processesTerminated: processes.length,
watchersClosed: this.watchers.size
});
}
}
// Create singleton instance
const executor = new CommandExecutor();
// Cleanup on exit
process.on('SIGINT', () => executor.cleanup());
process.on('SIGTERM', () => executor.cleanup());
export {
CommandExecutor,
executor,
executor as default,
executeCommand
};
function executeCommand(command, args, options) {
return executor.execute(command, args, options);
}