mcp-extended-tools
Version:
Extended tools for command execution
265 lines (232 loc) • 8.11 kB
JavaScript
const { exec, spawn } = require('child_process');
const { EventEmitter } = require('events');
const path = require('path');
class CommandExecutor extends EventEmitter {
constructor() {
super();
this.activeProcesses = new Map();
this.watchers = new Map();
}
// Original execute command - now more configurable
execute(command, args = [], options = {}) {
return new Promise((resolve, reject) => {
const process = spawn(command, args, {
shell: true,
...options,
env: {
...process.env,
...options.env
}
});
const processId = Date.now().toString();
this.activeProcesses.set(processId, {
process,
command,
args,
options,
startTime: new Date()
});
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 processInfo = this.activeProcesses.get(processId);
this.activeProcesses.delete(processId);
if (code === 0) {
resolve({
processId,
stdout,
stderr,
code,
runtime: new Date() - processInfo.startTime
});
} else {
reject({
processId,
stdout,
stderr,
code,
runtime: new Date() - processInfo.startTime
});
}
});
process.on('error', (error) => {
const processInfo = this.activeProcesses.get(processId);
this.activeProcesses.delete(processId);
reject({
processId,
error: error.message,
stderr,
code: 1,
runtime: new Date() - processInfo.startTime
});
});
});
}
// Enhanced process management
async stop(processId, options = { force: false }) {
const processInfo = this.activeProcesses.get(processId);
if (processInfo) {
const { process } = processInfo;
if (options.force) {
process.kill('SIGKILL');
} else {
process.kill('SIGTERM');
// Wait for graceful shutdown
await new Promise(resolve => {
const timeout = setTimeout(() => {
process.kill('SIGKILL');
resolve();
}, 5000);
process.on('close', () => {
clearTimeout(timeout);
resolve();
});
});
}
this.activeProcesses.delete(processId);
return true;
}
return false;
}
// Enhanced process listing with more details
listActive() {
const processes = [];
for (const [id, info] of this.activeProcesses) {
processes.push({
processId: id,
command: info.command,
args: info.args,
startTime: info.startTime,
runtime: new Date() - info.startTime,
pid: info.process.pid
});
}
return processes;
}
// New method - Watch and auto-restart
async watchAndExecute(command, args = [], options = {}) {
const {
watch = ['.'],
ignore = ['node_modules/**', '**/*.pyc', '__pycache__/**'],
restartDelay = 1000,
...spawnOptions
} = options;
// Initial start
let currentProcess = await this.execute(command, args, spawnOptions);
// Setup watcher
const chokidar = require('chokidar');
const watcher = chokidar.watch(watch, {
ignored: ignore,
persistent: true
});
watcher.on('change', async (path) => {
this.emit('file-change', { path });
// Debounce restart
if (this.restartTimeout) {
clearTimeout(this.restartTimeout);
}
this.restartTimeout = setTimeout(async () => {
// Stop current process
await this.stop(currentProcess.processId);
// Start new process
try {
currentProcess = await this.execute(command, args, spawnOptions);
this.emit('restart', {
processId: currentProcess.processId,
trigger: path
});
} catch (error) {
this.emit('restart-error', error);
}
}, restartDelay);
});
// Store watcher reference
this.watchers.set(currentProcess.processId, watcher);
return {
processId: currentProcess.processId,
stop: async () => {
watcher.close();
this.watchers.delete(currentProcess.processId);
await this.stop(currentProcess.processId);
}
};
}
// New method - Server process management
async manageServer(config) {
const {
command,
args = [],
port = 'auto',
autoRestart = true,
...options
} = config;
// Auto port assignment if needed
if (port === 'auto') {
const portfinder = require('portfinder');
options.env = {
...options.env,
PORT: await portfinder.getPortPromise()
};
} else {
options.env = {
...options.env,
PORT: port
};
}
// Start server with auto-restart
if (autoRestart) {
return this.watchAndExecute(command, args, {
...options,
watch: [options.watch || '.'],
ignore: [
...(options.ignore || []),
'node_modules/**',
'**/*.pyc',
'__pycache__/**'
]
});
}
// Start server without auto-restart
return this.execute(command, args, options);
}
// Cleanup method
async cleanup() {
// 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 }))
);
}
}
// Create singleton instance
const executor = new CommandExecutor();
// Cleanup on exit
process.on('SIGINT', () => executor.cleanup());
process.on('SIGTERM', () => executor.cleanup());
module.exports = {
executor,
executeCommand: (command, args, options) => executor.execute(command, args, options),
watchAndExecute: (command, args, options) => executor.watchAndExecute(command, args, options),
manageServer: (config) => executor.manageServer(config)
};