Skip to content

MCP OAuth (Copilot Desktop app): non-first-party HTTP servers cancel host-token then never launch the "runtime browser flow" they deferred to — no auth, no error, no popup #4017

Open

Description

@admatt01

Describe the bug
In the GitHub Copilot Desktop app, remote MCP servers configured with "type": "http" that are non-first-party (e.g. Atlassian, incident.io) fail to authenticate. Toggling the server off/on in Settings produces no browser popup and no visible error — the server simply never connects.

Affected version
Desktop app shell: v1.0.15 (latest, confirmed via in-app update check)
Bundled Copilot CLI engine: v1.0.69-0 (AppData\Local\github-copilot-sdk\cli\1.0.69-0\copilot.exe)

Steps to reproduce
Add a non-first-party remote MCP server with "type": "http" and a url in mcp-config.json (e.g. https://mcp.atlassian.com/v1/mcp/authv2)
Enable it via Settings → MCP servers in the Desktop app
Toggle it off, then on again
Observe: no browser window ever opens; the server stays disconnected with no user-facing error

Expected behavior
A browser window should open for OAuth consent, as it does for the raw copilot CLI and for local-type servers wrapped in npx mcp-remote.

Actual behavior
~/.copilot/logs/github-app..log shows this single log line and nothing further:

github_app::session::manager::mcp_status: MCP OAuth non-first-party server; cancelling host-token so runtime browser flow runs server_name=


This line repeats on every toggle attempt (confirmed 43 occurrences across multiple sessions for both atlassian and incident.io), but there is no subsequent log entry anywhere indicating the "runtime browser flow" it defers to is ever actually invoked — no browser-launch attempt, no OAuth URL, no error surfaced to the user.

## Root cause hypothesis
This behavior appears tied to the MCP host token-injection OAuth rework shipped in copilot-cli v1.0.66 (2026-06-30 changelog entry: "Added runtime telemetry for the MCP host token-injection OAuth flow, recording when an OAuth broadcast is emitted to the host and how the host responds (token or cancelled)"). For non-first-party servers, the host correctly identifies it should defer to a client-side browser flow and cancels its own token injection — but that deferred browser flow is never triggered inside the Desktop app shell.

## Workaround
Reconfiguring the server as a local type wrapping npx mcp-remote bypasses the broken Desktop-app host/browser-flow handoff entirely, since mcp-remote runs its own independent OAuth implementation:

```json
"atlassian": {
  "type": "local",
  "command": "npx",
  "args": ["--yes", "mcp-remote", "https://mcp.atlassian.com/v1/mcp"]
}

This works reliably. Native "type": "http" does not, for any non-first-party server, in the Desktop app.

Additional context

Possibly related to #2536, but distinct: that issue is about the raw copilot CLI's "Opening browser for X authentication..." log line firing and a browser genuinely opening, just re-registering (DCR) on every launch. In this Desktop-app case, the browser-flow deferral log line fires but no browser-launch attempt is ever logged afterward — a different failure point entirely (host→client handoff, not CLI OAuth caching).

Activity

  1. added
    area:authenticationLogin, OAuth, device auth, token management, and keychain integration
    area:mcpMCP server configuration, discovery, connectivity, OAuth, policy, and registry
    and removed on Jul 3, 2026
  2. m13v commented on Jul 5, 2026

    @m13v

    the config-level checks all pass here: type http, valid url, server toggles on. none of that surfaces the first-party/non-first-party classification the host applies at runtime, and that flag is the real gate. your server is eligible to register but not eligible for the host to hand off its token, so it cancels and defers to a browser flow the desktop shell never actually invokes. validating mcp-config.json will never catch it because the decision lives in the host handoff, not the config text.

  3. added
    triagedThis issue has been added to our internal issue tracker
    on Jul 15, 2026
  4. gokhanarkan commented on Jul 15, 2026

    @gokhanarkan
    Member

    Triage summary

    Runtime PR 11934 changed host cancellation from failed to needs-auth, and PR 11761 replatformed OAuth callbacks. Those changes do not prove an embedded app toggle launches the runtime browser flow.

    Please verify direct CLI and embedded app separately on a containing build.

    Route: needs-follow-up.

    This triage summary was prepared with GitHub Copilot and reviewed before posting.

  5. m13v commented on Sep 7, 2026

    @m13v

    needs-auth is the swallow. PR 11934 renaming host cancellation from failed to needs-auth is exactly what turns a loud, retryable failure into a quiet terminal state: failed surfaces or retries, needs-auth just parks on a browser flow the shell never fires. so the toggle reports settled with no error, which is the reported bug, not evidence the embedded flow launches.

  6. patniko commented on Oct 26, 2026

    @patniko
    Collaborator

    Thanks for the report. A fix that we believe addresses this has been merged and will ship in the next release (v1.0.104, currently in pre-release). Once it's out, could you retest and let us know whether you still see the problem? If we don't hear back after the release, we may close this issue — you can always reopen it.

    Triaged as part of a Copilot SDK backlog review, with help from GitHub Copilot.

  7. davidmatson commented on Oct 28, 2026

    @davidmatson

    Additional repro: same symptom with the Copilot app hosting sessions in WSL (app UI on Windows, sessions run by the app's WSL host).

    Environment

    • GitHub Copilot app 1.1.32 (Windows), sessions on a WSL host (Ubuntu 24.04)
    • Bundled CLI 1.0.100-1. A standalone copilot in the same WSL distro (1.0.105-1) behaves the same.
    • Servers: the Azure DevOps remote MCP (https://mcp.dev.azure.com/<org>) and an internal Entra-protected Streamable-HTTP MCP server. Both advertise Entra through /.well-known/oauth-protected-resource.

    What happens

    • At startup each server logs a 401 challenge, then the sign-in fails or is cancelled:
      [WARNING] [rust:mcp_engine::agent_host] HTTP 401 challenge (WWW-Authenticate: …"https://mcp.dev.azure.com/.well-known/oauth-protected-resource/<org>") {"server":"ado","error":"MCP OAuth authorization failed"}
      [ERROR] [rust:rmcp::transport::worker] worker quit with fatal: Transport channel closed, when Client(OAuthChallenge { ... })
      [WARNING] [rust:mcp_engine::agent_host] HTTP 401 challenge (...) {"server":"ado","error":"MCP authorization lifetime was replaced"}
      
    • No browser opens. If I copy the authorize URL by hand, the browser flow (VS Code client id aebc6443-996d-45c2-90f0-388ff96faa56, redirect https://vscode.dev/redirect → http://127.0.0.1:<fixed port>) reports success. The callback reaches the WSL process (WSL localhost forwarding works), but no token is ever stored and the server stays at 401.
    • With --log-level debug, after /mcp auth <server> the CLI contacts login.microsoftonline.com about every 3 s until exit, with no success and no visible error.
    • /mcp auth aborts the sign-ins that started automatically at launch ("OAuth flow was aborted"). Several servers share one auth server and one fixed callback port, which may be related to macOS: Entra-broker MCP servers fail at startup: concurrent sign-ins rejected ("sign-in is already in progress") and rejected cached token not recovered #5250.
    • Device code (Ctrl+G) hangs at "Connecting to ..." with no device-code activity in the log.
    • There is no OS broker in WSL, so the Windows (WAM) path from 1.0.81 doesn't apply.

    Evidence that the problem is in the client sign-in flow, not the server or network

    • az account get-access-token --resource <server resource> gives a token that both servers accept (initialize → HTTP 200, tools/list and tools/call work).
    • Workaround: a small local stdio MCP proxy that forwards JSON-RPC to the remote URL, adding an Authorization bearer header from az account get-access-token (refreshed near expiry). It's registered as a local server in ~/.copilot/mcp-config.json. All servers then work in app sessions, and the CLI logs no further 401 challenges. Same approach as the mcp-remote workaround above.

    Side note for anyone writing a similar proxy: Python's urllib capitalizes header names (x-custom-header → X-custom-header). Some servers match custom headers case-sensitively and silently ignore them. Use http.client to send header names exactly as written.
    Ask: a non-broker Entra path that completes in WSL/Linux app hosts. Either finish the browser flow and persist the token, or allow a token-command / headersHelper-style hook so a CLI credential such as az can supply the bearer token.

    — Copilot (AI assistant using GitHub Copilot app)

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

    area:authenticationLogin, OAuth, device auth, token management, and keychain integrationarea:mcpMCP server configuration, discovery, connectivity, OAuth, policy, and registrymore-info-neededtriagedThis issue has been added to our internal issue tracker

    Type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions