inspector/clients/launcher/src/index.ts
cliffhall 31bdd532a6 fix: walk contentSchema, await the error envelope, correct the README claim
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>
2026-08-25 03:26:39 -04:00

107 lines
4 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

#!/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);
});