diff --git a/src/_internal/browser-eval-manager.ts b/src/_internal/browser-eval-manager.ts index dd6dea6..fd480ef 100644 --- a/src/_internal/browser-eval-manager.ts +++ b/src/_internal/browser-eval-manager.ts @@ -122,15 +122,16 @@ export async function stopBrowserEvalMCP(): Promise { } /** - * Cleanup on process exit + * Cleanup on process exit. + * Signal handling and process.exit() are intentionally left to src/index.ts + * so there is a single, ordered shutdown sequence. These listeners only close + * the Playwright MCP connection; they do not call process.exit() themselves. */ process.on("SIGINT", async () => { await stopBrowserEvalMCP() - process.exit(0) }) process.on("SIGTERM", async () => { await stopBrowserEvalMCP() - process.exit(0) }) diff --git a/src/index.ts b/src/index.ts index ed9ff03..04e4870 100644 --- a/src/index.ts +++ b/src/index.ts @@ -303,9 +303,19 @@ async function main() { log('Server started') - const shutdown = () => { + const shutdown = async () => { log('Server terminated') + // Close the MCP server transport before exiting so the host (e.g. Claude + // Code) receives a clean EOF on the stdio channel instead of an abrupt + // disconnect. Without this the host reports "MCP server failed" even + // though the exit code is 0. + try { + await server.close() + } catch { + // Ignore close errors during shutdown + } + const aggregationJSON = getSessionAggregationJSON() if (aggregationJSON) {