Skip to content

feat(cli): --output <path> / --output-format raw|json to save a result to a file - #2582

Merged
cliffhall merged 3 commits into
v2/mainfrom
v2/feat/2431-cli-output-file
Oct 5, 2026
Merged

cliffhall merged 3 commits into
v2/mainfrom
v2/feat/2431-cli-output-file

Conversation

@cliffhall

Copy link
Copy Markdown
Member

Closes #2431

CLI half; TUI half split to #2571.

What

Two new CLI flags write a method result to a file instead of stdout:

  • --output <path> — the file to write (parent directory must exist; an existing file is replaced, like curl -o).
  • --output-format raw|json — how the file is encoded (default json).
Format File contents
json The whole result, JSON.stringify(result, null, 2) — the same shape the web client's exports download.
raw The text of the text-bearing blocks (text blocks + embedded text resources), joined by \n. With no text and exactly one binary block (image / audio / resource blob), that block's decoded bytes — so an image tool saves as a viewable file. Otherwise fails with output_not_raw. tools/call and resources/read only.

stdout: in text mode it stays empty and Wrote N bytes (fmt) to <path> goes to stderr; under --format json stdout still carries exactly one envelope, with output: {path, format, bytes} in place of result (plus appInfo / schemaFindings when they would otherwise appear). Exit codes are unchanged — an isError:true result is still written and still exits 5. A failed write exits 1 with envelope code output_write_failed.

Combinations that would be accepted and then silently do nothing are rejected up front, ahead of the short-circuit returns (the same pattern as --strict): --output with --app-info, --verify, --list-stored-auth, --print-handoff, servers/list/servers/show; --output-format without --output; raw for a method with no raw payload.

Decisions worth reviewing

  • --output-format, not --format raw|json as the issue sketched. --format text|json already exists and shapes stdout; overloading it with a file encoding would make --format json --output x ambiguous. The two now compose instead.
  • The web ToolResultPanel has no raw/json export to match. The issue cites toolResultUtils.ts, but that module only classifies errors and detects resource links. So "json" matches the web's log exports (2-space pretty JSON via useExportActions) and "raw" matches what the ContentViewer copy button copies (block text). Since the web has no such logic, there was nothing to lift into core/; the rendering is pure and self-contained in clients/cli/src/handlers/output-file.ts, so TUI has no keybinding to save a tool call result to a file #2571 can move it to core/ if the TUI needs it.
  • New logic lives in its own module (handlers/output-file.ts); cli.ts gets two .option() calls, one validation call and two passthrough fields, to keep merge conflicts with the sibling CLI branches small.

Files

  • clients/cli/src/handlers/output-file.ts — new: parse/validate/render/write.
  • clients/cli/src/handlers/emit-result.ts — route the result to the file when --output is set.
  • clients/cli/src/handlers/method-types.ts — output / outputFormat on MethodArgs.
  • clients/cli/src/cli.ts — flags, validation call, passthrough.
  • clients/cli/__tests__/output-file.test.ts — unit + in-process end-to-end against the stdio test server.
  • clients/cli/README.md, clients/cli/__tests__/README.md — docs.

Gate

npm run format clean. npm run local:gate, run with the sandbox proxy vars unset:

  • local:validate, verify:skills:cli: pass.
  • coverage:web: 8782/8783. The single failure is secret-store-selection.test.ts > isOnMountPoint > is false when the path itself can't be resolved, which is environmental. In this sandbox the repo itself is a bind mount (/Users/cliffhall/Projects/mcp-inspector-v2 is listed in /proc/self/mountinfo), so the unresolvable relative path walks up to a cwd that really is a mount point. It's independent of this diff (no web/core/ change), and sibling agents hit the same thing.
  • coverage:cli / coverage:tui / coverage:launcher: pass. output-file.ts 100 / 96.66 / 100 / 100, emit-result.ts 100 / 95.12 / 100 / 100 (stmts/branches/funcs/lines).
  • verify:build-gate, verify:bundle-externals: pass.
  • smoke:launcher, smoke:cli, smoke:tui: pass.
  • smoke:web*, smoke:web:firefox, local:storybook: not run locally. The machine-wide gate lease is ~20 deep with sibling agents and twice timed out (51m, 48m) before my turn. These stages cover web code this PR does not touch, so CI is the check for them.

No screenshots: no web or TUI UI change.

🤖 Generated with Claude Code

…ult to a file (#2431)

Writes the single-result path's output to a file instead of stdout.
json is the whole result pretty-printed; raw is the text of the
text-bearing blocks, or the decoded bytes of a single binary block.
Under --format json stdout carries an { output } envelope in place of
{ result }. Flag combinations that would be silently inert are
rejected up front. The TUI half is split to #2571.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Signed-off-by: cliffhall <cliff@futurescale.com>
@cliffhall cliffhall added the v2 Issues and PRs for v2 label Oct 5, 2026
@cliffhall cliffhall linked an issue Oct 5, 2026 that may be closed by this pull request
2 tasks done
@cliffhall
cliffhall requested a balanced review from Copilot October 5, 2026 07:28

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Copilot review overview

🟡 Changes recommended

Malformed base64 payloads are silently decoded into corrupted output instead of failing.

Review effort: Balanced
Findings: 1 Medium severity

Open (1)
What changed in this PR

Adds CLI file export for MCP method results, with TUI support deferred to #2571.

Changes:

  • Adds --output and --output-format raw|json.
  • Supports JSON, text, and decoded binary output.
  • Adds validation, documentation, and end-to-end tests.
File Description
clients/​cli/​src/​handlers/​output-file.ts Implements validation, rendering, and file writing.
clients/​cli/​src/​handlers/​method-types.ts Adds output options to method arguments.
clients/​cli/​src/​handlers/​emit-result.ts Routes results to files and reports metadata.
clients/​cli/​src/​cli.ts Registers and validates the new flags.
clients/​cli/​README.md Documents file export behavior.
clients/​cli/​__tests__/​README.md Catalogs the new test suite.
clients/​cli/​__tests__/​output-file.test.ts Covers validation, rendering, writes, and CLI integration.

💡 Add a code-review agent skill for context-aware, tailored reviews. Learn more in the docs.

Comment thread clients/cli/src/handlers/output-file.ts Outdated
#2431)

Buffer.from(x, 'base64') silently drops invalid characters, so a
malformed payload was written as unrelated bytes. Decode through core's
base64ToBytes (atob, which throws) and report output_not_raw instead.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Signed-off-by: cliffhall <cliff@futurescale.com>

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Copilot review overview

🟢 Approval recommended

The implementation matches the stated CLI contract and includes strong coverage for validation, output formats, failures, and exit behavior.

Review effort: Balanced
Findings: None

Resolved since last review (1)

@cliffhall

Copy link
Copy Markdown
Member Author

Copilot review loop closed: exit = clean round (round 2: no inline comments, no headline finding, no suppressed comments; "Approval recommended").

  • Round 1 (1 finding): Strictly validate base64 payloads before writing files. Fixed in 43ae67c: the single-binary raw path now decodes through core's base64ToBytes (atob, which throws) and reports output_not_raw on invalid base64 instead of writing unrelated bytes. There is a unit test for it, and I replied in the thread.
  • Round 2: clean.

Signed-off-by: cliffhall <cliff@futurescale.com>

# Conflicts:
#	clients/cli/README.md
#	clients/cli/__tests__/README.md
#	clients/cli/src/cli.ts
#	clients/cli/src/handlers/method-types.ts
@cliffhall
cliffhall merged commit e856b92 into v2/main Oct 5, 2026
5 checks passed
@cliffhall
cliffhall deleted the v2/feat/2431-cli-output-file branch October 5, 2026 14:57
cliffhall added a commit that referenced this pull request Oct 5, 2026
v2/main gained the #2568 CLI rollup PRs (incl. #2582 `--output`) and
#2587/#2588. Conflicts were import blocks in the TUI Prompts/Resources/
Skills tabs and ToolTestModal (#2430 / #2571 imports next to #2588's
errorText helpers) — both sides kept — and the CLI test README, where
v2/main's table is kept minus the `open-url.test.ts` row #2533 moved to
core.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Signed-off-by: cliffhall <cliff@futurescale.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

v2 Issues and PRs for v2

Projects

None yet

Development

Successfully merging this pull request may close these issues.

CLI has no way to export/save a tool call result to a file

2 participants