github / github/copilot-cli

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

Open
#3,973 0 comments 0 reactions 0 assignees View on GitHub
area:authentication area:mcp area:platform-windows
Dominant language
Shell
Stars
11.2k
Forks
1.9k
Avg merge
14h 16m
Merged PRs (30d)
6

Description

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/.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](https://learn.microsoft.com/en-us/troubleshoot/windows-server/networking/error-10013-wsaeacces-is-returned), 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/.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:`

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](https://www.rfc-editor.org/rfc/rfc8252.html#section-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](https://www.rfc-editor.org/rfc/rfc7591.html)). 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 `.json` registration and `.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 (#3393, #3039 headless/SSH browser; #3838 reauth not attached). I did not find one covering a Windows excluded-port listener bind failure.

Contributor guide

Open the contributing guide

Research direction

Start at the `/mcp` re-authentication flow and the cached files under `~/.copilot/mcp-oauth-config/`, then reproduce on Windows using `netsh interface ipv4 show excludedportrange protocol=tcp`. Trace dynamic registration and loopback listener binding, including diagnostic logging. Done means re-authentication recovers from an excluded or occupied cached port and the bind failure is persisted with its server, address, port, and error code.

Written by the indexing model from the issue text.

Assessment

Domain
authentication, cli, networking, operating-systems
Issue type
Bug
Difficulty
4/5
Estimated time
3-5 days
Activity status
Quiet
Clarity
Mostly clear
Newbie friendliness
52/100

Get new issues in your inbox

A short digest of beginner-friendly GitHub issues.