@debugmcp/mcp-debugger
Version:
Run-time step-through debugging for LLM agents.
221 lines • 8.25 kB
JavaScript
/**
* DAP Proxy Core - Pure business logic without side effects
*
* This module contains the core proxy runner functionality that can be
* instantiated and controlled programmatically without auto-execution.
*/
import readline from 'readline';
import { DapProxyWorker } from './dap-proxy-worker.js';
import { MessageParser } from './dap-proxy-message-parser.js';
import { ProxyState } from './dap-proxy-interfaces.js';
/**
* Core proxy runner that encapsulates all proxy logic
* without auto-execution or environment detection
*/
export class ProxyRunner {
dependencies;
options;
worker;
logger;
rl;
messageHandler;
isRunning = false;
constructor(dependencies, logger, options = {}) {
this.dependencies = dependencies;
this.options = options;
this.worker = new DapProxyWorker(dependencies);
this.logger = logger;
}
/**
* Start the proxy runner and set up communication channels
*/
async start() {
if (this.isRunning) {
throw new Error('Proxy runner is already running');
}
this.isRunning = true;
this.logger.info('[ProxyRunner] Starting proxy runner...');
try {
// Set up message processing
const processMessage = this.options.onMessage || this.createMessageProcessor();
// Set up communication channels based on options and availability
if (this.options.useIPC !== false && typeof process.send === 'function') {
this.setupIPCCommunication(processMessage);
}
else if (this.options.useStdin !== false) {
this.setupStdinCommunication(processMessage);
}
else {
this.logger.warn('[ProxyRunner] No communication channel configured');
}
this.logger.info('[ProxyRunner] Ready to receive commands');
}
catch (error) {
this.isRunning = false;
this.logger.error('[ProxyRunner] Failed to start:', error);
throw error;
}
}
/**
* Stop the proxy runner and clean up resources
*/
async stop() {
if (!this.isRunning) {
return;
}
this.logger.info('[ProxyRunner] Stopping proxy runner...');
// Shutdown worker
await this.worker.shutdown();
// Clean up communication channels
if (this.messageHandler && process.removeListener) {
process.removeListener('message', this.messageHandler);
}
if (this.rl) {
this.rl.close();
}
this.isRunning = false;
this.logger.info('[ProxyRunner] Stopped');
}
/**
* Get the current worker state
*/
getWorkerState() {
return this.worker.getState();
}
/**
* Get the worker instance (for testing)
*/
getWorker() {
return this.worker;
}
/**
* Create the default message processor
*/
createMessageProcessor() {
return async (messageStr) => {
this.logger.info(`[ProxyRunner] Received message (first 200 chars): ${messageStr.substring(0, 200)}...`);
let command = null;
try {
command = MessageParser.parseCommand(messageStr);
await this.worker.handleCommand(command);
}
catch (error) {
const errorMsg = MessageParser.getErrorMessage(error);
this.logger.error('[ProxyRunner] Error processing message:', { error: errorMsg });
this.dependencies.messageSender.send({
type: 'error',
message: `Proxy error processing command: ${errorMsg}`,
sessionId: command?.sessionId || 'unknown'
});
}
// Check if we should exit after handling the command
if (this.worker.getState() === ProxyState.TERMINATED) {
const isDryRun = command?.cmd === 'init' && command.dryRunSpawn;
const exitDelay = isDryRun ? 500 : 0;
this.logger.info(`[ProxyRunner] Worker state is TERMINATED. Exiting in ${exitDelay}ms.`);
setTimeout(() => {
process.exit(0);
}, exitDelay);
}
};
}
/**
* Set up IPC communication channel
*/
setupIPCCommunication(processMessage) {
this.logger.info('[ProxyRunner] Setting up IPC communication');
this.messageHandler = async (message) => {
this.logger.debug('[ProxyRunner] IPC message received');
if (typeof message === 'string') {
await processMessage(message);
}
else if (typeof message === 'object' && message !== null) {
this.logger.debug('[ProxyRunner] Received object message, stringifying:', message);
try {
await processMessage(JSON.stringify(message));
}
catch (e) {
this.logger.error('[ProxyRunner] Could not process object message:', {
message,
error: MessageParser.getErrorMessage(e)
});
}
}
else {
this.logger.warn('[ProxyRunner] Received message of unexpected type:', typeof message, message);
}
};
process.on('message', this.messageHandler);
}
/**
* Set up stdin/readline communication channel
*/
setupStdinCommunication(processMessage) {
this.logger.info('[ProxyRunner] Setting up stdin/readline communication');
this.rl = readline.createInterface({
input: process.stdin,
output: process.stdout,
terminal: false
});
this.rl.on('line', (line) => processMessage(line));
}
/**
* Set up global error handlers
*/
setupGlobalErrorHandlers(errorShutdown, getCurrentSessionId) {
// Uncaught exception handler
process.on('uncaughtException', (error) => {
this.logger.error('[ProxyRunner] Uncaught exception:', error);
const sessionId = getCurrentSessionId() || 'unknown';
this.dependencies.messageSender.send({
type: 'error',
message: `Proxy uncaught exception: ${error.message}`,
sessionId
});
errorShutdown().finally(() => {
process.exit(1);
});
});
// Unhandled rejection handler
process.on('unhandledRejection', (reason, promise) => {
this.logger.error('[ProxyRunner] Unhandled rejection:', { reason, promise });
const sessionId = getCurrentSessionId() || 'unknown';
this.dependencies.messageSender.send({
type: 'error',
message: `Proxy unhandled rejection: ${reason}`,
sessionId
});
});
// SIGTERM handler
process.on('SIGTERM', () => {
this.logger.info('[ProxyRunner] Received SIGTERM, shutting down gracefully');
errorShutdown().finally(() => {
process.exit(0);
});
});
// SIGINT handler
process.on('SIGINT', () => {
this.logger.info('[ProxyRunner] Received SIGINT, shutting down gracefully');
errorShutdown().finally(() => {
process.exit(0);
});
});
}
}
/**
* Detect if the module is being run directly or as a worker
*/
export function detectExecutionMode() {
const isDirectRun = (typeof require !== 'undefined' && require.main === module) ||
(typeof import.meta !== 'undefined' && import.meta.url === `file://${process.argv[1]}`);
const hasIPC = typeof process.send === 'function';
const isWorkerEnv = process.env.DAP_PROXY_WORKER === 'true';
return { isDirectRun, hasIPC, isWorkerEnv };
}
/**
* Check if the module should auto-execute based on execution mode
*/
export function shouldAutoExecute(mode) {
return mode.isDirectRun || mode.hasIPC || mode.isWorkerEnv;
}
//# sourceMappingURL=dap-proxy-core.js.map