Skip to content
Open
Show file tree
Hide file tree
Changes from 1 commit
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
62 changes: 45 additions & 17 deletions mcpjam-inspector/bin/start.js
Original file line number Diff line number Diff line change
Expand Up @@ -120,11 +120,11 @@ 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();

server.listen(port, "127.0.0.1", () => {
server.listen(port, host, () => {
server.close(() => {
resolve(true);
});
Expand All @@ -137,6 +137,31 @@ function isPortAvailable(port) {
});
}

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, maxPortOffset = 100, verbose = false) {
const defaultHost = process.env.ENVIRONMENT === "dev" ? "localhost" : "127.0.0.1";

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

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

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

function spawnPromise(command, args, options) {
return new Promise((resolve, reject) => {
const child = spawn(command, args, {
Expand Down Expand Up @@ -623,17 +648,25 @@ 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;

try {
// Check if user explicitly set a port via --port flag
const hasExplicitPort = envVars.PORT !== undefined;
const hasExplicitPort = requestedPortCandidate !== undefined;

if (hasExplicitPort) {
if (await isPortAvailable(requestedPort)) {
PORT = requestedPort.toString();
if (await isPortAvailable(requestedPort, baseHost)) {
PORT = String(requestedPort);
} else {
logError(`Explicitly requested port ${requestedPort} is not available`);
logInfo(
Expand All @@ -642,23 +675,18 @@ 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, 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
98 changes: 93 additions & 5 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,99 @@ 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;

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 > 65535
) {
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();
server.listen(port, host, () => {
server.close(() => {
resolve(true);
});
});

server.on("error", () => {
resolve(false);
});
});
}

async function findAvailablePort(
requestedPort: number,
host: string,
hasExplicitPort = false,
): Promise<number> {
if (await isPortAvailable(requestedPort, host)) {
return requestedPort;
}

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

for (let port = requestedPort + 1; port <= requestedPort + PORT_SCAN_LIMIT; 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}-${
requestedPort + PORT_SCAN_LIMIT
}`,
);
}

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,12 +142,11 @@ 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,
Expand Down
Loading