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:
MCP_HEADERS_REFRESH_REQUIRED = "mcp.headers_refresh_required"
MCP_HEADERS_REFRESH_COMPLETED = "mcp.headers_refresh_completed"
copilot/generated/rpc.py:
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:
$ 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:
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://<host>:8000/mcp, per RFC 3986) the document
is served and valid:
GET http://<host>:8000/.well-known/oauth-protected-resource -> 200
{"resource":"api://<guid>","authorization_servers":["https://login.microsoftonline.com/<tenant>/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: <model answers using only built-in tools>
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:
- Resolve a relative
resource_metadata URI reference against the MCP request URL.
- 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.
Package:
github-copilot-sdk(Python). Checked against 1.0.9 and 1.0.11.Summary
For MCP servers behind Entra (workload identity), the
Authorizationheader is abearer token with a finite lifetime — ours lasts ~24h.
create_session(mcp_servers=...)resolves each server's
headersonce, when the session is created, and the PythonSDK 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:copilot/generated/rpc.py:…plus the
mcp_headers_handle_pending_headers_refresh_requestmethod.The bindings don't
Nothing outside
generated/references it, in either version:Contrast
on_mcp_auth_request, which is wired (client.py:2155, and_register_mcp_auth_handlerat the session). So a host can answer an OAuth requestbut 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 wayon_mcp_auth_requestalready is.Why
on_mcp_auth_requestis 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:
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):Resolved against the request URL (
http://<host>:8000/mcp, per RFC 3986) the documentis served and valid:
Observed with a deliberately invalid token in
headersandauth: Truewith ahandler registered:
Two things would each independently help, and may deserve their own issue — happy to
split this out if you'd prefer:
resource_metadataURI reference against the MCP request URL.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
failedand the model answers without those tools, reporting themas 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
headersso no challenge is ever issued, and checkserver 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.