import { spawn, type ChildProcess } from "node:child_process"; import { connect, createServer } from "node:net"; export type SerenaServerManagerOptions = { defaultHost: string; defaultPort: number; context: string; startupTimeoutMs: number; pollIntervalMs: number; projectRoot: () => string; getPortOverride: () => number; }; export type SerenaServerManager = { resolveSerenaPort: () => Promise; isServerAvailable: () => Promise; ensureServerRunning: () => Promise; stopServer: () => Promise; getSerenaUrl: () => URL; getProjectRoot: () => string; }; export const createSerenaServerManager = ( options: SerenaServerManagerOptions, ): SerenaServerManager => { let serverProcess: ChildProcess | null = null; let serverStartPromise: Promise | null = null; let serenaPort = Number(options.getPortOverride() ?? "0"); let serenaUrl = new URL( `http://${options.defaultHost}:${serenaPort || options.defaultPort}/mcp`, ); const resolveSerenaPort = async () => { if (serenaPort !== 0) { serenaUrl = new URL(`http://${options.defaultHost}:${serenaPort}/mcp`); return serenaPort; } await new Promise((resolve, reject) => { const server = createServer(); server.listen(0, options.defaultHost, () => { const address = server.address(); if (typeof address === "object" && address?.port) { serenaPort = address.port; serenaUrl = new URL(`http://${options.defaultHost}:${serenaPort}/mcp`); } server.close((err) => (err ? reject(err) : resolve())); }); server.on("error", reject); }); return serenaPort; }; const isServerAvailable = async () => { const port = await resolveSerenaPort(); return new Promise((resolve) => { const socket = connect({ host: serenaUrl.hostname, port }, () => { socket.end(); resolve(true); }); socket.on("error", () => resolve(false)); }); }; const waitForServer = async () => { const deadline = Date.now() + options.startupTimeoutMs; while (Date.now() < deadline) { if (await isServerAvailable()) return true; if (serverProcess && serverProcess.exitCode !== null) return false; await new Promise((resolve) => setTimeout(resolve, options.pollIntervalMs)); } return false; }; const ensureServerRunning = async () => { const port = await resolveSerenaPort(); if (await isServerAvailable()) return; if (serverStartPromise) return serverStartPromise; serverStartPromise = (async () => { if (await isServerAvailable()) return; if (serverProcess && serverProcess.exitCode === null) { const ready = await waitForServer(); if (!ready) throw new Error("Serena MCP server failed to start."); return; } serverProcess = spawn( "uvx", [ "--from", "git+https://github.com/oraios/serena", "serena", "start-mcp-server", "--transport", "streamable-http", "--port", String(port), "--project", options.projectRoot(), "--context", options.context, ], { cwd: options.projectRoot(), env: { ...process.env }, stdio: "ignore", }, ); serverProcess.on("exit", () => { serverProcess = null; }); serverProcess.on("error", () => { serverProcess = null; }); const ready = await waitForServer(); if (!ready) throw new Error("Serena MCP server failed to start."); })(); try { await serverStartPromise; } finally { serverStartPromise = null; } }; const stopServer = async () => { if (!serverProcess) return; try { serverProcess.kill("SIGTERM"); } catch { // ignore shutdown errors } serverProcess = null; }; return { resolveSerenaPort, isServerAvailable, ensureServerRunning, stopServer, getSerenaUrl: () => serenaUrl, getProjectRoot: () => options.projectRoot(), }; };