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:
parent
21b85cdc40
commit
d06f3b5ebc
3 changed files with 154 additions and 46 deletions
|
|
@ -2,8 +2,9 @@
|
|||
|
||||
import open from "open";
|
||||
import { resolve, dirname } from "path";
|
||||
import { spawnPromise } from "spawn-rx";
|
||||
import { spawnPromise, spawn } from "spawn-rx";
|
||||
import { fileURLToPath } from "url";
|
||||
import { randomBytes } from "crypto";
|
||||
|
||||
const __dirname = dirname(fileURLToPath(import.meta.url));
|
||||
|
||||
|
|
@ -18,6 +19,7 @@ async function main() {
|
|||
const mcpServerArgs = [];
|
||||
let command = null;
|
||||
let parsingFlags = true;
|
||||
let isDev = false;
|
||||
|
||||
for (let i = 0; i < args.length; i++) {
|
||||
const arg = args[i];
|
||||
|
|
@ -27,6 +29,11 @@ async function main() {
|
|||
continue;
|
||||
}
|
||||
|
||||
if (parsingFlags && arg === "--dev") {
|
||||
isDev = true;
|
||||
continue;
|
||||
}
|
||||
|
||||
if (parsingFlags && arg === "-e" && i + 1 < args.length) {
|
||||
const envVar = args[++i];
|
||||
const equalsIndex = envVar.indexOf("=");
|
||||
|
|
@ -38,34 +45,25 @@ async function main() {
|
|||
} else {
|
||||
envVars[envVar] = "";
|
||||
}
|
||||
} else if (!command) {
|
||||
} else if (!command && !isDev) {
|
||||
command = arg;
|
||||
} else {
|
||||
} else if (!isDev) {
|
||||
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 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();
|
||||
|
||||
|
|
@ -74,42 +72,150 @@ async function main() {
|
|||
cancelled = true;
|
||||
abort.abort();
|
||||
});
|
||||
|
||||
let server, serverOk;
|
||||
|
||||
try {
|
||||
server = spawnPromise(
|
||||
"node",
|
||||
[
|
||||
inspectorServerPath,
|
||||
...(command ? [`--env`, command] : []),
|
||||
...(mcpServerArgs ? [`--args=${mcpServerArgs.join(" ")}`] : []),
|
||||
],
|
||||
{
|
||||
if (isDev) {
|
||||
// Development mode - use tsx watch
|
||||
const serverCommand = "npx";
|
||||
const serverArgs = [
|
||||
"tsx",
|
||||
"watch",
|
||||
"--clear-screen=false",
|
||||
"src/index.ts",
|
||||
];
|
||||
const isWindows = process.platform === "win32";
|
||||
|
||||
const serverOptions = {
|
||||
cwd: resolve(__dirname, "../..", "server"),
|
||||
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)]);
|
||||
// For Windows, we need to use stdin: 'ignore' to simulate < NUL
|
||||
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) {}
|
||||
|
||||
if (serverOk) {
|
||||
try {
|
||||
// Only auto-open when auth is disabled
|
||||
const authDisabled = !!process.env.DANGEROUSLY_OMIT_AUTH;
|
||||
if (process.env.MCP_AUTO_OPEN_ENABLED !== "false" && authDisabled) {
|
||||
open(`http://127.0.0.1:${CLIENT_PORT}`);
|
||||
if (isDev) {
|
||||
// Development mode - use vite
|
||||
const clientCommand = "npx";
|
||||
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) {
|
||||
if (!cancelled || process.env.DEBUG) throw e;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -27,8 +27,8 @@
|
|||
"build-client": "cd client && 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",
|
||||
"dev": "concurrently \"cd client && npm run dev\" \"cd server && npm run dev\"",
|
||||
"dev:windows": "concurrently \"cd client && npm run dev\" \"cd server && npm run dev:windows\"",
|
||||
"dev": "node client/bin/start.js --dev",
|
||||
"dev:windows": "node client/bin/start.js --dev",
|
||||
"start": "node client/bin/start.js",
|
||||
"start-server": "cd server && npm run start",
|
||||
"start-client": "cd client && npm run preview",
|
||||
|
|
|
|||
|
|
@ -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 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;
|
||||
|
||||
// Origin validation middleware to prevent DNS rebinding attacks
|
||||
|
|
@ -544,7 +546,7 @@ server.on("listening", () => {
|
|||
const clientPort = process.env.CLIENT_PORT || "6274";
|
||||
const clientUrl = `http://localhost:${clientPort}/?MCP_PROXY_AUTH_TOKEN=${sessionToken}`;
|
||||
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 {
|
||||
console.log(
|
||||
|
|
|
|||
Loading…
Reference in a new issue