Skip to content

MCP OAuth (interactive): authorization code is received and the success page is served, but the code->token exchange never runs — server stuck "Connecting" #5350

Open

Description

@poppj1

Describe the bug

For a remote HTTP MCP server using Entra ID OAuth (Azure DevOps remote MCP,
https://mcp.dev.azure.com/<org>), the interactive CLI completes the browser
round-trip but never exchanges the authorization code for a token.

Observed with Copilot CLI running inside WSL2 (Ubuntu 26.04 on a Windows 11
host). I have not tested native Windows or native Linux, so I cannot say whether
this is WSL-specific — see the note at the end.

Observed sequence:

  1. On startup the CLI auto-connects, gets the expected 401 with
    WWW-Authenticate: Bearer resource_metadata="https://mcp.dev.azure.com/.well-known/oauth-protected-resource/<org>".
  2. Metadata discovery and dynamic client registration succeed — a registration
    record is written to ~/.copilot/mcp-oauth-config/<key>.json, with
    authorizationServerUrl, clientId: aebc6443-996d-45c2-90f0-388ff96faa56,
    redirectUri: http://127.0.0.1:41003/.
  3. The CLI logs Opening browser for <server> authentication... then
    Requesting access token for <server>....
  4. I complete the sign-in. The browser lands on
    http://127.0.0.1:41003/?nonce=<nonce>&code=<code> and the CLI's own
    callback page renders
    ("Authorization Successful"). So the code reaches
    the CLI's listener.
  5. Nothing further happens. The TUI stays on "Authenticating: " /
    "Connecting to ...". No <key>.tokens.json is ever written, and no
    log line is emitted after this point.

The <key>.verifier file is not removed. Per the runtime's own state
machine (MCP OAuth token exchange completed; saving tokens ->
MCP OAuth tokens saved; clearing code verifier), a surviving verifier means
the exchange never completed successfully.

The MCP transport worker meanwhile keeps cycling:

[ERROR] [rust:rmcp::transport::worker] worker quit with fatal: Transport channel closed,
  when Client(OAuthChallenge { www_authenticate_header: "...oauth-protected-resource/<org>",
  response: McpOAuthHttpResponse { status_code: 401, ... } })

The failure is in the CLI, not the environment

I reproduced the identical flow manually with a small script — same client ID,
same scope, same vscode.dev relay, same loopback pattern
— and it succeeds
on the first attempt:

TOKEN EXCHANGE: HTTP 200 SUCCESS
  scope        : https://mcp.dev.azure.com/Ado.Mcp.Tools https://mcp.dev.azure.com/.default
  token_type   : Bearer
  expires_in   : 4102
  refresh_token: present

Driving the MCP server with that token also works:

[initialize] HTTP 200   serverInfo: {"name": "AzureDevOps.Mcp", "version": "1.0.0.0"}
[tools/list] HTTP 200   tool count: 43

Writing that token into ~/.copilot/mcp-oauth-config/<key>.tokens.json in the
CLI's own schema (accessToken, refreshToken, expiresAt, requestedScope,
scope) makes the server connect normally. grant_type=refresh_token renewal
against the same endpoint also works.

So tenant config, consent, scopes, app registration, the vscode.dev relay,
loopback reachability and the MCP server are all confirmed working. The only
step that fails is the CLI's own code->token exchange.

Affected version

GitHub Copilot CLI 1.0.105

Steps to reproduce the behavior

  1. ~/.copilot/mcp-config.json:
    {"mcpServers":{"ado-remote-mcp":{"type":"http","url":"https://mcp.dev.azure.com/<org>"}}}
    
  2. Start copilot. It auto-connects and begins the OAuth flow.
  3. Run /mcp auth ado-remote-mcp, press c, open the URL, complete sign-in.
  4. Browser shows the CLI's "Authorization Successful" page.
  5. ls ~/.copilot/mcp-oauth-config/ — no .tokens.json; the .verifier
    remains. The TUI never leaves the connecting state.

Expected behavior

After the callback is received, the CLI proceeds to exchanging_code, stores
the tokens, clears the verifier, and connects the server.

Additional context

  • No diagnostics are available for this step. RUST_LOG is accepted but
    produces no additional output, and the process log contains nothing at all
    between the callback and the stall. The runtime has specific failure strings
    (ExchangeResponseFailure, failed to clear OAuth code verifier after authorization failure) but none reach the log file. Surfacing the token
    endpoint's response on failure would make this self-diagnosable.
  • ctrl+g (device code) does not help: mcp.dev.azure.com advertises no
    device-code support, so the CLI logs Server does not support device code flow - falling back to browser and retries the browser path.
  • The callback arrives as ?nonce=<nonce>&code=<code> with no state
    parameter
    — vscode.dev/redirect consumes state (the CLI encodes the
    nonce in the state fragment). If the handler validates state, that may be
    relevant. Unverified.
  • Possibly related: Regression (1.0.88 → 1.0.89): MCP OAuth authorization page opens on every ACP session instead of the challenge being resolved silently #4994, ACP mode: MCP OAuth token is never persisted after successful sign-in — every new session re-opens the browser #5192, MCP OAuth tokens for HTTP servers not reliably reused across sessions — duplicate cache-key entries force repeated re-auth #4695 — all ACP-mode or cross-session reuse.
    This one is plain interactive mode on first sign-in.
  • Environment caveat (WSL2). This was only observed with the CLI running
    inside WSL2; I have not reproduced it on native Windows or native Linux, so I
    cannot confirm whether the platform matters. Two WSL-specific points, both
    ruled out as causes:
    • WSL has no browser launcher by default (xdg-open/wslview absent), so the
      CLI's Opening browser... step is a silent no-op until BROWSER is set.
      That produces a separate earlier symptom (no prompt at all). After adding a
      launcher the browser opens correctly, and the stall described above persists.
    • Windows→WSL loopback forwarding works. An independent listener inside WSL
      received the browser's redirect on 127.0.0.1 (including the follow-up
      favicon.ico request), so the callback path itself is sound.

Activity

  1. added theissue type on Nov 4, 2026
  2. added
    area:mcpMCP server configuration, discovery, connectivity, OAuth, policy, and registry
    and removed on Nov 5, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    area:mcpMCP server configuration, discovery, connectivity, OAuth, policy, and registry

    Type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions