Skip to content

MCP OAuth re-auth repeatedly fails on Windows when the cached loopback redirect port is in an excluded TCP port range #3973

Description

@NStarkebaum

On Windows, an HTTP MCP server using OAuth can get into a sticky re-auth failure state: repeated /mcp re-auth attempts fail with the same local listener bind error until the cached OAuth registration is manually removed. The observed failure mode is that the CLI reuses a cached, dynamically registered client whose loopback redirect URI contains a fixed port (e.g. http://127.0.0.1:60909/). On my machine that port was inside a Windows excluded TCP port range, so the local loopback listener could not bind and failed with EACCES. Because the same cached redirect URI is reused on each retry, every re-auth attempt hits the same unbindable port, with no self-healing path.

Transient error shown in the TUI during /mcp re-auth:

Authenticating: foundry-mcp
Authentication failed: MCPOAuthError: listen EACCES: permission denied 127.0.0.1:60909

I could not find this bind error persisted in the CLI log/cache locations I checked; I only observed it transiently in the TUI, which made it difficult to diagnose after the fact.

Evidence that the failure is the local listener bind, not the server/token/config:

  • Cached registration ~/.copilot/mcp-oauth-config/<hash>.json contained "redirectUri": "http://127.0.0.1:60909/" (dynamic registration, isStatic:false).
  • netsh interface ipv4 show excludedportrange protocol=tcp showed an excluded range 60868-60967; 60909 is inside it.
  • Get-NetTCPConnection -LocalPort 60909 -State Listen returned nothing — the port is reserved by the OS, not occupied by a listener (consistent with EACCES rather than EADDRINUSE; see KB 3039044, where tcpip.sys marks excluded-range ports RESERVED and bind() returns WSAEACCES/10013).
  • Using the still-valid cached access token, a manual POST of an MCP initialize request returned HTTP 200 with a valid response. That indicates the server endpoint, network path, cached token, and MCP config were not the cause.

Affected version

GitHub Copilot CLI 1.0.66-2

Steps to reproduce the behavior

  1. Run on Windows where one or more excluded TCP port ranges exist. Confirm with: netsh interface ipv4 show excludedportrange protocol=tcp
  2. Configure an HTTP MCP server that uses OAuth in ~/.copilot/mcp-config.json.
  3. Let the CLI perform dynamic client registration and cache a loopback redirect URI in ~/.copilot/mcp-oauth-config/<hash>.json.
  4. If the cached redirect URI's port is inside an excluded range, run /mcp and re-authenticate the server.
  5. Observe repeated failure: Authentication failed: MCPOAuthError: listen EACCES: permission denied 127.0.0.1:<port>

In my repro the cached redirect URI used port 60909, and Windows reported an excluded range of 60868-60967.

Expected behavior

The CLI should recover automatically instead of looping on an unbindable pinned port:

  1. Validate/recover the loopback port for dynamically registered clients. Before reusing a cached loopback redirect port, attempt to bind it; on EACCES/EADDRINUSE, discard or refresh that cached registration and register a new redirect URI on a bindable port. A robust approach is to bind an OS-selected loopback port first (bind to port 0), read back the assigned port, then use it when constructing the authorization request / dynamic registration. This matches RFC 8252 §7.3, whose expected pattern is for native-app clients to obtain an ephemeral loopback port from the OS at request time — the spec's normative "MUST allow any port" is placed on the authorization server specifically to enable this. §8.4 further notes loopback redirects match on everything except the port. Re-registering with a different loopback port is permitted by Dynamic Client Registration (RFC 7591). Pinning a fixed loopback port is fragile on platforms where a given port may be OS-excluded.
  2. Persist the bind failure (MCP server name, address, port, and errno/code) to the diagnostic log, so a local listener-bind failure can be distinguished from an actual auth-provider failure.

Additional context

  • OS / environment: Windows (win32-x64). Reproduced on a Microsoft Dev Box / Windows 365 Cloud PC (Manufacturer: Microsoft Corporation, Model: Virtual Machine) with hns, vmcompute, winnat, and WslService running and three excluded TCP ranges present (49723-49822, 50000-50059, 60868-60967). I did not independently determine which component created the range containing 60909.
  • Trigger scope: Excluded port ranges are common on Windows developer environments that use virtualization/container networking (Hyper-V, WSL2, Docker, HNS, WinNAT). They appear to be common on Microsoft Dev Box / Windows 365 Cloud PC images where these features are enabled by default. When the condition is hit, the failure is sticky and non-obvious: the server stays failed and re-auth loops until the hidden cached registration files are removed.
  • Manual workaround that fixed it: delete the server's <hash>.json registration and <hash>.tokens.json from ~/.copilot/mcp-oauth-config/, then re-auth — the CLI re-registers on a new (bindable) port and authentication succeeds.
  • I found related but apparently distinct OAuth issues (Can not authorize MCP server using Oauth #3393, MCP OAuth in headless session times out #3039 headless/SSH browser; Drive MCP OAuth not attached: tools fail with 'missing required authentication credential' after successful reauth #3838 reauth not attached). I did not find one covering a Windows excluded-port listener bind failure.

Metadata

Metadata

Assignees

No one assigned

    Labels

    area:authenticationLogin, OAuth, device auth, token management, and keychain integrationarea:mcpMCP server configuration, discovery, connectivity, OAuth, policy, and registryarea:platform-windowsWindows-specific: PowerShell, cmd, Git Bash, WSL, Windows Terminal

    Type

    No fields configured for Bug.

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions