Copilot review round 7. - Add `contentSchema` to the traversal. It holds the schema the decoded string content must satisfy, so a bare `true` or a remote `$ref` under it is a real finding all three clients were walking past. Also admit the three content keywords to `CONSTRAINING_KEYWORDS` on the same documented grounds as `format` — annotation-only by vocabulary, but plainly an attempt to constrain, so reporting such a schema as accepting anything would be noise. The comment now names that category rather than singling `format` out. - Make `handleError` async and await the envelope write. Round 6 made the `--strict` report await its write and stopped one line short: the envelope emitted for the very same throw still raced `process.exit()`, so the machine-readable line a CI caller parses could be lost on a pipe. The launcher's comment claiming the envelope was "well inside the pipe buffer" was wrong reasoning and is replaced — fitting in the buffer says nothing about whether the write was performed. Its call site now awaits; the bin's `.catch(handleError)` needed no change. - The CLI README called its sample block the "complete output" when the error envelope follows it on every non-zero exit. Now scoped to "the complete report", with the envelope named and linked. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01UL9retfvAXRgvi6EWk4SY4 Signed-off-by: cliffhall <cliff@futurescale.com>
107 lines
4 KiB
JavaScript
107 lines
4 KiB
JavaScript
#!/usr/bin/env node
|
||
|
||
import { dirname, join } from "node:path";
|
||
import { fileURLToPath, pathToFileURL } from "node:url";
|
||
import { Command } from "commander";
|
||
import { parseLauncherArgv } from "./parse-launcher-argv.js";
|
||
|
||
const launcherDir = dirname(fileURLToPath(import.meta.url));
|
||
|
||
function clientEntry(client: "web" | "cli" | "tui"): string {
|
||
return pathToFileURL(
|
||
join(launcherDir, "..", "..", client, "build", "index.js"),
|
||
).href;
|
||
}
|
||
|
||
/**
|
||
* Whether to append the error stack on a top-level failure. Gated on `MCP_DEBUG`
|
||
* or `DEBUG` being *meaningfully* set — `"0"` / `"false"` / empty read as off, so
|
||
* a stray `DEBUG=0` doesn't turn stacks on (and `DEBUG` stays useful for the
|
||
* `debug` package's namespace filter, which is any non-empty value).
|
||
*/
|
||
function wantsDebugStack(): boolean {
|
||
const on = (v: string | undefined): boolean => {
|
||
const s = v?.trim().toLowerCase();
|
||
return !!s && s !== "0" && s !== "false";
|
||
};
|
||
return on(process.env.MCP_DEBUG) || on(process.env.DEBUG);
|
||
}
|
||
|
||
const program = new Command();
|
||
|
||
program
|
||
.name("mcp-inspector")
|
||
.description("MCP Inspector – run web UI, CLI, or TUI")
|
||
.option("--web", "Run web UI (default)")
|
||
.option("--cli", "Run CLI")
|
||
.option("--tui", "Run TUI");
|
||
|
||
let parsedArgv;
|
||
try {
|
||
parsedArgv = parseLauncherArgv(process.argv);
|
||
} catch (err) {
|
||
const message =
|
||
err instanceof Error ? err.message : "Invalid launcher arguments.";
|
||
console.error(`Error: ${message}`);
|
||
process.exit(1);
|
||
}
|
||
|
||
const { mode, forwardedArgv, hasPrefixModeFlag } = parsedArgv;
|
||
|
||
const helpOnly = process.argv.includes("-h") || process.argv.includes("--help");
|
||
|
||
if (helpOnly && !hasPrefixModeFlag) {
|
||
program.outputHelp();
|
||
console.log(
|
||
"\nMode flags (--web, --cli, --tui) must appear before app options. All following arguments are forwarded unchanged.",
|
||
);
|
||
process.exit(0);
|
||
}
|
||
|
||
async function run(): Promise<void> {
|
||
if (mode === "web") {
|
||
const { runWeb } = await import(clientEntry("web"));
|
||
await runWeb(forwardedArgv);
|
||
} else if (mode === "cli") {
|
||
// Route a CLI failure through the CLI's own error sink so `mcp-inspector
|
||
// --cli` (a module import here, so the CLI bin's own `.catch(handleError)`
|
||
// never fires) still honors the EXIT_CODES map and emits the JSON
|
||
// `{"error":…}` envelope its README documents — instead of the generic
|
||
// exit-1 catch-all below.
|
||
const { runCli, handleError } = await import(clientEntry("cli"));
|
||
try {
|
||
// On success, let `run()` resolve and the event loop drain (rather than a
|
||
// `process.exit(0)`) — the CLI closes its own transports, and an eager exit
|
||
// risks truncating a large stdout payload on a pipe (async on macOS).
|
||
await runCli(forwardedArgv);
|
||
} catch (err) {
|
||
// A stale `cli/build` (built before handleError was exported) would make
|
||
// this `undefined`; fall through to the generic sink with the real message
|
||
// rather than throwing "handleError is not a function" over it.
|
||
if (typeof handleError !== "function") throw err;
|
||
// Write-then-exit like the direct bin — and AWAIT it. The envelope being
|
||
// a few hundred bytes is not the safeguard it looks like: on a pipe the
|
||
// write is asynchronous, so `process.exit()` discards it whether or not
|
||
// it would have fitted in the buffer. `handleError` settles on the write
|
||
// callback before exiting; skipping the await puts the truncation back.
|
||
await handleError(err); // writes the envelope + process.exit(code)
|
||
}
|
||
} else {
|
||
const { runTui } = await import(clientEntry("tui"));
|
||
await runTui(forwardedArgv);
|
||
}
|
||
}
|
||
|
||
run().catch((err: unknown) => {
|
||
// Print the message, not the Error stack — a startup config error (a bad flag,
|
||
// the OAuth callback loopback guard) should read as actionable, not internal.
|
||
// The stack is still available under DEBUG / MCP_DEBUG for real faults.
|
||
console.error(
|
||
"Error running MCP Inspector:",
|
||
err instanceof Error ? err.message : err,
|
||
);
|
||
if (wantsDebugStack() && err instanceof Error) {
|
||
console.error(err.stack);
|
||
}
|
||
process.exit(1);
|
||
});
|