Skip to content

fix(mcp): loopover-mcp CLI errors never respect --json, unlike @loopover/miner's cli-error contract #5928

Description

@JSONbored

Context

This repo already has an established JSON error-contract convention for CLIs. In
packages/loopover-miner/lib/cli-error.js:

/** Shared CLI failure output (#4836): when `--json` is set, emit a parseable `{ ok: false, error }`
 *  object on stdout (matching each command's success-path JSON stream); otherwise log plain text to
 *  stderr. */
export function reportCliFailure(wantsJson, message, exitCode = 2) {
  if (wantsJson) {
    console.log(JSON.stringify({ ok: false, error: message }, null, 2));
  } else {
    console.error(message);
  }
  return exitCode;
}

packages/loopover-miner/lib/cli.js wires this in at its top-level dispatch (reportCliFailure(argsWantJson(cliArgs), message, 1)), so any loopover-miner <cmd> --json failure still emits valid,
parseable JSON on stdout.

packages/loopover-mcp/bin/loopover-mcp.js — the sibling @loopover/mcp CLI in the same
monorepo, with its own --json flag threaded through every success path (login, logout,
whoami, status, doctor, decision-pack, analyze-branch, etc.) — has no equivalent. Its
entrypoint is:

if (cliArgs[0] && cliArgs[0] !== "--stdio") {
  const exitCode = await runCli(cliArgs);
  process.exit(typeof exitCode === "number" ? exitCode : 0);
}

There is no try/catch around this await, and no process.on("unhandledRejection", ...)
handler anywhere in the file. Every thrown Error inside runCli's command dispatch (missing
--login, unknown command, malformed --repo, a rejected apiFetch call, etc.) becomes an
uncaught exception: Node prints a full stack trace to stderr and exits 1, regardless of whether
--json was passed. This is confirmed by the test suite itself —
test/unit/mcp-cli-basics.test.ts line 117 asserts on a plain regex match against the thrown
message even when --json is in the argument list:

expect(() => run(["init-client", "--print", "codex", "--agent-profile", profile, "--json"])).toThrow(/Unsupported agent profile/);

— i.e. today's test suite documents (rather than catches) the fact that --json has zero effect on
error output. A caller scripting against loopover-mcp ... --json to parse machine-readable output
gets a raw stack trace on any failure instead of { ok: false, error: "..." }, unlike
loopover-miner's equivalent CLI.

Requirements

  • Add a packages/loopover-mcp/bin/loopover-mcp.js-local equivalent of
    reportCliFailure/argsWantJson/describeCliError (or extract a tiny shared module reused by
    both CLIs, if that's cleaner — the two implementations in packages/loopover-miner/lib/cli-error.js
    are trivial enough to duplicate or share, either is acceptable) that emits
    { ok: false, error: <message> } as pretty-printed JSON on stdout when --json/--json=... is
    present anywhere in process.argv, and the existing plain-text stderr message otherwise.
  • Wrap the top-level await runCli(cliArgs) call (and any other path that can let a runCli promise
    rejection reach the module top level) in a try/catch that calls this helper and sets
    process.exitCode (or calls process.exit) instead of letting Node print a raw stack trace.
  • Preserve the existing exit code behavior for the success path (process.exit(typeof exitCode === "number" ? exitCode : 0)) — only the failure branch changes.
  • Non-Error thrown values (rare, e.g. a rejected promise with a plain string) must still produce a
    usable message via a describeCliError-equivalent normalizer, not "[object Object]" or similar.

Deliverables

  • A JSON-aware failure reporter added to (or shared into) packages/loopover-mcp/bin/loopover-mcp.js,
    mirroring packages/loopover-miner/lib/cli-error.js's { ok: false, error } shape.
  • The top-level dispatch in loopover-mcp.js wraps runCli(cliArgs) so no command's failure
    path can produce a raw Node stack trace.
  • Every existing test/unit/mcp-cli-*.test.ts assertion that currently matches a bare error
    message via toThrow(/.../) continues to pass unchanged for the non---json case (no
    behavior change to stderr text), plus new tests asserting the --json case: running any
    failing command with --json prints valid, parseable { "ok": false, "error": "..." } JSON
    on stdout and a non-zero exit code, for at least: an unknown command, a missing --login, and
    an unsupported --agent-profile value.

Test Coverage Requirements

packages/loopover-mcp/bin/loopover-mcp.js is outside this repo's Codecov coverage.include
(vitest.config.ts's coverage.include covers src/**/*.ts, packages/loopover-engine/src/**/*.ts,
packages/loopover-miner/lib/**/*.js, and one review-enrichment file only —
packages/loopover-mcp/** is not listed), so this change is not numerically gated by Codecov's 99%
patch requirement. It IS still exercised by npm run test:mcp-pack/test:ci's
test/unit/mcp-cli-*.test.ts subprocess suite, which every existing .toThrow(...) assertion already
depends on — add new tests there (through test/unit/support/mcp-cli-harness.ts's
run()/runAsync(), so both stdout and the process exit code are exercised, not just in-process
function calls) covering both the --json and non---json branches of the new failure path.

Expected Outcome

Any loopover-mcp <command> --json invocation that fails prints a single parseable JSON object
{ "ok": false, "error": "<message>" } to stdout, never a raw Node stack trace — matching the
{ ok: false, error } contract @loopover/miner's CLI already guarantees, and making --json
mode reliable for scripting/CI use of @loopover/mcp regardless of success or failure.

Links & Resources

  • packages/loopover-miner/lib/cli-error.js — the existing convention to mirror.
  • packages/loopover-miner/lib/cli.js line 1 (import) and line 75 (reportCliFailure(...) call
    site) — how the sibling CLI wires it in.
  • packages/loopover-mcp/bin/loopover-mcp.js line 32 (cliArgs), line ~590
    (const exitCode = await runCli(cliArgs); process.exit(...)), runCli (~L1825).
  • test/unit/mcp-cli-basics.test.ts line 117 — the existing test that documents today's gap.

Metadata

Metadata

Assignees

No one assigned

    Labels

    gittensor:bugGittensor-scored bug fix — scores a 0.05x multiplier.help wantedExtra attention is needed

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions