Skip to content

feat(cli,mcpdo): --oauth-debug traces each OAuth step to stderr - #2902

Merged
cliffhall merged 5 commits into
v2/mainfrom
v2/feat/2844-oauth-debug
Nov 5, 2026
Merged

cliffhall merged 5 commits into
v2/mainfrom
v2/feat/2844-oauth-debug

Conversation

@cliffhall

Copy link
Copy Markdown
Member

Closes #2844

What

--oauth-debug for the one-shot CLI and for mcpdo connect prints 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:

[oauth] protected-resource metadata: GET http://localhost:61938/.well-known/oauth-protected-resource/mcp
[oauth]   -> 404 Not Found
[oauth] protected-resource metadata: GET http://localhost:61938/.well-known/oauth-protected-resource
[oauth]   -> 200 OK {"resource":"http://localhost:61938/","authorization_servers":["http://localhost:61938"],"scopes_supported":["mcp"]}
[oauth] authorization-server metadata: GET http://localhost:61938/.well-known/oauth-authorization-server
[oauth]   -> 200 OK {"issuer":"http://localhost:61938","authorization_endpoint":"…/oauth/authorize","token_endpoint":"…/oauth/token","registration_endpoint":"…/oauth/register",…}
[oauth] client registration: POST http://localhost:61938/oauth/register {"redirect_uris":["http://127.0.0.1:61941/oauth/callback"],"token_endpoint_auth_method":"none",…}
[oauth]   -> 201 Created {"client_id":"test_client_…","client_secret":"[redacted]",…}
[oauth] authorization: open http://localhost:61938/oauth/authorize?response_type=code&client_id=…&state=…&scope=mcp&resource=…
[oauth] token request: POST http://localhost:61938/oauth/token grant_type=authorization_code code=[redacted] code_verifier=[redacted] redirect_uri=… resource=… (Authorization: Basic [redacted])
[oauth]   -> 200 OK {"access_token":"[redacted]","token_type":"Bearer","expires_in":3600,"scope":"mcp","refresh_token":"[redacted]"}

How

  • core/auth/oauthDebug.ts (new) holds withOAuthDebugTrace(fetch, write). It names the step from each request (the well-known path, a grant_type or token form 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. An Authorization header shows only its scheme. Metadata documents are cut down to their routing fields.
  • Why a fetch wrapper and not the OAuthStep state. As the issue comment points out, oauthStep is set at only two points today, so a trace built on it would print two lines. Every request the issue lists already goes through InspectorClientEnvironment.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 or OAuthManager. 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. oauthDebugNavigationHook feeds a new onAuthorizationUrl option on createCliOAuthNavigation, which fires only for the armed navigation. That way the trace never shows a connect-time authorize URL that nobody can complete.
  • CLI: --oauth-debug is wired into environment.oauthFetch, the navigation, and the --use-stored-auth refresh path. The trace goes to stderr only, so --format json keeps stdout clean. It still prints under --quiet, because the user asked for it.
  • mcpdo: connect --oauth-debug runs 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 the pendingAuth hand-off.

Out of scope

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-debug against the OAuth test server (with openUrl standing in for the browser) and asserts every step plus the redaction.
  • clients/cli/__tests__/oauth-interactive.test.ts: a traced InspectorClient round trip, asserting that the real access token never appears in the trace.
  • mcpdo: authorize.test.ts (the flag reaches the fetch and the navigation) and mcp-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-debug paragraph. The CLI test README and the project-structure skill body are updated too.

npm run local:gate is green.

🤖 Generated with Claude Code

@cliffhall cliffhall added the v2 Issues and PRs for v2 label Nov 5, 2026
@cliffhall cliffhall linked an issue Nov 5, 2026 that may be closed by this pull request
2 tasks done
@cliffhall
cliffhall requested a balanced review from Copilot November 5, 2026 06:47

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.

🟡 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

Comment thread core/auth/oauthDebug.ts Resolved
Comment on lines +708 to +709
if (cmdOpts.oauthDebug === true) {
await authorizeInFrontend(serverConfig, serverSettings, {

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

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.

Comment thread clients/mcpdo/src/connection/mcp.ts Resolved
cliffhall added a commit that referenced this pull request Nov 5, 2026
…, 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>
@cliffhall
cliffhall force-pushed the v2/feat/2844-oauth-debug branch from f9e4fd5 to 7a82d57 Compare November 5, 2026 07:24
cliffhall and others added 3 commits November 5, 2026 02:25
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>
@cliffhall

Copy link
Copy Markdown
Member Author

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 1ba589fb (it was 7a82d571 before the rebase).

  • Credentials in metadata URLs (high): every URL value the trace prints from a response or request body now goes through the query redaction (metadata fields, authorization_servers, error_uri, registration and form redirect_uri). This is tested.
  • Roots in the preflight client: authorizeInFrontend now advertises cleanRoots(serverSettings?.roots ?? []), the same as the daemon's client. This is tested.
  • Non-TTY retry stays blocking under --oauth-debug: the detached-helper branch is skipped when the flag is set. This is tested.

The rebase conflict with #2846 is resolved by keeping both: on the --use-stored-auth refresh path, the trace (traceCliOAuthFetch) wraps #2846's refusal reporting over the guard, and on the connect path createCliOAuthFetch is the oauthFetch that InspectorClient adds its refusal reporting to. A refused fetch therefore shows as -> failed in the trace and still produces #2846's warning. local:gate is green on the rebased branch.

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.

🟡 Changes recommended

OAuth diagnostics can expose secrets in URLs embedded in error descriptions and failure-message traces because neither path properly sanitizes embedded URLs.

2 open issues
2 resolved since last review

Review effort: Balanced

Comment thread core/auth/oauthDebug.ts Resolved
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>
@cliffhall

Copy link
Copy Markdown
Member Author

Copilot round 2: one finding, fixed in 8413e99. URLs embedded in prose (error_description, fetch error messages) are now redacted by redactUrlsInText, with regression tests for both paths. The other open item in the review header is round 1's roots thread, which was already fixed and answered. local:gate is green.

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.

🟡 Changes recommended

Embedded-URL redaction still leaks credentials when URL schemes are uppercase, query values contain apostrophes, or URLs are comma-adjacent.

2 open issues
1 resolved since last review

Review effort: Balanced

Comment thread core/auth/oauthDebug.ts Resolved
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>
@cliffhall

Copy link
Copy Markdown
Member Author

Copilot round 3: one finding, fixed in fb1d9b4. Embedded URLs are now found with the shared hardened matcher from fetchTracking.ts (mapUrlsInText), which handles uppercase schemes, apostrophes in values and comma-joined URLs. Field names now match case-insensitively. Regression tests are added, and local:gate is green.

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.

🟡 Changes recommended

The URL matcher can expose credentials by splitting sensitive query values at embedded URL schemes instead of redacting the entire value.

2 open issues
1 resolved since last review

Review effort: Balanced

Comment thread core/auth/oauthDebug.ts
* comma-joined URLs split apart.
*/
export function redactUrlsInText(text: string): string {
return mapUrlsInText(text, redactUrl);

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

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).

@cliffhall

Copy link
Copy Markdown
Member Author

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 http(s):// is split by the shared embedded-URL matcher in fetchTracking.ts. That matcher predates this PR and serves every redaction caller, so the fix belongs in its own change.

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).

@cliffhall
cliffhall merged commit ecac785 into v2/main Nov 5, 2026
11 checks passed
@cliffhall
cliffhall deleted the v2/feat/2844-oauth-debug branch November 5, 2026 08:51
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 / mcpdo: --oauth-debug to show each OAuth step

2 participants