Ran into this with a hosted MCP server whose authorization server isn't at the origin root — token endpoint is https://host/oauth2/api/v1/token, not https://host/token.
If a client starts up with a cached-but-expired access token (+ refresh token) and hasn't done discovery yet, async_auth_flow refreshes at the very top — before any 401/metadata discovery. So oauth_metadata is None and _refresh_token uses the fallback urljoin(get_authorization_base_url(server_url), "/token"), i.e. just {scheme}://{netloc}/token. That 404s, _handle_refresh_response clears the tokens, and the flow drops to full interactive auth — which a headless/gateway client can't do. So the server silently disconnects every time the access token expires (mine are 5 min, so… constantly).
Same path-stripping fallback is in _get_token_endpoint, _perform_authorization_code_grant (/authorize) and DCR (/register) — refresh is just the one that bites silently.
Repro (roughly):
- MCP server whose AS metadata puts
token_endpoint under a path, not {origin}/token
- log in normally so tokens get cached
- let the access token expire (or clear the expiry), reconnect with a fresh provider
- watch the refresh POST go to
https://host/token → 404 → "Token refresh failed" → tokens cleared → it tries to open a browser
Fix looks like: discover metadata before the eager refresh (or stop dropping the issuer path in the fallback). Happy to PR — have a branch that pulls the PRM/ASM discovery out of the 401 branch and runs it before the refresh.
(used some AI help digging into this)
Ran into this with a hosted MCP server whose authorization server isn't at the origin root — token endpoint is
https://host/oauth2/api/v1/token, nothttps://host/token.If a client starts up with a cached-but-expired access token (+ refresh token) and hasn't done discovery yet,
async_auth_flowrefreshes at the very top — before any 401/metadata discovery. Sooauth_metadata is Noneand_refresh_tokenuses the fallbackurljoin(get_authorization_base_url(server_url), "/token"), i.e. just{scheme}://{netloc}/token. That 404s,_handle_refresh_responseclears the tokens, and the flow drops to full interactive auth — which a headless/gateway client can't do. So the server silently disconnects every time the access token expires (mine are 5 min, so… constantly).Same path-stripping fallback is in
_get_token_endpoint,_perform_authorization_code_grant(/authorize) and DCR (/register) — refresh is just the one that bites silently.Repro (roughly):
token_endpointunder a path, not{origin}/tokenhttps://host/token→ 404 → "Token refresh failed" → tokens cleared → it tries to open a browserFix looks like: discover metadata before the eager refresh (or stop dropping the issuer path in the fallback). Happy to PR — have a branch that pulls the PRM/ASM discovery out of the 401 branch and runs it before the refresh.
(used some AI help digging into this)