Skip to content
Open
Show file tree
Hide file tree
Changes from 2 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
120 changes: 90 additions & 30 deletions mcpjam-inspector/bin/start.js
Original file line number Diff line number Diff line change
Expand Up @@ -120,23 +120,88 @@ function delay(ms) {
return new Promise((resolve) => setTimeout(resolve, ms, true));
}

function isPortAvailable(port) {
function isPortAvailable(port, host = "127.0.0.1") {
return new Promise((resolve) => {
const server = createServer();
let settled = false;
const timeout = setTimeout(() => {
if (settled) {
return;
}
settled = true;
cleanup();
server.close();
resolve(false);
}, 1000);

const cleanup = () => {
clearTimeout(timeout);
server.removeListener("error", onError);
server.removeListener("listening", onListening);
};

server.listen(port, "127.0.0.1", () => {
const onListening = () => {
if (settled) {
return;
}
settled = true;
cleanup();
server.close(() => {
resolve(true);
});
});
};

server.on("error", () => {
// Port is not available
const onError = () => {
if (settled) {
return;
}
settled = true;
cleanup();
resolve(false);
});
};

server.once("error", onError);
server.once("listening", onListening);
server.listen(port, host);
});
}

function parsePort(value) {
const parsed = Number.parseInt(value, 10);
if (!Number.isFinite(parsed) || !Number.isInteger(parsed) || parsed <= 0 || parsed > 65535) {
throw new Error(`Invalid port value: ${value}`);
}
return parsed;
}

async function findAvailablePort(
startPort,
host,
maxPortOffset = 100,
verbose = false,
) {
const maxPort = Math.min(startPort + maxPortOffset, 65535);

if (maxPort <= startPort) {
throw new Error(
`No available port found in range ${startPort}-${maxPort}`,
);
}

for (let port = startPort; port <= maxPort; port++) {
const isAvailable = await isPortAvailable(port, host);
if (isAvailable) {
return port;
}

if (verbose) {
logWarning(`Port ${port} unavailable; checking next port`);
}
}

throw new Error(`No available port found in range ${startPort}-${maxPort}`);
}

function spawnPromise(command, args, options) {
return new Promise((resolve, reject) => {
const child = spawn(command, args, {
Expand Down Expand Up @@ -357,11 +422,6 @@ async function main() {
if (parsingFlags && arg === "--port" && i + 1 < args.length) {
const port = args[++i];
envVars.PORT = port;
// Default: localhost in development, 127.0.0.1 in production
const defaultHost =
process.env.ENVIRONMENT === "dev" ? "localhost" : "127.0.0.1";
const baseHost = process.env.HOST || defaultHost;
envVars.BASE_URL = `http://${baseHost}:${port}`;
continue;
}

Expand Down Expand Up @@ -623,17 +683,26 @@ async function main() {
// Apply parsed environment variables to process.env first
Object.assign(process.env, envVars);

// Port configuration (fixed default to 6274)
const requestedPort = 6274;
const requestedPortCandidate =
process.env.SERVER_PORT !== undefined
? process.env.SERVER_PORT
: envVars.PORT;
const requestedPort = requestedPortCandidate
? parsePort(requestedPortCandidate)
: 6274;
let PORT;
const defaultHost =
process.env.ENVIRONMENT === "dev" ? "localhost" : "127.0.0.1";
const baseHost = process.env.HOST || defaultHost;
const host = baseHost;

try {
// Check if user explicitly set a port via --port flag
const hasExplicitPort = envVars.PORT !== undefined;
const hasExplicitPort = requestedPortCandidate !== undefined;
Comment on lines +695 to +710
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

Truthiness vs. identity: "0" and "" silently become port 6274 yet are treated as explicit.

Line 699 uses a truthiness test — falsy strings like "0" or "" fall through to the default 6274. But line 710 uses !== undefined, so those same values are considered explicitly requested. The result: --port 0 or SERVER_PORT="" silently checks 6274 as if the user asked for it, yielding a confusing error ("Explicitly requested port 6274 is not available") or quiet success on a port the user never intended.

Align the two checks so parsePort is the single arbiter of validity:

Proposed fix
   const requestedPortCandidate =
     process.env.SERVER_PORT !== undefined
       ? process.env.SERVER_PORT
       : envVars.PORT;
-  const requestedPort = requestedPortCandidate
-    ? parsePort(requestedPortCandidate)
-    : 6274;
+  const requestedPort =
+    requestedPortCandidate !== undefined && requestedPortCandidate !== ""
+      ? parsePort(requestedPortCandidate)
+      : 6274;
   let PORT;
   const defaultHost =
     process.env.ENVIRONMENT === "dev" ? "localhost" : "127.0.0.1";
   const baseHost = process.env.HOST || defaultHost;
   const host = baseHost;
 
   try {
     // Check if user explicitly set a port via --port flag
-    const hasExplicitPort = requestedPortCandidate !== undefined;
+    const hasExplicitPort =
+      requestedPortCandidate !== undefined && requestedPortCandidate !== "";

This way --port 0 produces the clear "Invalid port value: 0" error from parsePort, and an empty SERVER_PORT="" falls through to the default scan — both matching user intent.

🤖 Prompt for AI Agents
In `@mcpjam-inspector/bin/start.js` around lines 695 - 710, The code treats falsy
strings like "" or "0" inconsistently: requestedPort is computed via
parsePort(requestedPortCandidate) but hasExplicitPort is based on
requestedPortCandidate !== undefined; change hasExplicitPort to rely on the
parse result so parsePort is the single arbiter. Concretely, after computing
requestedPort (via parsePort(requestedPortCandidate)), set hasExplicitPort =
requestedPort !== undefined (or !== null depending on parsePort) instead of
checking requestedPortCandidate, so invalid values cause parsePort errors and
empty strings fall through to the default behavior.


if (hasExplicitPort) {
if (await isPortAvailable(requestedPort)) {
PORT = requestedPort.toString();
if (await isPortAvailable(requestedPort, host)) {
PORT = String(requestedPort);
} else {
logError(`Explicitly requested port ${requestedPort} is not available`);
logInfo(
Expand All @@ -642,23 +711,17 @@ async function main() {
throw new Error(`Port ${requestedPort} is already in use`);
}
} else {
// Fixed port policy: use default port 6274 and fail fast if unavailable
if (await isPortAvailable(requestedPort)) {
PORT = requestedPort.toString();
} else {
logError(
`Default port ${requestedPort} is already in use. Please free the port`,
const resolvedPort = await findAvailablePort(requestedPort, host, 100, true);
if (resolvedPort !== requestedPort) {
logInfo(
`Default port ${requestedPort} is busy. Using next available port ${resolvedPort}.`,
);
throw new Error(`Port ${requestedPort} is already in use`);
}
PORT = String(resolvedPort);
}

// Update environment variables with the final port
envVars.PORT = PORT;
// Default: localhost in development, 127.0.0.1 in production
const defaultHost =
process.env.ENVIRONMENT === "dev" ? "localhost" : "127.0.0.1";
const baseHost = process.env.HOST || defaultHost;
envVars.BASE_URL = `http://${baseHost}:${PORT}`;
Object.assign(process.env, envVars);
} catch (error) {
Expand Down Expand Up @@ -744,9 +807,6 @@ async function main() {
// Open the browser automatically
// Use BASE_URL if set, otherwise construct from HOST and PORT
// Default: localhost in development, 127.0.0.1 in production
const defaultHost =
process.env.ENVIRONMENT === "dev" ? "localhost" : "127.0.0.1";
const host = process.env.HOST || defaultHost;
let url = process.env.BASE_URL || `http://${host}:${PORT}`;

// Append initial tab hash if specified
Expand Down
148 changes: 138 additions & 10 deletions mcpjam-inspector/src/main.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,8 +9,8 @@ Sentry.init({

import { app, BrowserWindow, shell, Menu } from "electron";
import { serve } from "@hono/node-server";
import { createServer } from "node:net";
import path from "path";
import { createHonoApp } from "../server/app.js";
import log from "electron-log";
import { updateElectronApp } from "update-electron-app";
import { registerListeners } from "./ipc/listeners-register.js";
Expand Down Expand Up @@ -41,10 +41,137 @@ let server: any = null;
let serverPort: number = 0;

const isDev = process.env.NODE_ENV === "development";
const DEFAULT_ELECTRON_PORT = 6274;
const PORT_SCAN_LIMIT = 100;
const MAX_PORT_NUMBER = 65535;

type PortParseResult = {
value: number;
isExplicit: boolean;
};

function parsePort(value: string | undefined, fallback: number): PortParseResult {
if (value === undefined || value.trim() === "") {
return { value: fallback, isExplicit: false };
}

const parsed = Number.parseInt(value, 10);
if (!Number.isFinite(parsed) || !Number.isInteger(parsed) || parsed <= 0 || parsed > MAX_PORT_NUMBER) {
log.warn(`Ignoring invalid port value "${value}", using fallback ${fallback}`);
return { value: fallback, isExplicit: false };
}

return { value: parsed, isExplicit: true };
}

function getRequestedPort(): {
port: number;
hasExplicitPort: boolean;
} {
const explicitPort =
process.env.ELECTRON_PORT ??
process.env.SERVER_PORT ??
process.env.PORT;
const parsedPort = parsePort(explicitPort, DEFAULT_ELECTRON_PORT);

return {
port: parsedPort.value,
hasExplicitPort: parsedPort.isExplicit,
};
}

function isPortAvailable(port: number, host: string): Promise<boolean> {
return new Promise((resolve) => {
const server = createServer();
let settled = false;
const timeout = setTimeout(() => {
if (settled) {
return;
}
settled = true;
cleanup();
server.close();
resolve(false);
}, 1000);

const cleanup = () => {
clearTimeout(timeout);
server.removeListener("error", onError);
server.removeListener("listening", onListening);
};

const onListening = () => {
if (settled) {
return;
}
settled = true;
cleanup();
server.close(() => {
resolve(true);
});
};

const onError = () => {
if (settled) {
return;
}
settled = true;
cleanup();
resolve(false);
};

server.once("error", onError);
server.once("listening", onListening);
server.listen(port, host);
});
}

async function findAvailablePort(
requestedPort: number,
host: string,
hasExplicitPort = false,
): Promise<number> {
if (requestedPort < 1 || requestedPort > MAX_PORT_NUMBER) {
throw new Error(`Requested port ${requestedPort} is outside valid range 1-${MAX_PORT_NUMBER}`);
}

if (await isPortAvailable(requestedPort, host)) {
return requestedPort;
}

if (hasExplicitPort) {
throw new Error(`Requested port ${requestedPort} is already in use`);
}

const maxPort = Math.min(requestedPort + PORT_SCAN_LIMIT, MAX_PORT_NUMBER);
if (maxPort <= requestedPort) {
throw new Error(`No available port found in range ${requestedPort}-${maxPort}`);
}

for (let port = requestedPort + 1; port <= maxPort; port++) {
if (await isPortAvailable(port, host)) {
log.warn(
`Port ${requestedPort} was unavailable. Using fallback free port ${port}`,
);
return port;
}
}

throw new Error(
`No available port found in range ${requestedPort}-${maxPort}`,
);
}

function getServerUrl(port: number, host: string): string {
return `http://${host}:${port}`;
}

async function startHonoServer(): Promise<number> {
try {
const port = 6274;
const hostname = app.isPackaged ? "127.0.0.1" : "localhost";
const { port: requestedPort, hasExplicitPort } = getRequestedPort();
const port = await findAvailablePort(requestedPort, hostname, hasExplicitPort);

// Set environment variables to tell the server it's running in Electron
process.env.ELECTRON_APP = "true";
process.env.IS_PACKAGED = app.isPackaged ? "true" : "false";
Expand All @@ -53,19 +180,18 @@ async function startHonoServer(): Promise<number> {
? process.resourcesPath
: app.getAppPath();
process.env.NODE_ENV = app.isPackaged ? "production" : "development";
process.env.SERVER_PORT = String(port);

const { createHonoApp } = await import("../server/app.js");
const honoApp = createHonoApp();

// Bind to 127.0.0.1 when packaged to avoid IPv6-only localhost issues
const hostname = app.isPackaged ? "127.0.0.1" : "localhost";

server = serve({
fetch: honoApp.fetch,
port,
hostname,
});

log.info(`🚀 MCPJam Server started on port ${port}`);
log.info(`🚀 MCPJam Server started on port ${port} (${hostname})`);
return port;
} catch (error) {
log.error("Failed to start Hono server:", error);
Expand Down Expand Up @@ -201,8 +327,9 @@ function createAppMenu(): void {
app.whenReady().then(async () => {
try {
// Start the embedded Hono server
const serverHost = app.isPackaged ? "127.0.0.1" : "localhost";
serverPort = await startHonoServer();
const serverUrl = `http://127.0.0.1:${serverPort}`;
const serverUrl = getServerUrl(serverPort, serverHost);

// Create the main window
createAppMenu();
Expand Down Expand Up @@ -237,14 +364,15 @@ app.on("window-all-closed", () => {
app.on("activate", async () => {
// On macOS, re-create window when the dock icon is clicked
if (BrowserWindow.getAllWindows().length === 0) {
const serverHost = app.isPackaged ? "127.0.0.1" : "localhost";
if (serverPort > 0) {
const serverUrl = `http://127.0.0.1:${serverPort}`;
const serverUrl = getServerUrl(serverPort, serverHost);
mainWindow = createMainWindow(serverUrl);
} else {
// Restart server if needed
try {
serverPort = await startHonoServer();
const serverUrl = `http://127.0.0.1:${serverPort}`;
const serverUrl = getServerUrl(serverPort, serverHost);
mainWindow = createMainWindow(serverUrl);
} catch (error) {
log.error("Failed to restart server:", error);
Expand All @@ -270,7 +398,7 @@ app.on("open-url", (event, url) => {
// Compute the base URL the renderer should load
const baseUrl = isDev
? MAIN_WINDOW_VITE_DEV_SERVER_URL
: `http://127.0.0.1:${serverPort}`;
: getServerUrl(serverPort, app.isPackaged ? "127.0.0.1" : "localhost");

const callbackUrl = new URL("/callback", baseUrl);
if (code) callbackUrl.searchParams.set("code", code);
Expand Down