feat(cli): --output <path> / --output-format raw|json to save a result to a file - #2582
Merged
Merged
Conversation
…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>
2 tasks done
2 tasks done
There was a problem hiding this comment.
Copilot review overview
🟡 Changes recommended
Malformed base64 payloads are silently decoded into corrupted output instead of failing.
Review effort: Balanced
Findings: 1
Open (1)
What changed in this PR
Adds CLI file export for MCP method results, with TUI support deferred to #2571.
Changes:
- Adds
--outputand--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.
#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>
Member
Author
|
Copilot review loop closed: exit = clean round (round 2: no inline comments, no headline finding, no suppressed comments; "Approval recommended").
|
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
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>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.

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, likecurl -o).--output-format raw|json— how the file is encoded (defaultjson).jsonJSON.stringify(result, null, 2)— the same shape the web client's exports download.raw\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 withoutput_not_raw.tools/callandresources/readonly.stdout: in text mode it stays empty and
Wrote N bytes (fmt) to <path>goes to stderr; under--format jsonstdout still carries exactly one envelope, withoutput: {path, format, bytes}in place ofresult(plusappInfo/schemaFindingswhen they would otherwise appear). Exit codes are unchanged — anisError:trueresult is still written and still exits5. A failed write exits1with envelope codeoutput_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):--outputwith--app-info,--verify,--list-stored-auth,--print-handoff,servers/list/servers/show;--output-formatwithout--output;rawfor a method with no raw payload.Decisions worth reviewing
--output-format, not--format raw|jsonas the issue sketched.--format text|jsonalready exists and shapes stdout; overloading it with a file encoding would make--format json --output xambiguous. The two now compose instead.toolResultUtils.ts, but that module only classifies errors and detects resource links. So "json" matches the web's log exports (2-space pretty JSON viauseExportActions) and "raw" matches what theContentViewercopy button copies (block text). Since the web has no such logic, there was nothing to lift intocore/; the rendering is pure and self-contained inclients/cli/src/handlers/output-file.ts, so TUI has no keybinding to save a tool call result to a file #2571 can move it tocore/if the TUI needs it.handlers/output-file.ts);cli.tsgets 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--outputis set.clients/cli/src/handlers/method-types.ts—output/outputFormatonMethodArgs.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 formatclean.npm run local:gate, run with the sandbox proxy vars unset:local:validate,verify:skills:cli: pass.coverage:web: 8782/8783. The single failure issecret-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-v2is 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.ts100 / 96.66 / 100 / 100,emit-result.ts100 / 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