[BUG] MCP OAuth: Claude doesn't auto-refresh access tokens, daily "Connection expired" despite valid refresh token

Status Open
Reported on v2.1.122
Maintainer reply None cached
Activity 9 comments · opened Jun 3, 2026

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 exchange
  • status.errorCode: 0
  • conditionalAccessStatus: success
  • appliedConditionalAccessPolicies: [] (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:

  1. Detect the expiry
  2. POST to the IdP's /token endpoint with the cached refresh token
  3. Receive a new access token (no user interaction required)
  4. Retry the original MCP call transparently
  5. 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

  1. 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.)

  1. Add the MCP server as a custom connector in Claude.ai (web) and/or

Claude Desktop. Provide OAuth Client ID + Secret.

  1. Click "Connect". OAuth flow completes successfully. Connector shows

as "Connected". Claude.ai stores: access_token (~1h lifetime) +
refresh_token (typically days to months).

  1. Use the MCP server normally, invoke its tools from a chat. Works fine.
  1. Wait for the access_token to expire (~1 hour after step 3).
  1. 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.

  1. 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.

  1. Use any MCP tool works again until the next ~1-hour expiry.
  2. 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.

View original on GitHub ↗

5 Comments

SupremeTechnopriest · 2 months ago

🤦‍♂️ 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.

ThomasLononno · 2 months ago

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.

czietsman · 2 months ago

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:

  1. **The client is requesting offline_access.** Captured the live /authorize request 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 grant offline_access — here it's requested and granted).
  1. The refresh exchange works independently. Separate from the manual-reconnect evidence already posted: a device-code flow against the same app requesting offline_access returns a refresh_token, and a grant_type=refresh_token POST to the Entra /token endpoint 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.
  1. Possible variable: the RFC 8707 resource parameter. Our custom connector advertises an RFC 9728 protected-resource resource, 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 /authorize with scope=api://…/.default offline_access openid email and no resource parameter, over the same claude.ai/api/mcp/auth_callback redirect, and does not show the daily drop for us. I can't observe whether the client sends resource on the refresh grant (server-side), but RFC 8707 resource handling on refresh is easy to get wrong, so it may be worth checking whether the resource indicator 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 /token on expiry/401, matching this issue's ask. Happy to share HAR/log snippets.

pavele · 2 months ago

+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.

Stache73 · 1 month ago

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.json server), used from an automated/agentic Claude Code session working against it over several hours.

Symptom: /mcp showed the connector as "connected" throughout, but every tool call against it returned MCP 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:

  1. Simple retry — failed identically.
  2. /mcp reconnect all from 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.)
  3. Full VS Code window reload ("Developer: Reload Window") — did not fix it. /mcp afterward 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.

Showing cached comments. Read the full discussion on GitHub ↗