github / github/copilot-cli

Remote MCP (OAuth): expired access token forces interactive re-auth instead of silent refresh_token grant

Offen
#4,203 1 Kommentar 3 Reaktionen 0 zugewiesene Personen Auf GitHub ansehen
area:authentication area:mcp
Vorherrschende Sprache
Shell
Sterne
11.2k
Forks
1.9k
Ø Merge
14 Std. 16 Min.
Gemergte PRs (30 T.)
6

Beschreibung

## Summary

When a remote OAuth-protected MCP server's cached **access token** is expired at session start, the Copilot CLI treats the server as needing a fresh **interactive** OAuth login and drops its tools — even though a valid **refresh token** is cached. It never attempts an [RFC 6749 §6](https://www.rfc-editor.org/rfc/rfc6749#section-6) `refresh_token` grant. In non-interactive/automation sessions the interactive flow can't complete, so the server's tools silently never load.

## Environment

- GitHub Copilot CLI `1.0.71`, running inside VS Code
- macOS (Darwin); credential store = Apple Keychain (`copilot-mcp-oauth`)
- MCP transport: `rmcp` streamable HTTP (`rmcp::transport::worker` / `::streamable`), `reqwest 0.13.4`

## Configuration

- Remote HTTP MCP server in `~/.copilot/mcp-config.json`:
```json
"notion": { "type": "http", "url": "https://mcp.notion.com/mcp" }
```
- Cached grant in Keychain (`service copilot-mcp-oauth`, account ``) and `~/.copilot/mcp-oauth-config/.tokens.json` — contains `accessToken`, `expiresAt`, `refreshToken`.
- Server authorization-server metadata ([RFC 8414](https://www.rfc-editor.org/rfc/rfc8414)) advertises refresh support:
`token_endpoint: https://mcp.notion.com/token`, `grant_types_supported: ["authorization_code", "refresh_token"]`

## Steps to reproduce

1. Authenticate a remote OAuth MCP server (e.g. Notion) so a valid access+refresh grant is cached.
2. Let the **access** token expire while the **refresh** token stays valid (Notion access tokens last ~8h — e.g. leave it overnight).
3. Start a new Copilot CLI session (especially a non-interactive/scheduled one).
4. Observe: the server's tools are absent from the session.

## Observed behavior

From `~/.copilot/logs/process-<...>.log` (redacted):

```
[ERROR] Starting remote MCP client for notion with url: https://mcp.notion.com/mcp
[ERROR] Connecting MCP client for notion...
-> connects to mcp.notion.com:443 using the cached (expired) access token
[ERROR] [rust:rmcp::transport::worker] worker quit with fatal: Transport channel closed,
when Client(OAuthChallenge { www_authenticate_header:
"...OAuth, resource_metadata=\".../.well-known/oauth-protected-resource/mcp\",
error=\"invalid_token\", error_description=\"Mis...\"" })
[ERROR] Server notion requires authentication, initiating OAuth flow
[ERROR] OAuth authentication required for notion
```

The runtime then reads the cached OAuth entry from Keychain but still ends at **"OAuth authentication required."**

### Evidence that no refresh was attempted

Grep of the entire session-start process log:

```
POSTs to mcp.notion.com/token : 0
```

Zero `refresh_token` grants were sent, despite a valid refresh token being cached.

## Expected behavior

On a `401` / `WWW-Authenticate: … error="invalid_token"` for a server that has a cached refresh token:

1. Silently POST `grant_type=refresh_token` (+ `client_id`) to the discovered `token_endpoint`.
2. Persist the rotated access/refresh tokens.
3. Reconnect and load the server's tools.
4. Only fall back to the interactive OAuth flow if the refresh itself fails (`invalid_grant`, expired/revoked).

## Impact

- Any remote OAuth MCP server silently loses its tools once the access token expires, until a manual re-auth — even though the refresh token is valid and the whole point of storing it is to avoid exactly this.
- Breaks unattended/scheduled sessions (e.g. a daily automation), where the interactive prompt can never be completed.

## Likely root cause / where to look

- The `rmcp` streamable transport surfaces the OAuth challenge as a **fatal** transport error ("worker quit with fatal … `OAuthChallenge`"), and the CLI's handler maps "auth required" directly to the interactive flow without first attempting a token refresh.
- Secondary: the session's tool list appears to be snapshotted at startup, so even a later successful (re)auth doesn't surface the tools into an already-running session.

## Suggested fix

- Insert a refresh step in the auth-challenge path: if a `refresh_token` is cached for the server, perform the refresh grant against the discovered `token_endpoint`, persist rotated tokens, and reconnect **before** considering interactive auth.
- Optionally refresh **proactively** when `expiresAt` is at/near expiry before the first connect.
- Consider re-syncing tools into active sessions after a late auth success.

## Workaround

- Manually perform the `refresh_token` grant against the `token_endpoint`, write the rotated tokens back into the cache; or re-run the CLI's interactive auth for the server before starting the session.

---

cc @connor4312 (VS Code team)

Filed with assistance from GitHub Copilot CLI.

Beitragsleitfaden

Beitragsleitfaden öffnen

Rechercherichtung

Beginne mit dem OAuthChallenge-Pfad des rmcp streamable HTTP transport und dem CLI-Handler, der "OAuth authentication required." protokolliert. Prüfe die zwischengespeicherten Tokens in ~/.copilot/mcp-oauth-config/.tokens.json und den ermittelten token_endpoint; verifiziere anschließend, dass ein Refresh-Grant rotierte Tokens persistiert und die Verbindung zum Server wiederherstellt. Als erledigt gilt die Aufgabe, wenn abgelaufene Access-Tokens stillschweigend aktualisiert werden und die Tools des Servers ohne interaktive Anmeldung geladen werden.

Vom Indexierungsmodell aus dem Issue-Text verfasst.

Bewertung

Tech-Stack
rust
Bereich
authentication, cli
Issue-Typ
Bug
Schwierigkeit
4/5
Geschätzter Aufwand
3-5 Tage
Aktivitätsstatus
Aktiv
Klarheit
Größtenteils klar
Anfängerfreundlichkeit
48/100

Neue Issues direkt in Ihr Postfach

Eine kurze Übersicht über anfängerfreundliche GitHub-Issues.