Some checks failed
CLI Tests / test (push) Has been cancelled
* chore: v1 deprecation notices in all four packages, bump to 1.0.1 Phase 2 of the v2 go-live runbook (#1804 §4). v1 publishes four separately installable names, so each gets its own notice — a root-README-only change is invisible to anyone who installed a sub-package. `cli/` and `server/` ship no README at all, so for those two the runtime banner plus the forthcoming `npm deprecate` (#1816) are the only notice their users ever get. Notices, all using identical wording so the runtime banner and the npm warning agree: - root `README.md` and `client/README.md` - `cli/src/cli.ts` — stderr banner in `main()`. Also covers the root `mcp-inspector` bin, which is `cli/build/cli.js`. Deliberately **stderr**: `--cli` mode emits JSON on stdout and callers pipe it, so a banner there would corrupt output. Verified against a real server — stdout stays parseable JSON while the banner goes to stderr. - `server/src/index.ts` — alongside the existing proxy listening log - `client/bin/start.js` — alongside the existing startup URL log - `client/src/components/DeprecationBanner.tsx` — dismissible in-app banner, dismissal persisted in localStorage with both read and write guarded so a storage failure degrades to "banner reappears" rather than breaking the app AGENTS.md is replaced with a hard stop: v1 takes security fixes only, by issue only, and agents must never open a PR against v1/main. Version bumped to 1.0.1 across all four package.json files plus the lockfile (`npm run update-version`); `check-version` passes. The lockfile diff is restricted to the version fields — the `libc` churn a newer npm wanted to introduce was reverted as unrelated. `publish-all` is deliberately NOT changed here: 1.0.1 must take `latest`, which is the entire point of shipping this notice to npx users. Adding `--tag v1` is #1816, immediately after this publishes. Note for review: `client/src/App.tsx` shows ~836 changed lines but `git diff -w` shows 8 — wrapping the layout in a flex column added one nesting level and prettier re-indented the subtree. Review it with `-w`. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Txmv2qqv3yeKgRzoqXytzD * docs: replace the README screenshot with one showing the deprecation banner Captured from this branch's build: v1.0.1 in the sidebar heading, the dismissible deprecation banner across the top, connected to the everything server with tools listed. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Txmv2qqv3yeKgRzoqXytzD * docs: pin the README screenshot to v1/main instead of main The image URL must stay absolute — a relative path breaks on the npm package page, which renders the README outside any repo context. But it was pinned to `main`, so the v1 README rendered whatever `main` happened to hold: today the pre-deprecation screenshot, and after the v2 tree swap (#1817) either the v2 screenshot or a 404 if that path doesn't exist there. Pinning to `v1/main` keeps it absolute for npm, shows the deprecation banner once this merges, and survives the swap. Verified all candidate refs resolve 200 — GitHub disambiguates the slash in `v1/main` as the ref without needing a `refs/heads/` prefix. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Txmv2qqv3yeKgRzoqXytzD --------- Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
355 lines
8.2 KiB
JavaScript
Executable file
355 lines
8.2 KiB
JavaScript
Executable file
#!/usr/bin/env node
|
|
|
|
import open from "open";
|
|
import { resolve, dirname } from "path";
|
|
import { spawnPromise, spawn } from "spawn-rx";
|
|
import { fileURLToPath } from "url";
|
|
import { randomBytes } from "crypto";
|
|
|
|
const __dirname = dirname(fileURLToPath(import.meta.url));
|
|
const DEFAULT_MCP_PROXY_LISTEN_PORT = "6277";
|
|
|
|
function delay(ms) {
|
|
return new Promise((resolve) => setTimeout(resolve, ms, true));
|
|
}
|
|
|
|
function getClientUrl(port, authDisabled, sessionToken, serverPort) {
|
|
const host = process.env.HOST || "localhost";
|
|
const baseUrl = `http://${host}:${port}`;
|
|
|
|
const params = new URLSearchParams();
|
|
if (serverPort && serverPort !== DEFAULT_MCP_PROXY_LISTEN_PORT) {
|
|
params.set("MCP_PROXY_PORT", serverPort);
|
|
}
|
|
if (!authDisabled) {
|
|
params.set("MCP_PROXY_AUTH_TOKEN", sessionToken);
|
|
}
|
|
return params.size > 0 ? `${baseUrl}/?${params.toString()}` : baseUrl;
|
|
}
|
|
|
|
async function startDevServer(serverOptions) {
|
|
const {
|
|
SERVER_PORT,
|
|
CLIENT_PORT,
|
|
sessionToken,
|
|
envVars,
|
|
abort,
|
|
transport,
|
|
serverUrl,
|
|
} = 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,
|
|
SERVER_PORT,
|
|
CLIENT_PORT,
|
|
MCP_PROXY_AUTH_TOKEN: sessionToken,
|
|
MCP_ENV_VARS: JSON.stringify(envVars),
|
|
...(transport ? { MCP_TRANSPORT: transport } : {}),
|
|
...(serverUrl ? { MCP_SERVER_URL: serverUrl } : {}),
|
|
},
|
|
signal: abort.signal,
|
|
echoOutput: true,
|
|
};
|
|
|
|
// For Windows, we need to ignore stdin to simulate < NUL
|
|
// spawn-rx's 'stdin' option expects an Observable, not 'ignore'
|
|
// Use Node's stdio option instead
|
|
if (isWindows) {
|
|
spawnOptions.stdio = ["ignore", "pipe", "pipe"];
|
|
}
|
|
|
|
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,
|
|
transport,
|
|
serverUrl,
|
|
} = serverOptions;
|
|
const inspectorServerPath = resolve(
|
|
__dirname,
|
|
"../..",
|
|
"server",
|
|
"build",
|
|
"index.js",
|
|
);
|
|
|
|
const server = spawnPromise(
|
|
"node",
|
|
[
|
|
inspectorServerPath,
|
|
...(command ? [`--command=${command}`] : []),
|
|
...(mcpServerArgs && mcpServerArgs.length > 0
|
|
? [`--args=${mcpServerArgs.join(" ")}`]
|
|
: []),
|
|
...(transport ? [`--transport=${transport}`] : []),
|
|
...(serverUrl ? [`--server-url=${serverUrl}`] : []),
|
|
],
|
|
{
|
|
env: {
|
|
...process.env,
|
|
SERVER_PORT,
|
|
CLIENT_PORT,
|
|
MCP_PROXY_AUTH_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,
|
|
SERVER_PORT,
|
|
authDisabled,
|
|
sessionToken,
|
|
abort,
|
|
cancelled,
|
|
} = clientOptions;
|
|
const clientCommand = "npx";
|
|
const host = process.env.HOST || "localhost";
|
|
const clientArgs = ["vite", "--port", CLIENT_PORT, "--host", host];
|
|
const isWindows = process.platform === "win32";
|
|
|
|
const spawnOptions = {
|
|
cwd: resolve(__dirname, ".."),
|
|
env: { ...process.env, CLIENT_PORT },
|
|
signal: abort.signal,
|
|
echoOutput: true,
|
|
};
|
|
|
|
// For Windows, we need to ignore stdin to prevent hanging
|
|
if (isWindows) {
|
|
spawnOptions.stdio = ["ignore", "pipe", "pipe"];
|
|
}
|
|
|
|
const client = spawn(clientCommand, clientArgs, spawnOptions);
|
|
|
|
const url = getClientUrl(
|
|
CLIENT_PORT,
|
|
authDisabled,
|
|
sessionToken,
|
|
SERVER_PORT,
|
|
);
|
|
|
|
// Give vite time to start before opening or logging the URL
|
|
setTimeout(() => {
|
|
console.log(
|
|
`\n⚠️ @modelcontextprotocol/inspector v1 is deprecated.\n` +
|
|
` Upgrade: npx @modelcontextprotocol/inspector@latest\n` +
|
|
` v1 receives security fixes only.`,
|
|
);
|
|
console.log(`\n🚀 MCP Inspector is up and running at:\n ${url}\n`);
|
|
if (process.env.MCP_AUTO_OPEN_ENABLED !== "false") {
|
|
console.log("🌐 Opening browser...");
|
|
open(url);
|
|
}
|
|
}, 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,
|
|
SERVER_PORT,
|
|
authDisabled,
|
|
sessionToken,
|
|
abort,
|
|
cancelled,
|
|
} = clientOptions;
|
|
const inspectorClientPath = resolve(
|
|
__dirname,
|
|
"../..",
|
|
"client",
|
|
"bin",
|
|
"client.js",
|
|
);
|
|
|
|
const url = getClientUrl(
|
|
CLIENT_PORT,
|
|
authDisabled,
|
|
sessionToken,
|
|
SERVER_PORT,
|
|
);
|
|
|
|
await spawnPromise("node", [inspectorClientPath], {
|
|
env: {
|
|
...process.env,
|
|
CLIENT_PORT,
|
|
INSPECTOR_URL: url,
|
|
},
|
|
signal: abort.signal,
|
|
echoOutput: true,
|
|
});
|
|
}
|
|
|
|
async function main() {
|
|
// Parse command line arguments
|
|
const args = process.argv.slice(2);
|
|
const envVars = {};
|
|
const mcpServerArgs = [];
|
|
let command = null;
|
|
let parsingFlags = true;
|
|
let isDev = false;
|
|
let transport = null;
|
|
let serverUrl = null;
|
|
|
|
for (let i = 0; i < args.length; i++) {
|
|
const arg = args[i];
|
|
|
|
if (parsingFlags && arg === "--") {
|
|
parsingFlags = false;
|
|
continue;
|
|
}
|
|
|
|
if (parsingFlags && arg === "--dev") {
|
|
isDev = true;
|
|
continue;
|
|
}
|
|
|
|
if (parsingFlags && arg === "--transport" && i + 1 < args.length) {
|
|
transport = args[++i];
|
|
continue;
|
|
}
|
|
|
|
if (parsingFlags && arg === "--server-url" && i + 1 < args.length) {
|
|
serverUrl = args[++i];
|
|
continue;
|
|
}
|
|
|
|
if (parsingFlags && arg === "-e" && i + 1 < args.length) {
|
|
const envVar = args[++i];
|
|
const equalsIndex = envVar.indexOf("=");
|
|
|
|
if (equalsIndex !== -1) {
|
|
const key = envVar.substring(0, equalsIndex);
|
|
const value = envVar.substring(equalsIndex + 1);
|
|
envVars[key] = value;
|
|
} else {
|
|
envVars[envVar] = "";
|
|
}
|
|
} else if (!command && !isDev) {
|
|
command = arg;
|
|
} else if (!isDev) {
|
|
mcpServerArgs.push(arg);
|
|
}
|
|
}
|
|
|
|
const CLIENT_PORT = process.env.CLIENT_PORT ?? "6274";
|
|
const SERVER_PORT = process.env.SERVER_PORT ?? DEFAULT_MCP_PROXY_LISTEN_PORT;
|
|
|
|
console.log(
|
|
isDev
|
|
? "Starting MCP inspector in development mode..."
|
|
: "Starting MCP inspector...",
|
|
);
|
|
|
|
// Use provided token from environment or generate a new one
|
|
const sessionToken =
|
|
process.env.MCP_PROXY_AUTH_TOKEN || randomBytes(32).toString("hex");
|
|
const authDisabled = !!process.env.DANGEROUSLY_OMIT_AUTH;
|
|
|
|
const abort = new AbortController();
|
|
|
|
let cancelled = false;
|
|
process.on("SIGINT", () => {
|
|
cancelled = true;
|
|
abort.abort();
|
|
});
|
|
|
|
let server, serverOk;
|
|
|
|
try {
|
|
const serverOptions = {
|
|
SERVER_PORT,
|
|
CLIENT_PORT,
|
|
sessionToken,
|
|
envVars,
|
|
abort,
|
|
command,
|
|
mcpServerArgs,
|
|
transport,
|
|
serverUrl,
|
|
};
|
|
|
|
const result = isDev
|
|
? await startDevServer(serverOptions)
|
|
: await startProdServer(serverOptions);
|
|
|
|
server = result.server;
|
|
serverOk = result.serverOk;
|
|
} catch (error) {}
|
|
|
|
if (serverOk) {
|
|
try {
|
|
const clientOptions = {
|
|
CLIENT_PORT,
|
|
SERVER_PORT,
|
|
authDisabled,
|
|
sessionToken,
|
|
abort,
|
|
cancelled,
|
|
};
|
|
|
|
await (isDev
|
|
? startDevClient(clientOptions)
|
|
: startProdClient(clientOptions));
|
|
} catch (e) {
|
|
if (!cancelled || process.env.DEBUG) throw e;
|
|
}
|
|
}
|
|
|
|
return 0;
|
|
}
|
|
|
|
main()
|
|
.then((_) => process.exit(0))
|
|
.catch((e) => {
|
|
console.error(e);
|
|
process.exit(1);
|
|
});
|