@debugmcp/mcp-debugger
Version:
Run-time step-through debugging for LLM agents.
357 lines • 13.5 kB
JavaScript
/**
* Production implementations of process launcher interfaces
* These delegate to the existing ProcessManager for actual process operations
*/
import { EventEmitter } from 'events';
/**
* Adapter to wrap IChildProcess as IProcess
* Provides a cleaner interface while delegating to the underlying child process
*/
class ProcessAdapter extends EventEmitter {
childProcess;
_exitCode = null;
_signalCode = null;
childProcessListeners = []; // eslint-disable-line @typescript-eslint/no-explicit-any
constructor(childProcess) {
super();
this.childProcess = childProcess;
// Create event handlers
const exitHandler = (code, signal) => {
this._exitCode = code;
this._signalCode = signal;
this.emit('exit', code, signal);
};
const closeHandler = (code, signal) => {
this.emit('close', code, signal);
};
const errorHandler = (error) => {
this.emit('error', error);
};
const spawnHandler = () => {
this.emit('spawn');
};
const messageHandler = (message) => {
this.emit('message', message);
};
// Add listeners and track them
childProcess.on('exit', exitHandler);
childProcess.on('close', closeHandler);
childProcess.on('error', errorHandler);
childProcess.on('spawn', spawnHandler);
childProcess.on('message', messageHandler);
// Track all listeners for cleanup
this.childProcessListeners.push({ event: 'exit', listener: exitHandler }, { event: 'close', listener: closeHandler }, { event: 'error', listener: errorHandler }, { event: 'spawn', listener: spawnHandler }, { event: 'message', listener: messageHandler });
// Add a default error handler to prevent unhandled errors
// This ensures that errors don't throw if no other handlers are attached
this.on('error', () => {
// Default error handler - prevents Node.js from throwing
// Actual error handling should be done by subclasses or external handlers
});
}
get pid() {
return this.childProcess.pid;
}
get stdin() {
return this.childProcess.stdin;
}
get stdout() {
return this.childProcess.stdout;
}
get stderr() {
return this.childProcess.stderr;
}
get killed() {
return this.childProcess.killed;
}
get exitCode() {
return this._exitCode;
}
get signalCode() {
return this._signalCode;
}
send(message) {
return this.childProcess.send(message);
}
kill(signal) {
return this.childProcess.kill(signal);
}
}
/**
* Production implementation of IProcessLauncher
*/
export class ProcessLauncherImpl {
processManager;
constructor(processManager) {
this.processManager = processManager;
}
launch(command, args, options) {
const childProcess = this.processManager.spawn(command, args, options);
return new ProcessAdapter(childProcess);
}
}
/**
* Production implementation of IDebugTargetLauncher
*/
export class DebugTargetLauncherImpl {
processLauncher;
networkManager;
constructor(processLauncher, networkManager) {
this.processLauncher = processLauncher;
this.networkManager = networkManager;
}
async launchPythonDebugTarget(scriptPath, args, pythonPath = 'python', debugPort) {
// Find a free port if not specified
const port = debugPort || await this.networkManager.findFreePort();
// Launch Python with debugpy
const debugArgs = [
'-m', 'debugpy',
'--listen', `127.0.0.1:${port}`,
'--wait-for-client',
scriptPath,
...args
];
// No cwd manipulation - let the process inherit the current working directory
const debugProcess = this.processLauncher.launch(pythonPath, debugArgs);
return {
process: debugProcess,
debugPort: port,
terminate: async () => {
return new Promise((resolve) => {
if (debugProcess.killed) {
resolve();
return;
}
debugProcess.once('exit', () => resolve());
debugProcess.kill('SIGTERM');
// Force kill after timeout
setTimeout(() => {
if (!debugProcess.killed) {
debugProcess.kill('SIGKILL');
}
resolve();
}, 5000);
});
}
};
}
}
/**
* Proxy process adapter that adds proxy-specific functionality
*/
class ProxyProcessAdapter extends ProcessAdapter {
sessionId;
initializationPromise;
initializationResolve;
initializationReject;
initializationState = 'none';
initializationCleanup;
disposed = false;
constructor(childProcess, sessionId) {
super(childProcess);
this.sessionId = sessionId;
// NO promise creation here - wait for waitForInitialization()
// Set up early exit handler
this.once('exit', this.handleEarlyExit.bind(this));
// Set up error handling immediately to prevent unhandled errors
// This must be done in the constructor to catch any early errors
this.setupErrorHandling();
}
setupErrorHandling() {
// Override error handling to support DAP spec
// Note: We must handle errors to prevent Node.js from throwing unhandled errors
this.on('error', () => {
if (this.initializationState === 'waiting') {
// Both reject promise AND emit event (DAP spec requirement)
// The failInitialization will reject the promise, but the error event
// will still be emitted for other listeners
// Don't pass the error directly - this will be handled by exit event
}
// Error is handled - prevent default throw behavior
});
}
createInitializationPromise(timeout) {
return new Promise((resolve, reject) => {
this.initializationResolve = resolve;
this.initializationReject = reject;
// Set up message handler
const messageHandler = (message) => {
const msg = message;
if (msg?.type === 'status' &&
(msg.status === 'adapter_configured_and_launched' ||
msg.status === 'dry_run_complete')) {
this.completeInitialization();
}
};
this.on('message', messageHandler);
// Set up timeout
const timeoutId = setTimeout(() => {
if (this.initializationState === 'waiting') {
this.failInitialization(new Error('Proxy initialization timeout'));
}
}, timeout);
// Store cleanup info
this.initializationCleanup = () => {
this.removeListener('message', messageHandler);
clearTimeout(timeoutId);
};
});
}
completeInitialization() {
if (this.initializationState !== 'waiting')
return;
this.initializationState = 'completed';
if (this.initializationResolve) {
this.initializationResolve();
this.initializationResolve = undefined;
this.initializationReject = undefined;
}
this.cleanupInitialization();
}
failInitialization(error) {
if (this.initializationState !== 'waiting')
return;
this.initializationState = 'failed';
if (this.initializationReject) {
this.initializationReject(error);
this.initializationResolve = undefined;
this.initializationReject = undefined;
}
this.cleanupInitialization();
}
cleanupInitialization() {
if (this.initializationCleanup) {
this.initializationCleanup();
this.initializationCleanup = undefined;
}
}
handleEarlyExit() {
if (this.initializationState === 'waiting' && this.initializationReject) {
// Only reject if someone is waiting for initialization
this.failInitialization(new Error('Proxy process exited before initialization'));
}
else if (this.initializationState === 'none') {
// Process exited without initialization being requested
// Mark as failed to prevent future initialization attempts
this.initializationState = 'failed';
}
this.dispose();
}
dispose() {
if (this.disposed)
return;
this.disposed = true;
// Clean up initialization resources
this.cleanupInitialization();
// Remove all listeners from this adapter
this.removeAllListeners();
// Remove listeners from the underlying childProcess
for (const { event, listener } of this.childProcessListeners) {
this.childProcess.removeListener(event, listener);
}
this.childProcessListeners = [];
}
sendCommand(command) {
// Send object directly - Node.js IPC will handle serialization
this.send(command);
}
async waitForInitialization(timeout = 30000) {
// Handle completed states
if (this.initializationState === 'completed') {
return; // Already initialized
}
if (this.initializationState === 'failed') {
throw new Error('Initialization already completed or failed');
}
// Handle concurrent calls - return existing promise if in progress
if (this.initializationState === 'waiting' && this.initializationPromise) {
return this.initializationPromise;
}
// Create promise only when first requested
if (!this.initializationPromise) {
this.initializationState = 'waiting';
this.initializationPromise = this.createInitializationPromise(timeout);
}
return this.initializationPromise;
}
kill(signal) {
if (this.killed || this.disposed) {
return false; // Already killed or disposed
}
// If waiting for initialization, fail it
if (this.initializationState === 'waiting') {
this.failInitialization(new Error('Process killed during initialization'));
}
const result = super.kill(signal);
// Ensure disposal happens after kill
if (result) {
this.once('exit', () => this.dispose());
}
return result;
}
}
/**
* Production implementation of IProxyProcessLauncher
*/
export class ProxyProcessLauncherImpl {
processLauncher;
constructor(processLauncher) {
this.processLauncher = processLauncher;
}
launchProxy(proxyScriptPath, sessionId, env) {
const diagnosticFlags = ['--trace-uncaught', '--trace-exit'];
const args = [...diagnosticFlags, proxyScriptPath];
// Convert process.env to ensure all values are strings
// Filter out test-related environment variables to ensure proxy runs normally
const processEnv = {};
if (env) {
Object.assign(processEnv, env);
}
else {
for (const [key, value] of Object.entries(process.env)) {
if (value !== undefined) {
// Skip test-related environment variables
if (key === 'NODE_ENV' || key === 'VITEST' || key === 'JEST_WORKER_ID') {
continue;
}
processEnv[key] = value;
}
}
}
// Ensure the proxy knows it's not in test mode
delete processEnv.NODE_ENV;
delete processEnv.VITEST;
delete processEnv.JEST_WORKER_ID;
const options = {
stdio: ['pipe', 'pipe', 'pipe', 'ipc'], // eslint-disable-line @typescript-eslint/no-explicit-any -- Required for Node.js StdioOptions IPC compatibility
env: processEnv,
cwd: process.cwd() // Ensure proxy runs from the MCP server's working directory
};
const launchedProcess = this.processLauncher.launch(process.execPath, args, options);
// Cast to ProcessAdapter to access the underlying child process
const processAdapter = launchedProcess;
return new ProxyProcessAdapter(processAdapter['childProcess'], sessionId);
}
}
/**
* Production implementation of IProcessLauncherFactory
*/
export class ProcessLauncherFactoryImpl {
processManager;
networkManager;
constructor(processManager, networkManager) {
this.processManager = processManager;
this.networkManager = networkManager;
}
createProcessLauncher() {
return new ProcessLauncherImpl(this.processManager);
}
createDebugTargetLauncher() {
const processLauncher = this.createProcessLauncher();
return new DebugTargetLauncherImpl(processLauncher, this.networkManager);
}
createProxyProcessLauncher() {
const processLauncher = this.createProcessLauncher();
return new ProxyProcessLauncherImpl(processLauncher);
}
}
//# sourceMappingURL=process-launcher-impl.js.map