Files
OmniRoute/bin/mcp-server.mjs
SAMUEL AUGUSTO GUIMARAES LOPES 236aad07c7 fix(mcp): stop DB init logging from corrupting the stdio JSON-RPC stream (#9281)
Merge-train validated (tip 6ce4effef8). Vitest failures confirmed as base-red (#9679).
2026-08-07 20:52:26 -03:00

80 lines
2.9 KiB
JavaScript

#!/usr/bin/env node
import { spawn } from "node:child_process";
import { existsSync } from "node:fs";
import { dirname, join } from "node:path";
import { fileURLToPath, pathToFileURL } from "node:url";
const __filename = fileURLToPath(import.meta.url);
const __dirname = dirname(__filename);
const ROOT = join(__dirname, "..");
/**
* @param {string} [rootDir] - project root directory (defaults to package root)
* @param {(path: string) => boolean} [existsSyncFn] - injectable fs.existsSync
* for testing; defaults to the real existsSync
* @returns {string|null} resolved absolute path to the MCP server entry, or null
*/
export function resolveMcpEntry(rootDir = ROOT, existsSyncFn = existsSync) {
const candidates = [
// Preferred distributable JS entry (npm publish artifact, built by prepublish.ts)
join(rootDir, "dist", "open-sse", "mcp-server", "server.js"),
// Local workspace TypeScript source fallback
join(rootDir, "open-sse", "mcp-server", "server.ts"),
];
for (const entry of candidates) {
if (existsSyncFn(entry)) return entry;
}
return null;
}
function formatSpawnError(exitCode, signal) {
if (signal) return `MCP server exited by signal ${signal}`;
return `MCP server exited with code ${exitCode ?? 1}`;
}
export async function startMcpCli(rootDir = ROOT) {
const mcpEntry = resolveMcpEntry(rootDir);
if (!mcpEntry) {
throw new Error(
"MCP server entrypoint not found. Expected dist/open-sse/mcp-server/server.js or open-sse/mcp-server/server.ts."
);
}
// `tsx` loader is only required for local `.ts` fallback; JS entry works without it.
const tsxLoaderArgs = mcpEntry.endsWith(".ts") ? ["--import", "tsx"] : [];
// Preload the stdout/stderr console guard before mcpEntry's own module graph evaluates —
// DB init (a side effect of createMcpServer()'s tool registration) logs via plain
// console.log, and by the time any code inside mcpEntry itself could redirect it, that
// module's own (hoisted) imports have already run. Loading the guard first, in a separate
// module, is the only point early enough to guarantee it never leaks into the JSON-RPC
// stream on stdout.
const consoleGuard = pathToFileURL(join(__dirname, "mcpStdioConsoleGuard.mjs")).href;
const loaderArgs = ["--import", consoleGuard, ...tsxLoaderArgs];
await new Promise((resolve, reject) => {
const child = spawn(process.execPath, [...loaderArgs, mcpEntry], {
cwd: rootDir,
env: process.env,
stdio: "inherit",
});
child.once("error", reject);
child.once("exit", (code, signal) => {
if ((code ?? 0) === 0 && !signal) {
resolve(undefined);
return;
}
reject(new Error(formatSpawnError(code, signal)));
});
});
}
if (process.argv[1] && fileURLToPath(import.meta.url) === process.argv[1]) {
startMcpCli().catch((err) => {
console.error("\x1b[31m✖ Failed to start MCP server:\x1b[0m", err?.message || err);
process.exit(1);
});
}