mcpipe
Version:
Decorate stdio MCP servers with debugging and other capabilities.
180 lines (179 loc) • 6.7 kB
JavaScript
;
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();
}
});
}