OAuth redirect URI port mismatch breaks login to most MCP servers
Summary
copilot CLI publishes a CIMD (Client ID Metadata Document) that declares a
single loopback redirect URI on a fixed port, but at runtime the CLI binds an
ephemeral loopback port and sends that port in the OAuth redirect_uri
parameter. GitHub's own OAuth server accepts the mismatch (per RFC 8252 §7.3
loopback matching), but many third-party OAuth servers — including
Entra/Azure-fronted MCP servers — do strict string matching against the
registered redirect_uris and reject the request.
The result is that authentication fails against almost every non-GitHub MCP
server users try to connect. Claude Code and Codex CLI are not affected in
the same environments.
Environment
- CLI version:
GitHub Copilot CLI 1.0.88
- OS: macOS (arm64)
- Install: Homebrew cask
copilot-cli (SEA binary)
Reproducer
- Configure any MCP server whose OAuth authorize endpoint validates
redirect_uri by exact match against the CIMD's redirect_uris list.
(Entra/Azure-fronted services typically behave this way.)
- Trigger the OAuth flow from Copilot CLI:
/mcp add <server-url> (or however the server is registered), then log in.
- The browser lands on the authorize endpoint, which returns:
{
"error": "invalid_request",
"error_description": "Redirect URI 'http://127.0.0.1:52752/' does not match CIMD redirect_uris.",
"state": "..."
}
The port (52752 above) changes on every attempt.
Expected behavior
The redirect_uri sent to the authorize endpoint should either:
- (a) match the port published in the CIMD, i.e.
http://127.0.0.1:33418/, so
strict-matching servers accept it; or
- (b) rely on RFC 8252 §7.3 loopback matching and have the CIMD advertise
http://127.0.0.1/ (no port), making clear to server implementers that
port-agnostic matching is expected.
Today it does neither: it publishes a specific port and then uses a different
one.
Actual behavior / root cause
The CLI's published metadata at
https://github.com/copilot/cli/client-metadata.json:
{
"client_id": "https://github.com/copilot/cli/client-metadata.json",
"client_name": "GitHub Copilot CLI",
"application_type": "native",
"redirect_uris": ["http://127.0.0.1:33418/"],
...
}
But the CLI's OAuth loopback listener is bound with port 0 (verifiable by
inspecting the SEA blob inside the shipped binary):
// Port 0 = let the OS pick a free ephemeral port. Bind to loopback only.
await new Promise((resolve) => server.listen(0, "127.0.0.1", resolve));
const address = server.address();
const port = typeof address === "object" && address ? address.port : 0;
The dynamically-assigned port then flows into the OAuth redirect_uri, which
never equals 33418.
Impact
- Blocks Copilot CLI from authenticating against most third-party MCP servers,
which is a large and growing set as MCP adoption widens.
- Internal reports at multiple organisations indicate the same failure pattern
across every non-GitHub MCP they've tried, with Claude Code / Codex CLI
unaffected in the same environments.
- Users have no clean local workaround. The one lever we found —
COPILOT_MCP_CIMD_CLIENT_ID_URL overriding the CIMD document URL — requires
self-hosting a metadata file that enumerates every ephemeral loopback port,
which is fragile and requires the MCP server to dynamically trust arbitrary
CIMDs.
Suggested fixes (either is sufficient)
- Bind the published port. Change the listener to
server.listen(33418, "127.0.0.1", …) with a fallback strategy (retry with
an ephemeral port only if 33418 is in use, and clearly document the
fallback), so the common case matches the CIMD exactly.
- Publish port-agnostic CIMD. Change
redirect_uris in
client-metadata.json to ["http://127.0.0.1/"] and rely on the RFC 8252
§7.3 loopback rule. This shifts the responsibility to non-conformant OAuth
servers, which is arguably correct, but will not fix real-world usage until
those servers are updated.
Option 1 is the most defensive; option 2 is the most spec-pure. A combination
(bind 33418 first, publish 127.0.0.1/) would maximise interoperability.
References
- RFC 8252 §7.3 (Loopback Interface Redirection): loopback redirects SHOULD be
matched ignoring the port component.
- OAuth 2.0 for Native Apps client identity metadata document (CIMD) spec.
Drafted with GitHub Copilot CLI.
OAuth redirect URI port mismatch breaks login to most MCP servers
Summary
copilotCLI publishes a CIMD (Client ID Metadata Document) that declares asingle loopback redirect URI on a fixed port, but at runtime the CLI binds an
ephemeral loopback port and sends that port in the OAuth
redirect_uriparameter. GitHub's own OAuth server accepts the mismatch (per RFC 8252 §7.3
loopback matching), but many third-party OAuth servers — including
Entra/Azure-fronted MCP servers — do strict string matching against the
registered
redirect_urisand reject the request.The result is that authentication fails against almost every non-GitHub MCP
server users try to connect. Claude Code and Codex CLI are not affected in
the same environments.
Environment
GitHub Copilot CLI 1.0.88copilot-cli(SEA binary)Reproducer
redirect_uriby exact match against the CIMD'sredirect_urislist.(Entra/Azure-fronted services typically behave this way.)
/mcp add <server-url>(or however the server is registered), then log in.The port (
52752above) changes on every attempt.Expected behavior
The
redirect_urisent to the authorize endpoint should either:http://127.0.0.1:33418/, sostrict-matching servers accept it; or
http://127.0.0.1/(no port), making clear to server implementers thatport-agnostic matching is expected.
Today it does neither: it publishes a specific port and then uses a different
one.
Actual behavior / root cause
The CLI's published metadata at
https://github.com/copilot/cli/client-metadata.json:{ "client_id": "https://github.com/copilot/cli/client-metadata.json", "client_name": "GitHub Copilot CLI", "application_type": "native", "redirect_uris": ["http://127.0.0.1:33418/"], ... }But the CLI's OAuth loopback listener is bound with port 0 (verifiable by
inspecting the SEA blob inside the shipped binary):
The dynamically-assigned port then flows into the OAuth
redirect_uri, whichnever equals
33418.Impact
which is a large and growing set as MCP adoption widens.
across every non-GitHub MCP they've tried, with Claude Code / Codex CLI
unaffected in the same environments.
COPILOT_MCP_CIMD_CLIENT_ID_URLoverriding the CIMD document URL — requiresself-hosting a metadata file that enumerates every ephemeral loopback port,
which is fragile and requires the MCP server to dynamically trust arbitrary
CIMDs.
Suggested fixes (either is sufficient)
server.listen(33418, "127.0.0.1", …)with a fallback strategy (retry withan ephemeral port only if 33418 is in use, and clearly document the
fallback), so the common case matches the CIMD exactly.
redirect_urisinclient-metadata.jsonto["http://127.0.0.1/"]and rely on the RFC 8252§7.3 loopback rule. This shifts the responsibility to non-conformant OAuth
servers, which is arguably correct, but will not fix real-world usage until
those servers are updated.
Option 1 is the most defensive; option 2 is the most spec-pure. A combination
(bind 33418 first, publish
127.0.0.1/) would maximise interoperability.References
matched ignoring the port component.
Drafted with GitHub Copilot CLI.