Merge pull request #513 from modelcontextprotocol/fweinberger/auto-open
Re-enable inspector auto-open & reloads post security fixes
This commit is contained in:
commit
a7336cd00c
4 changed files with 213 additions and 63 deletions
18
README.md
18
README.md
|
|
@ -148,9 +148,9 @@ The MCP Inspector proxy server requires authentication by default. When starting
|
||||||
http://localhost:6274/?MCP_PROXY_AUTH_TOKEN=3a1c267fad21f7150b7d624c160b7f09b0b8c4f623c7107bbf13378f051538d4
|
http://localhost:6274/?MCP_PROXY_AUTH_TOKEN=3a1c267fad21f7150b7d624c160b7f09b0b8c4f623c7107bbf13378f051538d4
|
||||||
```
|
```
|
||||||
|
|
||||||
This token must be included as a Bearer token in the Authorization header for all requests to the server. When authentication is enabled, auto-open is disabled by default to ensure you use the secure URL.
|
This token must be included as a Bearer token in the Authorization header for all requests to the server. The inspector will automatically open your browser with the token pre-filled in the URL.
|
||||||
|
|
||||||
**Recommended: Use the pre-filled URL** - Click or copy the link shown in the console to open the inspector with the token already configured.
|
**Automatic browser opening** - The inspector now automatically opens your browser with the token pre-filled in the URL when authentication is enabled.
|
||||||
|
|
||||||
**Alternative: Manual configuration** - If you already have the inspector open:
|
**Alternative: Manual configuration** - If you already have the inspector open:
|
||||||
|
|
||||||
|
|
@ -188,13 +188,13 @@ ALLOWED_ORIGINS=http://localhost:6274,http://127.0.0.1:6274,http://localhost:800
|
||||||
|
|
||||||
The MCP Inspector supports the following configuration settings. To change them, click on the `Configuration` button in the MCP Inspector UI:
|
The MCP Inspector supports the following configuration settings. To change them, click on the `Configuration` button in the MCP Inspector UI:
|
||||||
|
|
||||||
| Setting | Description | Default |
|
| Setting | Description | Default |
|
||||||
| --------------------------------------- | ------------------------------------------------------------------------------------------------------------- | ------- |
|
| --------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------- | ------- |
|
||||||
| `MCP_SERVER_REQUEST_TIMEOUT` | Timeout for requests to the MCP server (ms) | 10000 |
|
| `MCP_SERVER_REQUEST_TIMEOUT` | Timeout for requests to the MCP server (ms) | 10000 |
|
||||||
| `MCP_REQUEST_TIMEOUT_RESET_ON_PROGRESS` | Reset timeout on progress notifications | true |
|
| `MCP_REQUEST_TIMEOUT_RESET_ON_PROGRESS` | Reset timeout on progress notifications | true |
|
||||||
| `MCP_REQUEST_MAX_TOTAL_TIMEOUT` | Maximum total timeout for requests sent to the MCP server (ms) (Use with progress notifications) | 60000 |
|
| `MCP_REQUEST_MAX_TOTAL_TIMEOUT` | Maximum total timeout for requests sent to the MCP server (ms) (Use with progress notifications) | 60000 |
|
||||||
| `MCP_PROXY_FULL_ADDRESS` | Set this if you are running the MCP Inspector Proxy on a non-default address. Example: http://10.1.1.22:5577 | "" |
|
| `MCP_PROXY_FULL_ADDRESS` | Set this if you are running the MCP Inspector Proxy on a non-default address. Example: http://10.1.1.22:5577 | "" |
|
||||||
| `MCP_AUTO_OPEN_ENABLED` | Enable automatic browser opening when inspector starts. Only as environment var, not configurable in browser. | true |
|
| `MCP_AUTO_OPEN_ENABLED` | Enable automatic browser opening when inspector starts (works with authentication enabled). Only as environment var, not configurable in browser. | true |
|
||||||
|
|
||||||
These settings can be adjusted in real-time through the UI and will persist across sessions.
|
These settings can be adjusted in real-time through the UI and will persist across sessions.
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -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));
|
||||||
|
|
||||||
|
|
@ -11,6 +12,157 @@ function delay(ms) {
|
||||||
return new Promise((resolve) => setTimeout(resolve, ms, true));
|
return new Promise((resolve) => setTimeout(resolve, ms, true));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async function startDevServer(serverOptions) {
|
||||||
|
const { SERVER_PORT, CLIENT_PORT, sessionToken, envVars, abort } =
|
||||||
|
serverOptions;
|
||||||
|
const serverCommand = "npx";
|
||||||
|
const serverArgs = ["tsx", "watch", "--clear-screen=false", "src/index.ts"];
|
||||||
|
const isWindows = process.platform === "win32";
|
||||||
|
|
||||||
|
const spawnOptions = {
|
||||||
|
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,
|
||||||
|
};
|
||||||
|
|
||||||
|
// For Windows, we need to use stdin: 'ignore' to simulate < NUL
|
||||||
|
if (isWindows) {
|
||||||
|
spawnOptions.stdin = "ignore";
|
||||||
|
}
|
||||||
|
|
||||||
|
const server = spawn(serverCommand, serverArgs, spawnOptions);
|
||||||
|
|
||||||
|
// Give server time to start
|
||||||
|
const serverOk = await Promise.race([
|
||||||
|
new Promise((resolve) => {
|
||||||
|
server.subscribe({
|
||||||
|
complete: () => resolve(false),
|
||||||
|
error: () => resolve(false),
|
||||||
|
next: () => {}, // We're using echoOutput
|
||||||
|
});
|
||||||
|
}),
|
||||||
|
delay(3000).then(() => true),
|
||||||
|
]);
|
||||||
|
|
||||||
|
return { server, serverOk };
|
||||||
|
}
|
||||||
|
|
||||||
|
async function startProdServer(serverOptions) {
|
||||||
|
const {
|
||||||
|
SERVER_PORT,
|
||||||
|
CLIENT_PORT,
|
||||||
|
sessionToken,
|
||||||
|
envVars,
|
||||||
|
abort,
|
||||||
|
command,
|
||||||
|
mcpServerArgs,
|
||||||
|
} = serverOptions;
|
||||||
|
const inspectorServerPath = resolve(
|
||||||
|
__dirname,
|
||||||
|
"../..",
|
||||||
|
"server",
|
||||||
|
"build",
|
||||||
|
"index.js",
|
||||||
|
);
|
||||||
|
|
||||||
|
const 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
|
||||||
|
const serverOk = await Promise.race([server, delay(2 * 1000)]);
|
||||||
|
|
||||||
|
return { server, serverOk };
|
||||||
|
}
|
||||||
|
|
||||||
|
async function startDevClient(clientOptions) {
|
||||||
|
const { CLIENT_PORT, authDisabled, sessionToken, abort, cancelled } =
|
||||||
|
clientOptions;
|
||||||
|
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
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
async function startProdClient(clientOptions) {
|
||||||
|
const { CLIENT_PORT, authDisabled, sessionToken, abort } = clientOptions;
|
||||||
|
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,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
async function main() {
|
async function main() {
|
||||||
// Parse command line arguments
|
// Parse command line arguments
|
||||||
const args = process.argv.slice(2);
|
const args = process.argv.slice(2);
|
||||||
|
|
@ -18,6 +170,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 +180,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 +196,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 +223,41 @@ async function main() {
|
||||||
cancelled = true;
|
cancelled = true;
|
||||||
abort.abort();
|
abort.abort();
|
||||||
});
|
});
|
||||||
let server, serverOk;
|
|
||||||
try {
|
|
||||||
server = spawnPromise(
|
|
||||||
"node",
|
|
||||||
[
|
|
||||||
inspectorServerPath,
|
|
||||||
...(command ? [`--env`, command] : []),
|
|
||||||
...(mcpServerArgs ? [`--args=${mcpServerArgs.join(" ")}`] : []),
|
|
||||||
],
|
|
||||||
{
|
|
||||||
env: {
|
|
||||||
...process.env,
|
|
||||||
PORT: SERVER_PORT,
|
|
||||||
MCP_ENV_VARS: JSON.stringify(envVars),
|
|
||||||
},
|
|
||||||
signal: abort.signal,
|
|
||||||
echoOutput: true,
|
|
||||||
},
|
|
||||||
);
|
|
||||||
|
|
||||||
// Make sure server started before starting client
|
let server, serverOk;
|
||||||
serverOk = await Promise.race([server, delay(2 * 1000)]);
|
|
||||||
|
try {
|
||||||
|
const serverOptions = {
|
||||||
|
SERVER_PORT,
|
||||||
|
CLIENT_PORT,
|
||||||
|
sessionToken,
|
||||||
|
envVars,
|
||||||
|
abort,
|
||||||
|
command,
|
||||||
|
mcpServerArgs,
|
||||||
|
};
|
||||||
|
|
||||||
|
const result = isDev
|
||||||
|
? await startDevServer(serverOptions)
|
||||||
|
: await startProdServer(serverOptions);
|
||||||
|
|
||||||
|
server = result.server;
|
||||||
|
serverOk = result.serverOk;
|
||||||
} catch (error) {}
|
} catch (error) {}
|
||||||
|
|
||||||
if (serverOk) {
|
if (serverOk) {
|
||||||
try {
|
try {
|
||||||
// Only auto-open when auth is disabled
|
const clientOptions = {
|
||||||
const authDisabled = !!process.env.DANGEROUSLY_OMIT_AUTH;
|
CLIENT_PORT,
|
||||||
if (process.env.MCP_AUTO_OPEN_ENABLED !== "false" && authDisabled) {
|
authDisabled,
|
||||||
open(`http://127.0.0.1:${CLIENT_PORT}`);
|
sessionToken,
|
||||||
}
|
abort,
|
||||||
await spawnPromise("node", [inspectorClientPath], {
|
cancelled,
|
||||||
env: { ...process.env, PORT: CLIENT_PORT },
|
};
|
||||||
signal: abort.signal,
|
|
||||||
echoOutput: true,
|
await (isDev
|
||||||
});
|
? startDevClient(clientOptions)
|
||||||
|
: startProdClient(clientOptions));
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
if (!cancelled || process.env.DEBUG) throw e;
|
if (!cancelled || process.env.DEBUG) throw e;
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -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",
|
||||||
|
|
|
||||||
|
|
@ -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(
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue