Closes#1795.
Enforce localhost-only binding in code (the "never 0.0.0.0" rule was only a spec note): refuse an all-interfaces HOST — across every spelling the OS binds as the wildcard (inet_aton decimal/octal/hex, IPv6, IPv4-mapped, zone-scoped, IDNA/Unicode) — unless DANGEROUSLY_BIND_ALL_INTERFACES=true (Docker's sanctioned opt-in). Fixes the reported stdio-connect 403 by expanding the default loopback origin allow-list to all three interchangeable forms.
Hardening surfaced across an extended review: closes an IDNA/fullwidth wildcard bypass, a blank-ALLOWED_ORIGINS fail-open, and the CLI/TUI OAuth-callback wildcard bind (loopback-only now, RFC 8252); canonicalizes the origin allow-list the way a browser does; derives frame-ancestors from the real embedder list (IPv6-literal exclusion verified in headless Chromium); strictPort on all three bind points; and preserves the CLI exit-code map + JSON envelope through the mcp-inspector --cli launcher path. Shared host normalization lives in core/node/hostUrl.ts. Docs + tests + smokes throughout.
🤖 Generated with [Claude Code](https://claude.com/claude-code)
- Refresh failure now falls back to injecting a still-present stored access
token instead of hard-exiting 3, so a transient auth-server hiccup doesn't
regress a previously-working invocation (review #1). Only a failure with no
access token to fall back on surfaces as exit 3.
- Persist rotated tokens via the shared writeStoreFile (0o600 + mkdir -p)
instead of a raw writeFile, so the secrets file keeps owner-only perms
identical to the web backend (review #2).
- Missing clientInformation now throws the distinct no_client_information
envelope code rather than reusing no_stored_token (review #5a).
- Docs: --wait-for-auth no longer claims parity with --use-stored-auth (it
injects the freshly-landed token without refreshing); noted the
always-refresh rotation/concurrency caveat (review #3, #4).
- Tests: discovery-returns-undefined metadata path, no_client_information
code, and an e2e refresh-failure → access-token fallback.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01BrihGWcrM9JGRyu41nzZYw
Closes#1665
`--use-stored-auth` previously injected the stored access token blindly, so an
expired token failed even with a valid refresh_token on disk. Now, when a
`refresh_token` is stored, the CLI runs the OAuth refresh grant and injects the
fresh access token, persisting the rotation.
- New exported `refreshStoredAuthToken(serverUrl, statePath, deps?)`: reuses the
SDK's `refreshAuthorization` (not a hand-rolled token request) with the stored
`clientInformation` + `serverMetadata` (discovered via core
`getAuthorizationServerUrl` + SDK `discoverAuthorizationServerMetadata` when
the metadata wasn't persisted). Rotated tokens are written back through
`serializeOAuthPersistBlob` under the same `{servers,idpSessions}` key, so web
and CLI stay consistent. Missing refresh_token/clientInformation or a failed
grant throw `CliExitCodeError(AUTH_REQUIRED)` with a clear message + envelope
code (`no_stored_token` / `refresh_failed`).
- `--use-stored-auth` prefers refresh when a refresh_token is present (covers
both absent and expired access tokens — the stored blob has no expiry, so the
refresh token is the durable credential); falls back to injecting the stored
access token otherwise (unchanged).
- `deps` are injectable so the refresh/discovery are unit-tested without a live
token endpoint; an e2e test drives the real SDK path against a mock token
endpoint and asserts the refreshed token is injected + the rotation persisted.
- CLI README updated (removed the "blind injection" caveat).
CLI coverage gate green (cli.ts 100/92/100/100).
Part of the #1579 decomposition (Wave 4, CLI lane).
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01BrihGWcrM9JGRyu41nzZYw
Closes#1666#1575 shipped `--print-handoff` with an interim deep-link shape (hardcoded
`transport: "http"`, marked `TODO(#1576)`). Now that #1576 owns the canonical
`?serverUrl&transport&autoConnect` format, `--print-handoff` emits exactly it:
- New `deepLinkTransport(serverUrl, transport)` helper derives `http`|`sse`
from the resolved server — explicit `--transport` wins, else auto-detect from
the URL path (`/sse` → sse), else default `http`. Unlike the core resolver it
defaults instead of throwing (the handoff is best-effort; the web parser
defaults an unknown transport to http too).
- `buildHandoff` takes the transport, emits the normalized serverUrl +
derived transport in the query, and keeps `autoConnect=<token>` (which #1576
adopted as the canonical CSRF gate, so the interim guess was already correct).
- Removed the `TODO(#1576)` marker; updated the doc comment + CLI README.
Tests: handoff emits `transport=http` for `/mcp`, `transport=sse` for `/sse`
and for explicit `--transport sse`; `deepLinkTransport` unit tests cover the
explicit/auto/default/stdio/unparseable branches. CLI coverage gate green
(cli.ts 100/92.8/100/100).
Part of the #1579 decomposition (Wave 4).
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01BrihGWcrM9JGRyu41nzZYw
* feat(cli): programmatic ergonomics — initialize, tool-args-json, connect-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
* feat(cli): --app-info probe + --format json envelope
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
* feat(cli): stored-auth suite — --use-stored-auth, --wait-for-auth, --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
* docs(core): scope MCP_STORAGE_DIR comment in getStateFilePath to the 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
* review(cli): per-tool-tolerant app-info listing + drop dead helper (PR #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
* review(cli): stored-auth follow-ups — TODO(#1576), wait-for-auth guard 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
---------
Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>