github / github/copilot-sdk

Python: no way to answer mcp.headers_refresh_required, so MCP auth headers are fixed for the life of a session

Aperta
#2,492 0 commenti 0 reazioni 0 assegnatari Vedi su GitHub
Lingua principale
Java
Stelle
10.5k
Fork
1.5k
Merge medio
1g 11h
PR unite (30g)
127

Descrizione

**Package:** `github-copilot-sdk` (Python). Checked against **1.0.9** and **1.0.11**.

### Summary

For MCP servers behind Entra (workload identity), the `Authorization` header is a
bearer token with a finite lifetime — ours lasts ~24h. `create_session(mcp_servers=...)`
resolves each server's `headers` once, when the session is created, and the Python
SDK exposes no way to replace them afterwards. Any host process outliving its token
permanently loses those servers.

The wire protocol already models exactly this. What is missing is the Python binding.

### The protocol has it

`copilot/generated/session_events.py`:

```python
MCP_HEADERS_REFRESH_REQUIRED = "mcp.headers_refresh_required"
MCP_HEADERS_REFRESH_COMPLETED = "mcp.headers_refresh_completed"
```

`copilot/generated/rpc.py`:

```python
class MCPHeadersHandlePendingHeadersRefreshRequest:
"""Host response: supply dynamic headers or decline this refresh."""
kind: MCPHeadersHandlePendingHeadersRefreshRequestKind # "headers" | "none"
headers: dict[str, str] | None = None
"""Headers to overlay onto the MCP request. Dynamic headers override static
config headers but do not replace SDK-managed request headers."""
```

…plus the `mcp_headers_handle_pending_headers_refresh_request` method.

### The bindings don't

Nothing outside `generated/` references it, in either version:

```console
$ grep -rn "headers_refresh" --include=*.py . | grep -v generated/rpc.py
./generated/session_events.py:203: MCP_HEADERS_REFRESH_REQUIRED = "mcp.headers_refresh_required"
./generated/session_events.py:204: MCP_HEADERS_REFRESH_COMPLETED = "mcp.headers_refresh_completed"
```

Contrast `on_mcp_auth_request`, which *is* wired (`client.py:2155`, and
`_register_mcp_auth_handler` at the session). So a host can answer an OAuth request
but cannot answer a headers refresh.

### Ask

Expose a handler for the pending headers refresh, e.g.
`create_session(..., on_mcp_headers_refresh_request=...)` returning
`{"kind": "headers", "headers": {...}}`, registered the same way
`on_mcp_auth_request` already is.

---

### Why `on_mcp_auth_request` is not a workaround for us

#1669 added the OAuth host-token handlers and covers "refresh/replacement tokens,
upscope, reauth", so this looks like the intended path. We cannot reach it, because
the handler is never called for our servers.

Our servers answer an unauthenticated request with a **relative** resource_metadata
reference:

```console
POST /mcp -> 401
WWW-Authenticate: Bearer resource_metadata="/.well-known/oauth-protected-resource"
```

The CLI appears to fetch that verbatim rather than resolving it against the MCP
request URL, and then abandons the transport (`~/.copilot/logs/process-*.log`):

```
[ERROR] Failed to connect with OAuth in background: Error: Failed to fetch MCP
OAuth protected-resource metadata from /.well-known/oauth-protected-resource
[ERROR] worker quit with fatal: Transport channel closed, when Client(OAuthChallenge { ...401... })
```

Resolved against the request URL (`http://:8000/mcp`, per RFC 3986) the document
is served and valid:

```console
GET http://:8000/.well-known/oauth-protected-resource -> 200
{"resource":"api://","authorization_servers":["https://login.microsoftonline.com//v2.0"],"scopes_supported":[...]}
```

Observed with a deliberately invalid token in `headers` **and** `auth: True` with a
handler registered:

```
EVENT session.mcp_server_status_changed gis pending
EVENT session.mcp_servers_loaded
EVENT session.mcp_server_status_changed gis needs-auth
EVENT session.mcp_server_status_changed gis failed
REPLY:
STATUS: {'gis': 'failed'} | auth callback fired: 0 time(s)
```

Two things would each independently help, and may deserve their own issue — happy to
split this out if you'd prefer:

1. Resolve a relative `resource_metadata` URI reference against the MCP request URL.
2. Fall back to the registered host auth handler when protected-resource discovery
fails, instead of tearing down the transport.

### Why this is easy to miss

The failure is entirely silent. No exception, no callback, no failed turn — the
server just goes `failed` and the model answers without those tools, reporting them
as not existing rather than as having gone away. Nothing in the symptom leads back
to the cause.

### Workaround, for anyone else hitting this

Send the token in the connection `headers` so no challenge is ever issued, and check
server status every turn so expiry is at least visible. That trades a silent failure
for a bounded one, but the session still cannot outlive its token.

Guida per i contributori

Apri la guida per i contributori

Direzione di ricerca

Start in copilot/client.py around line 2155 and _register_mcp_auth_handler, then inspect generated/rpc.py for mcp_headers_handle_pending_headers_refresh_request and generated/session_events.py for the refresh event names. Trace how on_mcp_auth_request is exposed and registered, then confirm the new handler accepts headers or none and is invoked for pending refreshes.

Scritto dal modello di indicizzazione a partire dal testo della issue.

Valutazione

Stack tecnologico
python
Ambito
api, authentication
Tipo di issue
Funzionalità
Difficoltà
3/5
Tempo stimato
1-2 giorni
Stato di attività
Attiva
Chiarezza
Specificata chiaramente
Idoneità per principianti
72/100

Ricevi le nuove issue nella tua casella

Un breve riepilogo di issue GitHub adatte ai principianti.