[BUG] MCP OAuth: Claude doesn't auto-refresh access tokens, daily "Connection expired" despite valid refresh token
Preflight Checklist
- [x] I have searched existing issues and this hasn't been reported yet
- [x] This is a single bug report (please file separate reports for different bugs)
- [x] I am using the latest version of Claude Code
What's Wrong?
Custom HTTP MCP server (Azure Function App) using Entra OAuth 2.0 + PKCE shows "Connection expired" daily in Claude.ai web and Claude Desktop, despite the refresh token being valid and the user's Entra session being within its 180-day sign-in frequency window.
When the user clicks Connect, Claude exchanges the stored refresh token at Entra's /token endpoint silently and reconnects in ~200ms — proving the refresh path itself works correctly. Claude just doesn't call it automatically when the access token expires; it falls through to the "needs reconnect" UI immediately.
This matches the diagnosis in closed issue #43789 by @nwoonet:
The encrypted token cache is populated and persisted across sessions, so refresh tokens are being stored. The client just isn't exercising the refresh endpoint when the access token expires — it falls straight through to the bootstrap auth tools.
Related closed/duplicate issues: #43789, #28262, #5706, #12447, #55055, #53803, #54742, #47219.
Evidence from Entra sign-in logs when user clicks Connect after seeing "Connection expired":
additionalDetails:"MFA requirement satisfied by claim in the token"— Entra's signature of a successful silent refresh-token exchangestatus.errorCode:0conditionalAccessStatus:successappliedConditionalAccessPolicies:[](no challenge required)
This proves the refresh exchange itself succeeds instantly when invoked — Claude is just not invoking it without a manual user trigger.
Impact: every OAuth-based MCP connector in our org sidebar (custom MCPs + Anthropic-first-party Atlassian, Microsoft 365, Google Drive) shows "Connection expired" daily. Multi-step automations stall when access tokens expire mid-workflow. The only workaround is the community mcp-stdio local bridge, which requires per-laptop install.
Cross-platform — not Windows-specific. Confirmed by multiple colleagues at our org on both Windows 11 and macOS. Matches @nwoonet's observation in #43789: "Confirming this on macOS 26 / Claude Code 2.1.122. The issue is not Windows-specific."
What Should Happen?
When the access token expires (or returns 401 from the MCP server), Claude should:
- Detect the expiry
- POST to the IdP's
/tokenendpoint with the cached refresh token - Receive a new access token (no user interaction required)
- Retry the original MCP call transparently
- Surface no UI to the user
Only fall back to the "Connection expired / reconnect" UI when the refresh attempt itself fails (e.g. refresh token is also expired or revoked).
This is standard OAuth 2.0 client behavior (RFC 6749 §6) — auto-refresh is what makes long-running OAuth sessions usable. Most OAuth-aware HTTP clients (Microsoft.Identity.Web, MSAL, Google's auth libs, etc.) handle this automatically.
Error Messages/Logs
UI banner shown daily:
"Connection has expired. You can reconnect to re-authenticate."
No actual error logs are produced by Claude — the failure is silent (Claude flips state to expired without logging a 401 from a refresh attempt).
Entra sign-in log entry observed when user clicks Connect to recover (proves refresh works when invoked):
createdDateTime: 2026-06-01T13:18:50Z
status.errorCode: 0
status.additionalDetails: "MFA requirement satisfied by claim in the token"
conditionalAccessStatus: success
appliedConditionalAccessPolicies: []
The "MFA satisfied by claim in token" detail is Entra's signature of a silent refresh-token exchange. This succeeded in ~200ms with no user prompt. The same exchange should happen automatically on access token expiry, before showing the "Connection expired" UI.
Steps to Reproduce
- Set up a custom HTTP MCP server that requires OAuth (Authorization Code + PKCE)
against any IdP that follows standard OAuth 2.0. We used Microsoft Entra,
but the issue is IdP-agnostic per other reports in #43789 (which mentions
Sentry, Atlassian, Superhuman Mail, Vercel, etc.)
- Add the MCP server as a custom connector in Claude.ai (web) and/or
Claude Desktop. Provide OAuth Client ID + Secret.
- Click "Connect". OAuth flow completes successfully. Connector shows
as "Connected". Claude.ai stores: access_token (~1h lifetime) +
refresh_token (typically days to months).
- Use the MCP server normally, invoke its tools from a chat. Works fine.
- Wait for the access_token to expire (~1 hour after step 3).
- Open Claude.ai / Claude Desktop. Observed: the connector shows
"Connection issue / Connection has expired. You can reconnect to
re-authenticate." Expected: silent background refresh; user sees
nothing.
- Click "Connect". Claude silently exchanges the cached refresh_token
at the IdP's /token endpoint and reconnects in ~200ms — no Microsoft
login window, no MFA prompt, no UI interaction.
This step proves the refresh_token is valid and the refresh exchange
works perfectly when manually triggered. The bug is that step 6
should not require step 7 — Claude should perform step 7 automatically
when the access_token expires.
- Use any MCP tool works again until the next ~1-hour expiry.
- Next morning, GOTO step 6.
Minimal repro setup is in the linked closed issues #43789, #55055, etc.
We can provide our specific Azure Function App OAuth metadata if helpful
for reproduction with an Entra-backed test app.
Claude Model
None
Is this a regression?
No, this never worked
Last Working Version
_No response_
Claude Code Version
2.1.144 (Claude Code)
Platform
Anthropic API
Operating System
Windows
Terminal/Shell
PowerShell
Additional Information
Workaround in use today
Daily 1-click reconnect. We extended our Entra Conditional Access "Sign-in frequency" policy to 180 days specifically scoped to the MCP app, which makes the reconnect silent (no Microsoft login window, no MFA prompt) — but the "Connection expired" UI in Claude still appears daily because it's not driven by actual token state.
Community workaround
mcp-stdio (https://github.com/shigechika/mcp-stdio) — a local stdio bridge that holds the refresh token client-side and refreshes transparently on 401. Confirms the behavior is fixable from the client side; the gap is in Claude itself.
OAuth refresh is the foundation of every modern long-lived API integration. Without it, every OAuth-based MCP feels broken to users and looks "in beta" forever. Fixing this single client-side bug probably resolves a large portion of the connector pain reported across #28262, #5706, #12447, #43789, #47219, #53803, #54742, #55055.
Showing cached comments. Read the full discussion on GitHub ↗
5 Comments
🤦♂️ Just finished implementing oauth2 that I didn't need so customers could easily connect to our MCP with Claude desktop. Weeks of work to find that tokens don't refresh. What a joke... Why did you over complicate connecting an MCP when a simple header would suffice only to partially implement the spec? Please fix.
Confirming this on Claude Cowork (Windows) as well — same daily "Connection expired" against a custom Entra ID (Azure AD) OAuth 2.0 + PKCE MCP server hosted on Azure App Service. Clicking Connect silently refreshes in ~200 ms, so the refresh token and Entra session are valid; the client just isn't calling the refresh endpoint when the access token expires. We run several of these connectors across a team, so the daily manual reconnect scales linearly with seat count — a proactive client-side refresh on 401/expiry would be a real reliability win for enterprise Entra deployments. Happy to share App Service / Entra sign-in logs if useful.
Independent repro with a few diagnostics not yet in the thread, in case they help isolate the root cause.
Setup: custom Streamable-HTTP MCP server, OAuth 2.1 auth-code + PKCE, public client (no secret), Microsoft Entra as IdP. Same symptom: drops at ~the access-token lifetime (~60–90 min); a manual "Connect" silently re-establishes in well under a second with no login/MFA prompt.
Added data points:
offline_access.** Captured the live/authorizerequest via browser devtools; scope is…/<resource-scope> offline_access openid. So this isn't the client failing to ask for a refresh token (re: the open question about whether the provider must grantoffline_access— here it's requested and granted).offline_accessreturns arefresh_token, and agrant_type=refresh_tokenPOST to the Entra/tokenendpoint mints a fresh access token (with a rotated refresh token). Entra issues and honors the refresh token fine; the client just never performs this exchange automatically on expiry/401.resourceparameter. Our custom connector advertises an RFC 9728 protected-resourceresource, so the client includes&resource=<https /mcp URL>in/authorize. By contrast, a first-party Entra-backed connector in the same workspace (Microsoft 365) issues/authorizewithscope=api://…/.default offline_access openid emailand noresourceparameter, over the sameclaude.ai/api/mcp/auth_callbackredirect, and does not show the daily drop for us. I can't observe whether the client sendsresourceon the refresh grant (server-side), but RFC 8707 resource handling on refresh is easy to get wrong, so it may be worth checking whether theresourceindicator affects the auto-refresh path. (This thread lists M365 as affected for others, so this may be a per-config difference rather than strictly first-party-vs-custom.)Net: request side and IdP side both check out — the only broken link is the client not calling
/tokenon expiry/401, matching this issue's ask. Happy to share HAR/log snippets.+1 This is a serious issue. Our team lost trust in the connectors because they're not stable, and it is not clear when something is stale or not working.
Confirming this also happens in the Claude Code VS Code extension specifically (Windows), with one addition: in my case even the manual-reconnect workaround this issue's own "workaround in use today" section relies on didn't reliably recover it.
Setup: A claude.ai account-level MCP connector (OAuth-based, not a project-scoped
.mcp.jsonserver), used from an automated/agentic Claude Code session working against it over several hours.Symptom:
/mcpshowed the connector as "connected" throughout, but every tool call against it returnedMCP server "<name>" session expired— identical error text on every attempt, across multiple different tools on that same server. A useful additional data point: a lightweight resource-listing call (ListMcpResourcesTool) against the same server succeeded cleanly at the same time tool invocation was failing — so the connector was reachable, and this wasn't a total disconnect, only the tool-invocation/authorization path specifically.Recovery attempts, all before this comment:
/mcp reconnect allfrom the interactive chat panel — did not run at all:MCP controls aren't available right now — the terminal is still starting up or is showing another view.(Session was fully up and otherwise responsive; this wasn't actually starting up.)/mcpafterward still showed the same server as not connected (4 of 9 configured servers not connected, consistent before and after reload).So in this environment, the expected "just click reconnect" workaround wasn't available either — not just inconvenient, but actually blocked by a separate UI-state issue on top of the underlying token-refresh gap this issue already describes. Happy to provide more detail (Claude Code version, timestamps) if useful — didn't want to include anything specific to the private server/repo involved.