Skip to content

MCP OAuth: pre-registered (static) client's secret isn't sent at token exchange → invalid_client #5351

Closed as not planned

Description

@t1agob

Describe the bug

With a pre-registered (static) confidential OAuth client, Copilot CLI completes the browser authorization step, but the authorization server rejects the code-for-token exchange:

Authentication failed: MCPOAuthError: Token exchange failed: Server returned error response: invalid_client: Bad credentials

The client ID and secret are correct. They work in Claude Code, VS Code and others.

Affected version

GitHub Copilot CLI 1.0.105 (macOS 26.6.2, arm64)

Steps to reproduce the behavior

  1. Use a remote HTTP MCP server whose authorization server doesn't support dynamic client registration, and register a confidential OAuth client (client ID + secret) for it. We used https://mcp.pagerduty.com/mcp, with authorization server https://app.pagerduty.com/global/oauth/anonymous.
  2. Add the server, either in /mcp or from a plugin's .mcp.json:
    { "mcpServers": { "pagerduty": { "type": "http", "url": "https://mcp.pagerduty.com/mcp" } } }
  3. Connect it in /mcp. The CLI logs Client registration failed: Authorization server does not support dynamic client registration and shows the client credentials form.
  4. Enter the client ID, press Tab, and enter the client secret (not marked as a public client).
  5. Register the redirect URI shown in the browser's redirect_uri on the OAuth app (see the port note below), then approve in the browser.
  6. The token exchange fails with invalid_client: Bad credentials.
  7. ~/.copilot/mcp-oauth-config/<hash>.json contains the client ID but no clientSecret:
    {"serverUrl":"https://mcp.pagerduty.com/mcp","authorizationServerUrl":"https://app.pagerduty.com/global/oauth/anonymous","clientId":"<redacted>","redirectUri":"http://127.0.0.1:62372/","resourceUrl":"https://mcp.pagerduty.com/mcp","isStatic":true,"httpsRedirect":null}

Reproduced 3 times, both with a plugin-provided server and with one added by hand in /mcp. The token exchange error only appears in the UI, not in ~/.copilot/logs/*.log.

Expected behavior

For a static confidential client, the CLI should store the client secret and send it at the token exchange, using client_secret_basic or client_secret_post as the server's token_endpoint_auth_methods_supported advertises.

Additional context

Redirect port: the loopback callback uses a random port on every attempt (53676, 54429, 55205, 62372...), so a pre-registered client can't have a stable redirect URI on servers that require an exact match. The config schema has auth.redirectPort (McpServerAuthConfigRedirectPort), but setting "auth": {"redirectPort": 8080} in a plugin's .mcp.json has no effect: the port stays random. Please honor redirectPort for plugin-provided servers and document it.

Comparison: VS Code, with the same server, OAuth app, client ID and secret, succeeds once its redirect port is registered.

Activity

  1. t1agob commented on Nov 4, 2026

    @t1agob
    Author

    Closing: further testing shows the same invalid_client: Bad credentials at the token exchange from VS Code and Cursor too (Cursor uses a fixed redirect URI), with the same OAuth app and a freshly regenerated secret. The cause appears to be on the authorization server side, not in Copilot CLI. I'll reopen if that's ruled out.

    The auth.redirectPort point (ignored for plugin-provided servers) still stands; happy to file that separately if useful.

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

    No labels
    No labels

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions