Skip to content

cli: Wave 3 — programmatic review path (rollup of #1573–#1575)#1655

Merged
cliffhall merged 8 commits into
v2/mainfrom
v2/1575-cli-stored-auth
Jul 12, 2026
Merged

cli: Wave 3 — programmatic review path (rollup of #1573–#1575)#1655
cliffhall merged 8 commits into
v2/mainfrom
v2/1575-cli-stored-auth

Conversation

@cliffhall

@cliffhall cliffhall commented Jul 11, 2026

Copy link
Copy Markdown
Member

Closes #1573
Closes #1574
Closes #1575

Wave 3 rollup — CLI programmatic review path

Rollup of the three Wave 3 sub-issues from the #1579 decomposition of PR #1510, so the whole CLI lane can be smoke-tested and merged as one cumulative diff against v2/main. This branch (v2/1575-cli-stored-auth) already contains all three merged down base→tip, each run through the @claude review loop to clean with npm run ci green.

Included (base→tip):

Interim/follow-ups tracked separately: --print-handoff deep-link format (#1666, needs #1576) and refresh_token injection (#1665).

The three per-issue PRs (#1651, #1653, #1655) are superseded by this rollup; #1655 is repurposed as the rollup. Per AGENTS.md the Closes lines won't auto-fire on v2/main — the sub-issues will be closed and their board cards moved to Done on merge.

🤖 Generated with Claude Code

cliffhall and others added 3 commits July 11, 2026 12:51
…ect-timeout, env-var fixes

Adds the #1573 programmatic-ergonomics surface to the CLI, re-implemented
informed by PR #1510 and adapted to the v2/main OAuth-wired cli.ts:

- `--method initialize` — connect-only probe emitting
  `{serverInfo, protocolVersion, capabilities, instructions}`.
- `--tool-args-json '{"k":"v"}'` — tool args passed verbatim as JSON (no
  key=value coercion); mutually exclusive with `--tool-arg`, with clear
  errors for combined use, malformed JSON, and non-object values.
- `--connect-timeout <ms>` — default 15000 for ad-hoc `--server-url`/target
  runs, file-level timeout otherwise; `0` disables. Backed by the exported
  `withConnectTimeout()` helper.
- MCP_CATALOG_PATH is ignored when an ad-hoc target is given, so a shell that
  exports it can still run one-off ad-hoc invocations.
- MCP_STORAGE_DIR is honored by core `getStateFilePath` (`<dir>/oauth.json`),
  below the per-file MCP_INSPECTOR_OAUTH_STATE_PATH override.

Tests: new programmatic-ergonomics.test.ts (CLI, in-process runCli + direct
withConnectTimeout unit tests) and getStateFilePath resolution cases in the
web storage integration suite. Per-file coverage gate stays ≥90.
README: documents the new flags and env-var semantics.

Closes #1573

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01BrihGWcrM9JGRyu41nzZYw
Adds the #1574 app-probing and machine-readable output surface, re-implemented
informed by PR #1510 and adapted to the v2/main OAuth-wired cli.ts.

- `--method tools/call --tool-name <t> --app-info` — prints one JSON line
  describing the tool's app posture (hasApp, resourceUri, csp, permissions,
  domain, …) WITHOUT invoking the tool; exit 0 when it has an app, 2 (no_app)
  when it doesn't. A missing tool exits 5 (tool_not_found) — distinct from the
  no-app exit so a typo isn't mistaken for "no app".
- `--method tools/list --app-info` — one NDJSON line per tool over a single
  connection (`| jq -c 'select(.hasApp)'`).
- `--format json` — a single stdout envelope (`{result}` plus `{appInfo}` as a
  sibling key for App tools), no banner, so any method pipes into jq.
- A tools/call returning isError:true now exits 5 (tool_is_error) after
  printing its payload, so && chains stop on a failed call.

Implementation: `collectAppInfo()` (core `extractAppInfo` + a tolerated
resource read that reports `resourceError`), an NDJSON loop in tools/list, and
a single `emitResult()` that owns format + exit-code handling. `runMethod` now
returns a `MethodOutcome` discriminated union so the NDJSON path can signal
"already emitted".

Tests: app-info.test.ts (exit-0/2/5 + NDJSON + no-invoke), format-json.test.ts
(envelope shapes + bad --format), emit-result.test.ts (direct unit coverage of
the isError→exit-5 and resourceError branches via exported emitResult/
collectAppInfo). Updated the tools.test "nonexistent tool" case to the new
exit-5 tool_not_found contract. Per-file coverage ≥90 on all four dimensions
(cli.ts 100% stmts/funcs/lines). README documents the flags, NDJSON shape, and
exit semantics.

Closes #1574

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01BrihGWcrM9JGRyu41nzZYw
…list-stored-auth, --print-handoff

Adds the #1575 web→CLI OAuth handoff surface, re-implemented informed by PR
#1510 and adapted to v2/main.

- `--use-stored-auth` — read the shared oauth.json and inject
  `Authorization: Bearer <token>` for --server-url; exit 3 (no_stored_token),
  listing stored keys, when none matches.
- `--wait-for-auth <sec>` — poll oauth.json (500ms) until a token for
  --server-url appears, then proceed; timeout → exit 3 (auth_wait_timeout).
- `--list-stored-auth` — print `{oauthStatePath, storedServerUrls}` and exit
  (short-circuit, no connect).
- `--print-handoff` — emit `{serverUrl, deepLink, portForwardCmd,
  oauthStatePath, apiToken, note}` for --server-url and exit.

Adaptations vs. the reference:
- State-path resolution reuses core `getStateFilePath()` (the
  MCP_INSPECTOR_OAUTH_STATE_PATH → <MCP_STORAGE_DIR>/oauth.json → default
  precedence added in #1573) instead of a CLI-local resolver.
- The state file is read through the shared isomorphic
  `parseOAuthPersistBlob`, so both the plain `{servers,idpSessions}` layout the
  web backend writes and the legacy `{state,version}` envelope are accepted.
- `parseArgs` now returns a ParseResult union so the short-circuit modes return
  without a server connection.

Blind injection: the token is passed as-is; a stale one surfaces as HTTP 401 →
exit 3. Documented in the README along with all four flags, the state-path
precedence, and a follow-up note that --print-handoff's deepLink shape is
interim pending #1576's canonical handoff format.

Tests: stored-auth.test.ts drives injection, --header coexistence, missing
--server-url, no-token exit-3, URL-normalisation matching, MCP_STORAGE_DIR
resolution, listing, handoff (+note), wait-for-auth polling/timeout, and
normalizeServerUrl units — all against a real in-process HTTP test server.
Per-file coverage ≥90 on all four dimensions (cli.ts 100% stmts/funcs/lines).

Closes #1575

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01BrihGWcrM9JGRyu41nzZYw
@cliffhall cliffhall added the v2 Issues and PRs for v2 label Jul 11, 2026
…OAuth backend

Address review of PR #1651: the doc comment claimed MCP_STORAGE_DIR was
honoured "across the app, so setting it once points every persist backend at
the same directory", but the branch only relocates oauth.json. Narrow the
comment to state it is OAuth-backend-only (client.json/mcp.json still resolve
from getDefaultStorageDir → HOME) and record why the resolution lives here
rather than in getDefaultStorageDir (the default-path constant is evaluated at
module load, so a runtime-set MCP_STORAGE_DIR — as the CLI stored-auth /
--wait-for-auth flows set it — must be read at call time). Comment-only; no
behavior change.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01BrihGWcrM9JGRyu41nzZYw
cliffhall and others added 3 commits July 11, 2026 15:26
…#1653)

Address review of PR #1653:

- collectAppInfo now never throws: a malformed `_meta.ui.resourceUri`
  (extractAppInfo throws) folds into `{hasApp:false, resourceError}` just like a
  resources/read failure. This makes `tools/list --app-info` per-tool tolerant —
  one bad tool no longer aborts the whole NDJSON listing with a generic exit-1.
  Guarding inside collectAppInfo (vs. a loop-local try/catch) keeps the branch
  unit-testable; added an emit-result.test.ts case for the malformed-URI path.
- Documented that `tools/list --app-info` always emits NDJSON regardless of
  `--format` (the per-tool list shape is fixed; --format json only reshapes the
  single-result paths) — in both the code comment and the README.
- Removed the now-unused `expectJsonError` helper (its only caller moved to the
  exit-5 tool_not_found contract).

Coverage stays ≥90 on all four dimensions (cli.ts 100% stmts/funcs/lines).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01BrihGWcrM9JGRyu41nzZYw
@cliffhall

Copy link
Copy Markdown
Member Author

@claude review

@claude

claude Bot commented Jul 11, 2026

Copy link
Copy Markdown
Contributor

Claude finished @cliffhall's task in 2m 20s —— View job


Review — PR #1655: CLI stored-auth suite

  • Read the PR diff (cli.ts, tests, README)
  • Review cli.ts logic (arg parsing, stored-auth injection, wait/list/handoff)
  • Check tests & coverage
  • Post review feedback

Overall this is a clean, well-scoped addition. The four flags are cohesive, the state-path resolution correctly reuses core getStateFilePath(), the store shape (servers[url].tokens.access_token) matches ServerOAuthState, and the parseArgsParseResult union is a tidy way to model the short-circuit modes. Docs are thorough and honest about the interim/blind-injection tradeoffs. No blocking issues found. A handful of notes below.


1. --print-handoff deepLink doesn't carry the token the web app reads (interim, worth double-checking)

clients/cli/src/cli.ts:558-559

const params = new URLSearchParams({ serverUrl, transport: "http" });
if (apiToken) params.set("autoConnect", apiToken);

Per CLAUDE.md ("Web backend auth token"), the browser recovers the API token from window.__INSPECTOR_API_TOKEN__, ?MCP_INSPECTOR_API_TOKEN=…, or sessionStoragenot from an autoConnect param. As written, opening this deepLink on a fresh session (no injected token) would hit an unauthenticated /api/* and fail, since autoConnect=<token> isn't one of the three recognized sources. You've flagged this as interim pending #1576, and there's a note when the token is unset — so this is a "confirm the reconciliation" item, not a defect. Worth an explicit // TODO(#1576) on this line so it isn't forgotten. Also transport: "http" is hardcoded even when the real server is SSE — same interim bucket.

2. Blind injection ignores a stored refresh_token

clients/cli/src/cli.ts:498-507 / 957-960 — the token is injected as-is. ServerOAuthState.tokens (OAuthTokens) can also carry a refresh_token, so an expired access token surfaces as a 401 (documented, exit 3) even when a silent refresh was possible. Acceptable for a prototype and clearly documented, but a natural follow-up: fall back to a refresh when the access token is expired (connection-state.ts already has isJwtExpired).

3. Minor test gap: --wait-for-auth without --server-url

clients/cli/src/cli.ts:924-929 shares the missing---server-url guard between both flags via a ternary. Tests cover the --use-stored-auth arm (stored-auth.test.ts:103) but not the --wait-for-auth arm, so the "--wait-for-auth" branch of that message ternary is untested. A one-line test would close it and lock the branch coverage. Fix this →

4. Conflicting short-circuit flags resolve silently

--list-stored-auth is checked before --print-handoff (cli.ts:880-898), so passing both silently runs the listing; likewise both ignore any other flags without complaint. Fine as-is, but a note in the README that these modes are mutually exclusive and take precedence would preempt confusion.

Nits (non-blocking)

  • readOAuthServers does a dynamic import("node:fs/promises") on every poll iteration (cli.ts:484). Node caches the module so cost is negligible — just flagging it's re-imported per 500 ms tick.
  • waitForStoredToken computes const key only for the timeout message (cli.ts:521); fine, just slightly eager.
  • Empty-string MCP_INSPECTOR_OAUTH_STATE_PATH correctly falls through to MCP_STORAGE_DIR (verified against getStateFilePath), and the handoff test relies on this — good, subtle, and covered.

Nice work — the tests exercise real injection over an in-process HTTP server and the URL-normalization matching, which is exactly where this class of feature tends to break.
· branch v2/1575-cli-stored-auth

…d test, docs (PR #1655)

Address review of PR #1655:

- Added an explicit TODO(#1576) on buildHandoff's deepLink: `transport:"http"`
  is hardcoded even for SSE, and `autoConnect=<token>` is not one of the three
  token sources the web app reads (per CLAUDE.md) — both to be reconciled with
  #1576's canonical handoff format.
- Added a test for `--wait-for-auth` without `--server-url`, covering the
  previously-untested `--wait-for-auth` arm of the shared missing-server-url
  guard's message ternary.
- Documented that `--list-stored-auth` / `--print-handoff` are mutually
  exclusive short-circuit modes (list takes precedence) and that a stored
  refresh_token is not yet used (noted as a follow-up in code + README).

No behavior change; coverage stays ≥90 on all four dimensions (cli.ts 100%
stmts/funcs/lines).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01BrihGWcrM9JGRyu41nzZYw
@cliffhall

Copy link
Copy Markdown
Member Author

Thanks — applied 3 of the 4 substantive findings, declined #2 as a documented follow-up. Details:

1. --print-handoff deepLink / autoConnect param (interim) — applied a // TODO(#1576). Added an explicit inline TODO(#1576) on the buildHandoff params, calling out both that transport:"http" is hardcoded even for SSE servers and that autoConnect=<token> is not one of the three token sources the web app reads (per CLAUDE.md). Both to be reconciled with #1576's canonical format. The unset-token note already warns the user at runtime.

2. Blind injection ignores refresh_token — declined (documented follow-up). Agreed it's the right eventual behavior, but minting a fresh token from a stored refresh_token pulls in the SDK auth-provider refresh path and is beyond this PR's "reuse an existing token" scope. Recorded it as an explicit follow-up in both the injection-site comment and the README so it isn't lost.

3. Test gap: --wait-for-auth without --server-url — applied. Added a test asserting that arm of the shared guard's message ternary ("--wait-for-auth requires --server-url"), which was previously only covered for the --use-stored-auth arm.

4. Conflicting short-circuit flags resolve silently — documented. Added a README note that --list-stored-auth / --print-handoff are mutually exclusive short-circuit modes and that --list-stored-auth takes precedence. Left the precedence behavior as-is (erroring on the combo felt heavier than clarifying it).

Nits (dynamic import("node:fs/promises") per poll, eager key in waitForStoredToken) — left as-is; both are negligible and you flagged them as non-blocking. Empty-string MCP_INSPECTOR_OAUTH_STATE_PATH fall-through is intentional and covered.

npm run ci is green. (This branch also carries the #1651 and #1653 review fixes via merges from the parent branches.)

@cliffhall

Copy link
Copy Markdown
Member Author

@claude review

@claude

claude Bot commented Jul 11, 2026

Copy link
Copy Markdown
Contributor

Claude finished @cliffhall's task in 53s —— View job


Re-review — PR #1655: CLI stored-auth suite

  • Read the follow-up commit (133b9041) addressing the prior review
  • Review cli.ts stored-auth logic
  • Review tests & README changes
  • Post review feedback

Verified the follow-up commit against my earlier review. All four substantive items are resolved as described, and I found no regressions or new issues in the delta. This looks ready to merge.


Prior findings — verification

1. --print-handoff deepLink / autoConnect param — ✅ resolved. The explicit TODO(#1576) at cli.ts:558-562 now calls out both interim gaps precisely: transport:"http" hardcoded even for SSE, and autoConnect=<token> not being one of the three token sources the web app reads (window.__INSPECTOR_API_TOKEN__ / ?MCP_INSPECTOR_API_TOKEN / sessionStorage, per CLAUDE.md). Cross-referenced against buildHandoff and the runtime note — an unset-token caller is still warned at runtime. Good.

2. Blind injection ignores refresh_token — ✅ recorded as follow-up (correctly declined for scope). The injection-site comment (cli.ts:938-941) and README (clients/cli/README.md:203) now both note that a stored refresh_token is not yet used and that this is the natural next step. Agreed this is beyond "reuse an existing token" scope.

3. Test gap: --wait-for-auth without --server-url — ✅ closed. stored-auth.test.ts:112-121 now asserts the --wait-for-auth arm of the shared guard's message ternary ("--wait-for-auth requires --server-url"), which was previously covered only for the --use-stored-auth arm. The two arms of that ternary are now both exercised.

4. Conflicting short-circuit flags — ✅ documented. clients/cli/README.md:205 adds the "Short-circuit modes" note: both flags print-and-exit without connecting, ignore method/target flags, are mutually exclusive, and --list-stored-auth takes precedence. Behavior left as-is, which matches the check order at cli.ts:885-903.

Nits (dynamic import per poll, eager key in waitForStoredToken) left as-is — as expected, both were flagged non-blocking.


Nothing further from me. The delta is comment/test/docs-only with no behavior change, and coverage holds (cli.ts remains 100% stmts/funcs/lines per the PR notes). Nice, tidy close-out.
· branch v2/1575-cli-stored-auth

@cliffhall

Copy link
Copy Markdown
Member Author

Thanks for the tidy close-out. No further changes on this PR.

@cliffhall cliffhall changed the title cli: stored-auth suite — --use-stored-auth, --wait-for-auth, --list-stored-auth, --print-handoff cli: Wave 3 — programmatic review path (rollup of #1573–#1575) Jul 12, 2026
@cliffhall cliffhall changed the base branch from v2/1574-cli-app-info-format-json to v2/main July 12, 2026 01:54
@cliffhall cliffhall merged commit a029e42 into v2/main Jul 12, 2026
3 checks passed
@cliffhall cliffhall deleted the v2/1575-cli-stored-auth branch July 12, 2026 02:14
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.

Wave 3 rollup: CLI programmatic review path (backs PR #1655)

1 participant