From 16fce8229b862632cf840fd5cbe4492c8e6557b0 Mon Sep 17 00:00:00 2001 From: Bryon Bowman <97320404+btbowman@users.noreply.github.com> Date: Mon, 16 Feb 2026 12:30:06 -0700 Subject: [PATCH] fix: add pre-flight port check and graceful shutdown to prevent EADDRINUSE race condition MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Three bugs caused the Inspector to report "PORT IS IN USE" even when ports were free, and to leave ports in CLOSE_WAIT after Ctrl+C: 1. No pre-flight port availability check — both the client (client.js) and server (index.ts) attempted to bind directly without first verifying the port was free, leading to confusing EADDRINUSE errors from stale processes. 2. Missing process.exit(1) in client EADDRINUSE handler — the client's error handler logged the error but never exited, leaving the process hanging indefinitely. 3. No SIGINT/SIGTERM graceful shutdown — neither process registered signal handlers, so Ctrl+C left TCP sockets in CLOSE_WAIT state. The next launch would then fail with EADDRINUSE because the OS hadn't released the port. Fixes: - Add checkPort() pre-flight check to both client/bin/client.js and server/src/index.ts that tests port availability before attempting to bind - Add process.exit(1) to the client's EADDRINUSE error handler - Add SIGINT/SIGTERM handlers with server.close() for graceful shutdown in both client and server, with a 3-second force-exit timeout - Provide actionable error messages with lsof/kill instructions Resolves #1090 Co-Authored-By: Claude Opus 4.6 --- client/bin/client.js | 47 ++++++++++++++++++++++++++++++++++++++++++-- server/src/index.ts | 37 ++++++++++++++++++++++++++++++++++ 2 files changed, 82 insertions(+), 2 deletions(-) diff --git a/client/bin/client.js b/client/bin/client.js index 2a7419e66..28c31d1bb 100755 --- a/client/bin/client.js +++ b/client/bin/client.js @@ -5,10 +5,40 @@ import { join, dirname } from "path"; import { fileURLToPath } from "url"; import handler from "serve-handler"; import http from "http"; +import net from "net"; const __dirname = dirname(fileURLToPath(import.meta.url)); const distPath = join(__dirname, "../dist"); +const port = parseInt(process.env.CLIENT_PORT || "6274", 10); +const host = process.env.HOST || "localhost"; + +// Check port availability before attempting to bind. +// Prevents confusing EADDRINUSE errors from stale processes. +function checkPort(targetHost, targetPort) { + return new Promise((resolve) => { + const tester = net.createServer(); + tester.once("error", (err) => { + resolve(false); + }); + tester.once("listening", () => { + tester.close(() => resolve(true)); + }); + tester.listen(targetPort, targetHost); + }); +} + +const portFree = await checkPort(host, port); +if (!portFree) { + console.error( + `❌ MCP Inspector PORT IS IN USE at http://${host}:${port} ❌ `, + ); + console.error( + `💡 To fix: run "lsof -ti:${port} | xargs kill -9" to free the port, or set CLIENT_PORT to use a different port.`, + ); + process.exit(1); +} + const server = http.createServer((request, response) => { const handlerOptions = { public: distPath, @@ -40,8 +70,6 @@ const server = http.createServer((request, response) => { return handler(request, response, handlerOptions); }); -const port = parseInt(process.env.CLIENT_PORT || "6274", 10); -const host = process.env.HOST || "localhost"; server.on("listening", () => { const url = process.env.INSPECTOR_URL || `http://${host}:${port}`; console.log(`\n🚀 MCP Inspector is up and running at:\n ${url}\n`); @@ -58,5 +86,20 @@ server.on("error", (err) => { } else { throw err; } + process.exit(1); }); + +// Graceful shutdown: properly close the HTTP server so the port is +// released immediately instead of lingering in CLOSE_WAIT state. +function shutdown() { + server.close(() => { + process.exit(0); + }); + // Force exit if close takes too long (e.g. hanging connections) + setTimeout(() => process.exit(0), 3000); +} + +process.on("SIGINT", shutdown); +process.on("SIGTERM", shutdown); + server.listen(port, host); diff --git a/server/src/index.ts b/server/src/index.ts index 4d1fffa29..2b0934e73 100644 --- a/server/src/index.ts +++ b/server/src/index.ts @@ -29,6 +29,7 @@ import rateLimit from "express-rate-limit"; import { findActualExecutable } from "spawn-rx"; import mcpProxy from "./mcpProxy.js"; import { randomUUID, randomBytes, timingSafeEqual } from "node:crypto"; +import net from "net"; import { fileURLToPath } from "url"; import { dirname, join } from "path"; import { readFileSync } from "fs"; @@ -822,6 +823,30 @@ const PORT = parseInt( ); const HOST = process.env.HOST || "localhost"; +// Check port availability before attempting to bind. +// Prevents confusing EADDRINUSE errors from stale processes. +function checkPort(targetHost: string, targetPort: number): Promise { + return new Promise((resolve) => { + const tester = net.createServer(); + tester.once("error", () => { + resolve(false); + }); + tester.once("listening", () => { + tester.close(() => resolve(true)); + }); + tester.listen(targetPort, targetHost); + }); +} + +const portFree = await checkPort(HOST, PORT); +if (!portFree) { + console.error(`❌ Proxy Server PORT IS IN USE at port ${PORT} ❌ `); + console.error( + `💡 To fix: run "lsof -ti:${PORT} | xargs kill -9" to free the port, or set SERVER_PORT to use a different port.`, + ); + process.exit(1); +} + const server = app.listen(PORT, HOST); server.on("listening", () => { console.log(`⚙️ Proxy server listening on ${HOST}:${PORT}`); @@ -844,3 +869,15 @@ server.on("error", (err) => { } process.exit(1); }); + +// Graceful shutdown: properly close the HTTP server so the port is +// released immediately instead of lingering in CLOSE_WAIT state. +function shutdown() { + server.close(() => { + process.exit(0); + }); + setTimeout(() => process.exit(0), 3000); +} + +process.on("SIGINT", shutdown); +process.on("SIGTERM", shutdown);