Repository navigation
Description
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:
- On startup the CLI auto-connects, gets the expected
401with
WWW-Authenticate: Bearer resource_metadata="https://mcp.dev.azure.com/.well-known/oauth-protected-resource/<org>". - 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/. - The CLI logs
Opening browser for <server> authentication...then
Requesting access token for <server>.... - 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. - Nothing further happens. The TUI stays on "Authenticating: " /
"Connecting to ...". No<key>.tokens.jsonis 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
~/.copilot/mcp-config.json:{"mcpServers":{"ado-remote-mcp":{"type":"http","url":"https://mcp.dev.azure.com/<org>"}}}- Start
copilot. It auto-connects and begins the OAuth flow. - Run
/mcp auth ado-remote-mcp, pressc, open the URL, complete sign-in. - Browser shows the CLI's "Authorization Successful" page.
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_LOGis 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.comadvertises no
device-code support, so the CLI logsServer does not support device code flow - falling back to browserand retries the browser path.- The callback arrives as
?nonce=<nonce>&code=<code>with nostate
parameter —vscode.dev/redirectconsumesstate(the CLI encodes the
nonce in the state fragment). If the handler validatesstate, 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/wslviewabsent), so the
CLI'sOpening browser...step is a silent no-op untilBROWSERis 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 on127.0.0.1(including the follow-up
favicon.icorequest), so the callback path itself is sound.
- WSL has no browser launcher by default (
Activity