UNPKG

mcpipe

Version:

Decorate stdio MCP servers with debugging and other capabilities.

180 lines (179 loc) 6.7 kB
"use strict"; var __importDefault = (this && this.__importDefault) || function (mod) { return (mod && mod.__esModule) ? mod : { "default": mod }; }; Object.defineProperty(exports, "__esModule", { value: true }); exports.startWrappedServer = startWrappedServer; const child_process_1 = require("child_process"); const readline_1 = __importDefault(require("readline")); const transforms_1 = require("./mcp/transforms"); /** * Formats a timestamp for debug logging */ function formatTimestamp() { return new Date().toISOString().replace('T', ' ').replace('Z', ''); } /** * Logs a debug message to stderr with timestamp and prefix */ function debugLog(prefix, category, message) { const timestamp = formatTimestamp(); process.stderr.write(`[${timestamp}] [${prefix}${category}] ${message}\n`); } /** * Safely formats a message for debug output, handling both JSON and string messages */ function formatMessageForDebug(message, maxLength = 1000) { if (typeof message === 'string') { return message.length > maxLength ? message.substring(0, maxLength) + '...[truncated]' : message; } try { const jsonString = JSON.stringify(message); return jsonString.length > maxLength ? jsonString.substring(0, maxLength) + '...[truncated]' : jsonString; } catch (error) { return '[Unable to serialize message]'; } } /** * Parses a line of text, attempting to convert it to a JSON object. * Returns the parsed object or the original line if parsing fails. */ function tryParseJson(line) { try { return JSON.parse(line); } catch (error) { // If it's not JSON, return the raw line return line; } } /** * Spawns the child MCP process with piped stdio and sets up interception. */ function startWrappedServer(prefix, command, args, debug = false) { if (debug) { debugLog(prefix, 'STARTUP', `Starting MCP server: ${command} ${args.join(' ')}`); debugLog(prefix, 'STARTUP', `Debug logging enabled for prefix: ${prefix}`); } const child = (0, child_process_1.spawn)(command, args, { stdio: ['pipe', 'pipe', 'pipe'], // stdin, stdout, stderr shell: true, // Add shell: true env: process.env, // Pass current environment variables }); if (debug) { debugLog(prefix, 'STARTUP', `Child process spawned with PID: ${child.pid}`); } // --- Stdio Handling --- const rlOut = readline_1.default.createInterface({ input: child.stdout }); const rlErr = readline_1.default.createInterface({ input: child.stderr }); const rlIn = readline_1.default.createInterface({ input: process.stdin }); // 1. Client -> Server (process.stdin -> child.stdin) rlIn.on('line', (line) => { const message = tryParseJson(line); if (debug) { debugLog(prefix, 'CLIENT→SERVER', formatMessageForDebug(message)); } const { message: transformedMessage, isModified } = (0, transforms_1.transformClientToServerMessage)(message, prefix); if (isModified) { const modifiedLine = JSON.stringify(transformedMessage) + '\n'; if (debug) { debugLog(prefix, 'TRANSFORM', `Tool name prefix removed`); debugLog(prefix, 'CLIENT→SERVER', `(Modified) ${formatMessageForDebug(transformedMessage)}`); } child.stdin.write(modifiedLine); } else { // Forward original line (JSON or non-JSON) child.stdin.write(line + '\n'); } }); // 2. Server -> Client (child.stdout -> process.stdout) rlOut.on('line', (line) => { const message = tryParseJson(line); if (debug) { debugLog(prefix, 'SERVER→CLIENT', formatMessageForDebug(message)); } const { message: transformedMessage, isModified, } = (0, transforms_1.transformServerToClientMessage)(message, prefix); if (isModified) { const modifiedLine = JSON.stringify(transformedMessage) + '\n'; if (debug) { debugLog(prefix, 'TRANSFORM', `Tool names prefixed with: ${prefix}`); debugLog(prefix, 'SERVER→CLIENT', `(Modified) ${formatMessageForDebug(transformedMessage)}`); } process.stdout.write(modifiedLine); } else { // Forward original line (JSON or non-JSON) process.stdout.write(line + '\n'); } }); // 3. Server Errors (child.stderr -> process.stderr) rlErr.on('line', (line) => { if (debug) { debugLog(prefix, 'SERVER_ERR', line); } else { process.stderr.write(`[${prefix} SERVER_ERR] ${line}\n`); } }); // --- Process Lifecycle Management --- const cleanup = (signal) => { if (debug) { debugLog(prefix, 'LIFECYCLE', `Received signal ${signal}, terminating child process`); } child.kill(signal); // Forward the signal }; process.on('SIGINT', () => cleanup('SIGINT')); process.on('SIGTERM', () => cleanup('SIGTERM')); child.on('close', (code, signal) => { if (signal) { const message = `Child process terminated by signal: ${signal}`; if (debug) { debugLog(prefix, 'LIFECYCLE', message); } else { console.warn(`[${prefix} WRAPPER] ${message}`); } // Propagate the signal exit to the wrapper process.kill(process.pid, signal); } else { const message = `Child process exited with code ${code}`; if (debug) { debugLog(prefix, 'LIFECYCLE', message); } else { console.warn(`[${prefix} WRAPPER] ${message}`); } process.exit(code !== null && code !== void 0 ? code : 1); // Exit wrapper with child's code } }); child.on('error', (err) => { const message = `Failed to start child process: ${err.message}`; if (debug) { debugLog(prefix, 'ERROR', message); } else { console.error(`[${prefix} WRAPPER] ${message}`); } process.exit(1); }); // Handle wrapper stdin closing process.stdin.on('close', () => { const message = 'Wrapper stdin closed. Closing child stdin.'; if (debug) { debugLog(prefix, 'LIFECYCLE', message); } else { console.warn(`[${prefix} WRAPPER] ${message}`); } if (!child.stdin.destroyed) { child.stdin.end(); } }); }