feat: Enable auto-open with authentication and unify start scripts

- Unified start.js to handle both dev and production modes with --dev flag
- Generate session token in parent process, pass via MCP_PROXY_TOKEN env var
- Enable browser auto-open even when authentication is enabled by including token in URL
- Auto-reloads now work seamlessly with persistent tokens across hot reloads
- Simplified npm scripts - dev and dev:windows now use the same unified script
- Better developer experience with consistent token handling

The token is generated once per session and remains stable through server/client
reloads, making development smoother while maintaining security.

🤖 Generated with [Claude Code](https://claude.ai/code)

Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
Felix Weinberger 2025-06-16 13:42:37 +01:00
commit d06f3b5ebc
No known key found for this signature in database
3 changed files with 154 additions and 46 deletions

View file

@ -2,8 +2,9 @@
import open from "open"; import open from "open";
import { resolve, dirname } from "path"; import { resolve, dirname } from "path";
import { spawnPromise } from "spawn-rx"; import { spawnPromise, spawn } from "spawn-rx";
import { fileURLToPath } from "url"; import { fileURLToPath } from "url";
import { randomBytes } from "crypto";
const __dirname = dirname(fileURLToPath(import.meta.url)); const __dirname = dirname(fileURLToPath(import.meta.url));
@ -18,6 +19,7 @@ async function main() {
const mcpServerArgs = []; const mcpServerArgs = [];
let command = null; let command = null;
let parsingFlags = true; let parsingFlags = true;
let isDev = false;
for (let i = 0; i < args.length; i++) { for (let i = 0; i < args.length; i++) {
const arg = args[i]; const arg = args[i];
@ -27,6 +29,11 @@ async function main() {
continue; continue;
} }
if (parsingFlags && arg === "--dev") {
isDev = true;
continue;
}
if (parsingFlags && arg === "-e" && i + 1 < args.length) { if (parsingFlags && arg === "-e" && i + 1 < args.length) {
const envVar = args[++i]; const envVar = args[++i];
const equalsIndex = envVar.indexOf("="); const equalsIndex = envVar.indexOf("=");
@ -38,34 +45,25 @@ async function main() {
} else { } else {
envVars[envVar] = ""; envVars[envVar] = "";
} }
} else if (!command) { } else if (!command && !isDev) {
command = arg; command = arg;
} else { } else if (!isDev) {
mcpServerArgs.push(arg); mcpServerArgs.push(arg);
} }
} }
const inspectorServerPath = resolve(
__dirname,
"../..",
"server",
"build",
"index.js",
);
// Path to the client entry point
const inspectorClientPath = resolve(
__dirname,
"../..",
"client",
"bin",
"client.js",
);
const CLIENT_PORT = process.env.CLIENT_PORT ?? "6274"; const CLIENT_PORT = process.env.CLIENT_PORT ?? "6274";
const SERVER_PORT = process.env.SERVER_PORT ?? "6277"; const SERVER_PORT = process.env.SERVER_PORT ?? "6277";
console.log("Starting MCP inspector..."); console.log(
isDev
? "Starting MCP inspector in development mode..."
: "Starting MCP inspector...",
);
// Generate session token for authentication
const sessionToken = randomBytes(32).toString("hex");
const authDisabled = !!process.env.DANGEROUSLY_OMIT_AUTH;
const abort = new AbortController(); const abort = new AbortController();
@ -74,42 +72,150 @@ async function main() {
cancelled = true; cancelled = true;
abort.abort(); abort.abort();
}); });
let server, serverOk; let server, serverOk;
try { try {
server = spawnPromise( if (isDev) {
"node", // Development mode - use tsx watch
[ const serverCommand = "npx";
inspectorServerPath, const serverArgs = [
...(command ? [`--env`, command] : []), "tsx",
...(mcpServerArgs ? [`--args=${mcpServerArgs.join(" ")}`] : []), "watch",
], "--clear-screen=false",
{ "src/index.ts",
];
const isWindows = process.platform === "win32";
const serverOptions = {
cwd: resolve(__dirname, "../..", "server"),
env: { env: {
...process.env, ...process.env,
PORT: SERVER_PORT, PORT: SERVER_PORT,
CLIENT_PORT: CLIENT_PORT,
MCP_PROXY_TOKEN: sessionToken,
MCP_ENV_VARS: JSON.stringify(envVars), MCP_ENV_VARS: JSON.stringify(envVars),
}, },
signal: abort.signal, signal: abort.signal,
echoOutput: true, echoOutput: true,
}, };
);
// Make sure server started before starting client // For Windows, we need to use stdin: 'ignore' to simulate < NUL
serverOk = await Promise.race([server, delay(2 * 1000)]); if (isWindows) {
serverOptions.stdin = "ignore";
}
server = spawn(serverCommand, serverArgs, serverOptions);
// Give server time to start
serverOk = await Promise.race([
new Promise((resolve) => {
server.subscribe({
complete: () => resolve(false),
error: () => resolve(false),
next: () => {}, // We're using echoOutput
});
}),
delay(3000).then(() => true),
]);
} else {
// Production mode - use built files
const inspectorServerPath = resolve(
__dirname,
"../..",
"server",
"build",
"index.js",
);
server = spawnPromise(
"node",
[
inspectorServerPath,
...(command ? [`--env`, command] : []),
...(mcpServerArgs ? [`--args=${mcpServerArgs.join(" ")}`] : []),
],
{
env: {
...process.env,
PORT: SERVER_PORT,
CLIENT_PORT: CLIENT_PORT,
MCP_PROXY_TOKEN: sessionToken,
MCP_ENV_VARS: JSON.stringify(envVars),
},
signal: abort.signal,
echoOutput: true,
},
);
// Make sure server started before starting client
serverOk = await Promise.race([server, delay(2 * 1000)]);
}
} catch (error) {} } catch (error) {}
if (serverOk) { if (serverOk) {
try { try {
// Only auto-open when auth is disabled if (isDev) {
const authDisabled = !!process.env.DANGEROUSLY_OMIT_AUTH; // Development mode - use vite
if (process.env.MCP_AUTO_OPEN_ENABLED !== "false" && authDisabled) { const clientCommand = "npx";
open(`http://127.0.0.1:${CLIENT_PORT}`); const clientArgs = ["vite", "--port", CLIENT_PORT];
const client = spawn(clientCommand, clientArgs, {
cwd: resolve(__dirname, ".."),
env: { ...process.env, PORT: CLIENT_PORT },
signal: abort.signal,
echoOutput: true,
});
// Auto-open browser after vite starts
if (process.env.MCP_AUTO_OPEN_ENABLED !== "false") {
const url = authDisabled
? `http://127.0.0.1:${CLIENT_PORT}`
: `http://127.0.0.1:${CLIENT_PORT}/?MCP_PROXY_AUTH_TOKEN=${sessionToken}`;
// Give vite time to start before opening browser
setTimeout(() => {
open(url);
console.log(`\n🔗 Opening browser at: ${url}\n`);
}, 3000);
}
await new Promise((resolve) => {
client.subscribe({
complete: resolve,
error: (err) => {
if (!cancelled || process.env.DEBUG) {
console.error("Client error:", err);
}
resolve(null);
},
next: () => {}, // We're using echoOutput
});
});
} else {
// Production mode - use client.js
const inspectorClientPath = resolve(
__dirname,
"../..",
"client",
"bin",
"client.js",
);
// Auto-open browser with token
if (process.env.MCP_AUTO_OPEN_ENABLED !== "false") {
const url = authDisabled
? `http://127.0.0.1:${CLIENT_PORT}`
: `http://127.0.0.1:${CLIENT_PORT}/?MCP_PROXY_AUTH_TOKEN=${sessionToken}`;
open(url);
}
await spawnPromise("node", [inspectorClientPath], {
env: { ...process.env, PORT: CLIENT_PORT },
signal: abort.signal,
echoOutput: true,
});
} }
await spawnPromise("node", [inspectorClientPath], {
env: { ...process.env, PORT: CLIENT_PORT },
signal: abort.signal,
echoOutput: true,
});
} catch (e) { } catch (e) {
if (!cancelled || process.env.DEBUG) throw e; if (!cancelled || process.env.DEBUG) throw e;
} }

View file

@ -27,8 +27,8 @@
"build-client": "cd client && npm run build", "build-client": "cd client && npm run build",
"build-cli": "cd cli && npm run build", "build-cli": "cd cli && npm run build",
"clean": "rimraf ./node_modules ./client/node_modules ./cli/node_modules ./build ./client/dist ./server/build ./cli/build ./package-lock.json && npm install", "clean": "rimraf ./node_modules ./client/node_modules ./cli/node_modules ./build ./client/dist ./server/build ./cli/build ./package-lock.json && npm install",
"dev": "concurrently \"cd client && npm run dev\" \"cd server && npm run dev\"", "dev": "node client/bin/start.js --dev",
"dev:windows": "concurrently \"cd client && npm run dev\" \"cd server && npm run dev:windows\"", "dev:windows": "node client/bin/start.js --dev",
"start": "node client/bin/start.js", "start": "node client/bin/start.js",
"start-server": "cd server && npm run start", "start-server": "cd server && npm run start",
"start-client": "cd client && npm run preview", "start-client": "cd client && npm run preview",

View file

@ -89,7 +89,9 @@ app.use((req, res, next) => {
const webAppTransports: Map<string, Transport> = new Map<string, Transport>(); // Web app transports by web app sessionId const webAppTransports: Map<string, Transport> = new Map<string, Transport>(); // Web app transports by web app sessionId
const serverTransports: Map<string, Transport> = new Map<string, Transport>(); // Server Transports by web app sessionId const serverTransports: Map<string, Transport> = new Map<string, Transport>(); // Server Transports by web app sessionId
const sessionToken = randomBytes(32).toString("hex"); // Use provided token from environment or generate a new one
const sessionToken =
process.env.MCP_PROXY_TOKEN || randomBytes(32).toString("hex");
const authDisabled = !!process.env.DANGEROUSLY_OMIT_AUTH; const authDisabled = !!process.env.DANGEROUSLY_OMIT_AUTH;
// Origin validation middleware to prevent DNS rebinding attacks // Origin validation middleware to prevent DNS rebinding attacks
@ -544,7 +546,7 @@ server.on("listening", () => {
const clientPort = process.env.CLIENT_PORT || "6274"; const clientPort = process.env.CLIENT_PORT || "6274";
const clientUrl = `http://localhost:${clientPort}/?MCP_PROXY_AUTH_TOKEN=${sessionToken}`; const clientUrl = `http://localhost:${clientPort}/?MCP_PROXY_AUTH_TOKEN=${sessionToken}`;
console.log( console.log(
`\n🔗 Open inspector with token pre-filled:\n ${clientUrl}\n (Auto-open is disabled when authentication is enabled)\n`, `\n🔗 Open inspector with token pre-filled:\n ${clientUrl}\n`,
); );
} else { } else {
console.log( console.log(