Repository navigation
feat(cli,mcpdo): --oauth-debug traces each OAuth step to stderr - #2902
Conversation
There was a problem hiding this comment.
🟡 Changes recommended
OAuth debug output still leaks secrets in metadata URLs, while preflight and non-TTY retry paths can break root-dependent initialization and documented blocking behavior.
3 open issues
What changed in this PR
Adds --oauth-debug to the one-shot CLI and mcpdo connect, giving terminal users step-by-step OAuth diagnostics through shared core tracing.
Changes:
- Adds request/response tracing with credential redaction and authorization URL logging.
- Wires tracing into CLI login and stored-token refresh, and moves mcpdo debug login into the foreground.
- Adds unit and integration coverage and documents flag behavior.
| File | Description |
|---|---|
core/cli/oauth-debug.ts |
Shares guarded fetch tracing and stderr hooks. |
core/cli/cli-oauth-navigation.ts |
Adds an armed authorization URL hook. |
core/auth/oauthDebug.ts |
Implements OAuth classification, redaction, and trace formatting. |
clients/web/src/test/core/auth/oauthDebug.test.ts |
Tests trace formatting, redaction, and fetch behavior. |
clients/mcpdo/src/connection/mcp.ts |
Adds the flag and foreground authorization preflight. |
clients/mcpdo/src/connection/authorize.ts |
Enables tracing during foreground authorization. |
clients/mcpdo/README.md |
Documents tracing and blocking behavior. |
clients/mcpdo/__tests__/mcp-auth-coverage.test.ts |
Tests preflight ordering and traced retries. |
clients/mcpdo/__tests__/authorize.test.ts |
Tests fetch and navigation wiring. |
clients/cli/src/cli.ts |
Wires the flag into connection and refresh paths. |
clients/cli/README.md |
Documents trace output, redaction, and quiet-mode behavior. |
clients/cli/__tests__/README.md |
Adds the debug suite to the test index. |
clients/cli/__tests__/oauth-interactive.test.ts |
Adds a traced OAuth round-trip test. |
clients/cli/__tests__/oauth-debug.test.ts |
Tests terminal helpers and interactive CLI tracing. |
.claude/skills/project-structure/SKILL.md |
Documents the shared terminal tracing module. |
Review effort: Balanced
| if (cmdOpts.oauthDebug === true) { | ||
| await authorizeInFrontend(serverConfig, serverSettings, { |
There was a problem hiding this comment.
Fixed in 7a82d57. authorizeInFrontend now passes roots: cleanRoots(serverSettings?.roots ?? []), the same as daemon/connections.ts, so the preflight client negotiates the same roots capability. A test covers both configured and empty roots.
…, advertise roots Addresses Copilot round 1 on #2902: - redact credential query params inside URL values the trace prints (metadata endpoints, authorization_servers, error_uri, redirect URIs) - connect --oauth-debug never hands the retry to the untraced detached helper - the front-end preflight client advertises the configured roots Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Signed-off-by: cliffhall <cliff@futurescale.com>
f9e4fd5 to
7a82d57
Compare
Adds `--oauth-debug` to the one-shot CLI and `mcpdo connect`. Every OAuth request on the guarded OAuth base fetch (protected-resource and authorization-server metadata, client registration, token exchange and refresh) is printed as a request line and a response line, plus the authorization URL the armed navigation prints. Credentials are redacted by field name in query strings, form bodies and JSON, and an Authorization header shows only its scheme. mcpdo runs the sign-in in the front-end before the daemon dials when the flag is set, so the whole flow is traced in the caller's terminal. Closes #2844 Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Signed-off-by: cliffhall <cliff@futurescale.com>
…, advertise roots Addresses Copilot round 1 on #2902: - redact credential query params inside URL values the trace prints (metadata endpoints, authorization_servers, error_uri, redirect URIs) - connect --oauth-debug never hands the retry to the untraced detached helper - the front-end preflight client advertises the configured roots Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Signed-off-by: cliffhall <cliff@futurescale.com>
…sh path Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Signed-off-by: cliffhall <cliff@futurescale.com>
|
Copilot round 1: all three findings fixed. The branch has since been rebased onto v2/main, which now includes #2846, so the fix commit is now
The rebase conflict with #2846 is resolved by keeping both: on the |
7a82d57 to
c3f455f
Compare
Copilot round 2 on #2902: an error_description or a fetch error message quoting a URL with a credential in its query printed it. Both now go through redactUrlsInText; the original error is still rethrown unchanged. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Signed-off-by: cliffhall <cliff@futurescale.com>
|
Copilot round 2: one finding, fixed in 8413e99. URLs embedded in prose ( |
Copilot round 3 on #2902: an uppercase scheme, an apostrophe inside a query value, or comma-joined URLs slipped past the trace's own embedded-URL pattern. Reuse the matcher the Network log's error redaction already hardened for those cases (fetchTracking.ts, now exposed as mapUrlsInText), and compare sensitive field names case-insensitively. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Signed-off-by: cliffhall <cliff@futurescale.com>
|
Copilot round 3: one finding, fixed in fb1d9b4. Embedded URLs are now found with the shared hardened matcher from |
| * comma-joined URLs split apart. | ||
| */ | ||
| export function redactUrlsInText(text: string): string { | ||
| return mapUrlsInText(text, redactUrl); |
There was a problem hiding this comment.
Declined as out of scope for this PR, and filed as #2907. The splitting comes from the matcher in core/mcp/fetchTracking.ts, which predates this PR. The CLI error envelope and the web and TUI error text already use it, and this PR only reuses it (the round-3 fix). Changing how it tells a nested scheme apart from an adjacent URL changes behavior for all of those callers, so it deserves its own change and tests. That is #2907 (Low: it only applies when a credential is itself an http(s) URL).
|
Copilot round 4: one finding, declined as out of scope and filed as #2907. The finding is that a sensitive query value which itself contains Review loop closed. The round held only a finding declined as out of scope (pr-flow 7c). Across 4 rounds: round 1 had 3 findings (fixed), rounds 2 and 3 had 1 each (fixed), and round 4 had 1 (declined, #2907). |


Closes #2844
What
--oauth-debugfor the one-shot CLI and formcpdo connectprints each OAuth step to stderr while it runs. That covers the protected-resource and authorization-server metadata URLs with their statuses, the client registration request and response, the authorization URL, and the token request and response. Tokens, codes, PKCE verifiers and client secrets are redacted.Real output from the new integration test, which runs against the composable OAuth test server:
How
core/auth/oauthDebug.ts(new) holdswithOAuthDebugTrace(fetch, write). It names the step from each request (the well-known path, agrant_typeortokenform field, or a JSON registration body), then writes one request line and one response line. A failed request prints-> failed: <reason>and the error is rethrown unchanged. The response body is read from a clone, so the SDK still gets an unread body. Redaction works by field name (SENSITIVE_FIELDS) in query strings, form bodies and JSON. AnAuthorizationheader shows only its scheme. Metadata documents are cut down to their routing fields.OAuthStepstate. As the issue comment points out,oauthStepis set at only two points today, so a trace built on it would print two lines. Every request the issue lists already goes throughInspectorClientEnvironment.oauthFetch, the guarded base from Port v1's link-local/metadata destination guard to v2's /api/fetch proxy #2749. Wrapping that one fetch therefore covers the SDK's discovery, DCR, code exchange and refresh without changing the SDK orOAuthManager. The trace sits above the guard, so a refused destination shows as a failed request.core/cli/oauth-debug.ts(new) is the shared terminal wiring.createCliOAuthFetch(oauthDebug)builds the guarded fetch, traced when the flag is on.oauthDebugNavigationHookfeeds a newonAuthorizationUrloption oncreateCliOAuthNavigation, which fires only for the armed navigation. That way the trace never shows a connect-time authorize URL that nobody can complete.--oauth-debugis wired intoenvironment.oauthFetch, the navigation, and the--use-stored-authrefresh path. The trace goes to stderr only, so--format jsonkeeps stdout clean. It still prints under--quiet, because the user asked for it.connect --oauth-debugruns the sign-in in the front-end process before the daemon dials. Otherwise the daemon's first attempt would run discovery and registration in a process whose stderr isn't the user's terminal. The daemon then connects using the tokens that sign-in stored. One consequence: with the flag, the flow blocks even without a TTY, instead of taking thependingAuthhand-off.Out of scope
oauthStepinto a fully recorded state, and pointing the TUI or web OAuth view at it, is a separate change.Tests
clients/web/src/test/core/auth/oauthDebug.test.ts: classification, redaction, response summaries and the wrapper, at 100% on all four dimensions.clients/cli/__tests__/oauth-debug.test.ts: the helpers against the real guard. It also runs a full in-process interactive CLI login with--oauth-debugagainst the OAuth test server (withopenUrlstanding in for the browser) and asserts every step plus the redaction.clients/cli/__tests__/oauth-interactive.test.ts: a tracedInspectorClientround trip, asserting that the real access token never appears in the trace.authorize.test.ts(the flag reaches the fetch and the navigation) andmcp-auth-coverage.test.ts(front-end sign-in runs before the daemon dial, and the retry is traced too).Docs: the CLI README has a flag row, a "Tracing OAuth" section and a quiet-table row. The mcpdo README has a usage line and a
connect --oauth-debugparagraph. The CLI test README and theproject-structureskill body are updated too.npm run local:gateis green.🤖 Generated with Claude Code