HOW-TO GUIDE | AI SECURITY
12 min read

Why Your Internal MCP Server Keeps Losing Its Connection: OAuth Token Lifecycle Failures

A platform engineer's troubleshooting guide to the re-auth loop, the daily connection-expired banner, and the mid-session drop

5
Distinct documented root causes behind the MCP OAuth re-auth loop, from auto-refresh gaps to transport session timeouts
60s
Typical grace period implementations add to JWT exp/nbf validation to absorb ordinary clock skew
3
Major MCP clients with publicly filed issues for this exact failure pattern: Claude Code, Gemini CLI, and Codex

SponsoredHorizon3.ai

Proactive Security for the AI Era

NodeZero continuously and autonomously pentests infrastructure, identity, cloud, and now web applications, chaining weaknesses across every domain the way real attackers do. Every finding ships with replayable proof showing exploitable business impact, not theoretical risk.

See NodeZero WebApp in action

The symptom is always some variant of the same thing. An internal MCP server that gates access to internal tools, ticketing systems, code hosts, or data stores authenticates cleanly the first time. Somewhere between one hour and one day later, the agent's next tool call fails, the client shows a banner along the lines of "connection expired, reconnect to re-authenticate," and the only fix that seems to work is manually re-running the OAuth flow, which then fails again on its own schedule. If that loop is happening on an MCP server your own platform team runs, it is not a misconfigured client secret or a wrong redirect URI. It is one of a handful of specific, already-documented gaps in how MCP clients and servers handle OAuth token lifecycle and transport session state, and as of late 2026 those gaps are common enough across MCP client implementations that treating the symptom as a one-off bug in your own deployment usually wastes a debugging cycle.

This guide walks through the actual causes behind the loop, how to tell which one you are hitting from your own logs, the fixes that correspond to each cause, how to confirm a fix actually held, the failure modes that survive a naive fix, and when the right move is to stop patching your own server and escalate to the client vendor or the protocol spec instead. For the wider security exposure of running an MCP server at all, not just the OAuth plumbing, see our MCP security risks guide. If part of your response here is standing up scanning in front of these servers, our roundup of MCP server security scanners covers what Cisco's, Invariant's, and Akto's tools actually check for.

Cause 1: the client never implements auto-refresh in the first place

The single most common root cause is not a bug in your server at all. It is that the MCP client sitting between your agent and your server never actually refreshes the access token on the client's own initiative. Anthropic's own Claude Code has shipped this exact defect twice against real users: one report describes MCP OAuth tokens for HTTP-based servers not auto-refreshing despite a valid refresh token already stored locally, and a separate, later report describes a daily "Connection expired" banner appearing even though the stored refresh token is still valid, meaning the client is choosing to surface a re-auth prompt instead of quietly using the refresh token it already has. Google's Gemini CLI has reported the same underlying gap from a different angle: once the access token expires (commonly after one hour), every subsequent MCP tool call fails, there is no recovery mechanism built into the CLI, and the documented workaround is to restart the CLI process entirely, which forces a fresh OAuth flow. None of these are configuration mistakes on the server side. They are gaps in the client's session-management code, and industry write-ups from mid-2026 describe this as a pattern across MCP clients broadly, not an isolated defect in any one product.

Cause 2: the refresh request itself is malformed

A second, more subtle cause is a client that does attempt to refresh, but sends a refresh request the authorization server rejects. OpenAI's Codex hit a specific version of this: its MCP OAuth refresh flow omitted the RFC 8707 resource parameter that the MCP authorization specification requires on token requests, which meant that after the access token expired, the refresh attempt itself failed, the MCP server became unreachable, and in Codex's case a required MCP server could block creation of a new task entirely with an "OAuth authorization required" error. This is diagnosable specifically because the failure happens at the refresh step, not at the point the original access token expires: the client is trying to do the right thing and getting a rejection back from the authorization server.

Free daily briefing

Briefings like this, every morning before 9am.

Threat intel, active CVEs, and campaign alerts, distilled for practitioners. 50,000+ subscribers. No noise.

Cause 3: refresh token rotation races itself into an invalid_grant

Many identity providers used behind MCP servers, and Atlassian and Asana are commonly cited examples, rotate the refresh token on every use: each refresh both issues a new access token and invalidates the refresh token that was just spent, issuing a new one in its place. That is a reasonable security control on its own, but it creates a specific failure mode when more than one request hits an expired access token at the same moment, which happens naturally when an agent fires several tool calls in quick succession. The first refresh request succeeds and rotates the token. Every other in-flight request, which captured the same now-spent refresh token before the first one completed, then presents it and gets back invalid_grant. Under RFC 6819's replay-detection guidance, some authorization servers respond to that reuse by revoking the entire token family, which is worse than a transient failure: it forces a full manual re-authentication, not just a retry. This exact defect has been filed directly against the MCP TypeScript SDK, where a race condition in the client's auth() function causes refresh token invalidation specifically when the identity provider uses rotating refresh tokens, confirming this is not merely a generic OAuth footgun but one that has already reached MCP's own reference client implementation.

Cause 4: clock skew rejects a token that has not actually expired

JWT-based access tokens are validated in part by checking the exp (expiration) and nbf (not-before) claims against the verifier's own local clock. If the MCP server's clock and the authorization server's clock have drifted, a token that is genuinely still valid by the issuer's clock can be rejected as expired or not-yet-valid by the verifier's clock, or the reverse. This is a materially bigger risk in containerized MCP server deployments than on a stable host, because a container's clock can drift from its host's clock independently, and container restarts do not always re-sync time immediately. The practical signal here is that the failure looks intermittent and does not correlate cleanly with the token's actual configured lifetime; some implementations mitigate this with a roughly 60-second grace period on expiration checks specifically to absorb ordinary clock drift, which is a strong hint at what is missing if your own server enforces exp and nbf with zero tolerance.

Cause 5: the transport session times out independently of the OAuth token

The last major cause is not an OAuth problem at all, even though it produces an identical symptom from the user's seat. MCP's Streamable HTTP transport uses a session, tracked via an MCP-Session-Id header, that is logically separate from the OAuth access token. The specification is explicit that a server may terminate a session at any time and must respond with HTTP 404 to any further request carrying that session ID, at which point a compliant client is supposed to start an entirely new session by sending a fresh InitializeRequest without a session ID. In practice, this handoff is where clients without robust reconnection logic fail outright, and even clients that do attempt to reconnect can cycle through repeated "session not found" errors before they succeed. Docker's MCP Gateway has documented Streamable HTTP sessions being terminated after roughly sixty seconds in some configurations, and the MCP TypeScript SDK has an open report specifically about idle session timeout behavior on this transport. The tell here is a 404 with a session-not-found style message in your server logs, as opposed to a 401 pointing at the OAuth layer, and it means the fix belongs in session lifetime and reconnection handling, not in token refresh logic at all.

Diagnostics: reading your own logs to pick the right cause

Before applying any fix, pull the actual failure response your server returned at the moment the connection dropped, because the five causes above leave distinguishable fingerprints. A 401 at the moment the access token's own lifetime expired, with no refresh attempt visible in the authorization server's logs at all, points to cause 1, a client that never tries to refresh. A refresh attempt that the authorization server itself rejects as malformed, visible in the authorization server's own request logs, points to cause 2. An invalid_grant response specifically on the refresh call, especially one that correlates with a burst of concurrent tool calls, points to cause 3. A rejection that does not line up with the token's configured lifetime at all, and that clears up after a container restart or an NTP resync, points to cause 4. And a 404 carrying a session-not-found message, distinct from any 401, points to cause 5, the transport session, not the token. Reproducing the failure deliberately, by forcing a token to sit past expiry and then firing several tool calls at once, is the fastest way to separate cause 3 from the others, since it will not show up under a single serialized request.

Fixes, matched to the cause

Each cause above has a distinct fix; applying the wrong one will not resolve the loop even though it might feel like progress.

Subscribe to unlock Remediation & Mitigation steps

Free subscribers unlock full IOC lists, Sigma detection rules, remediation steps, and every daily briefing.

Validation: confirming the fix actually held

A fix that looks like it worked for the next ten minutes is not validated. Run a soak test that deliberately spans at least two full token-expiry boundaries, not just one, since single-boundary tests miss failures that only show up on the second refresh cycle. Separately, fire a burst of concurrent tool calls timed to land exactly at an access token's expiry to specifically re-trigger the refresh-rotation race from cause 3; a single-flight lock that works under a serialized test can still fail under real concurrency if the lock scope was set incorrectly. Restart the MCP server process mid-session to confirm the transport-session fix actually produces a clean reconnect rather than a stuck client, and check your authorization server's own audit log after 24 to 48 hours of normal traffic for zero token-family revocations and zero repeated invalid_grant responses, which is the clearest sign the loop is actually gone rather than just less frequent.

Failure cases: when the fix does not fully resolve the loop

Some instances of this problem do not have a full technical fix available on your side today. If the client you are integrating against has no reconnection logic whatsoever, as opposed to buggy reconnection logic, patching your server cannot compensate; the honest options are switching clients, wrapping the client in a supervising process that restarts it on failure, or accepting the manual re-auth cadence until the client ships a fix. If an authorization server has already revoked a refresh token family due to confirmed reuse, that family is gone; the user has to complete a fresh OAuth authorization, and no retry or backoff strategy recovers it. And if the identity provider behind your MCP server enforces a short, non-negotiable session or token lifetime as a platform policy, for example a compliance-driven one-hour cap that cannot be extended, then proactive refresh reduces how often users notice the boundary but does not eliminate the requirement to cross it correctly every time.

Escalation criteria: when this stops being your server's problem to fix

Escalate to the MCP client vendor, rather than continuing to iterate on your own server, once you have confirmed through the diagnostics above that the failure originates in the client's session or refresh handling and not in anything your server controls; cite the specific upstream issue if one already exists, since several of the causes in this guide are already tracked against Claude Code, Gemini CLI, Codex, and the MCP TypeScript SDK directly. Escalate to your identity provider's support channel when a token family has been revoked repeatedly for the same user or service account, since that pattern usually means the rotation-race fix on your side is incomplete rather than the user doing anything wrong. Escalate to your own security or platform leadership, separate from a vendor ticket, when the workaround in place is "restart the client on a timer" or "issue very long-lived tokens to avoid frequent refresh," since both are operational patches that quietly widen your actual exposure window and deserve a real risk conversation rather than living on indefinitely as the fix.

The bottom line

An internal MCP server that keeps losing its connection is very rarely a one-off misconfiguration. It is almost always one of five documented gaps: a client that never auto-refreshes, a malformed refresh request missing a required parameter, a refresh-token rotation race under concurrent calls, clock skew rejecting a token that has not actually expired, or a transport session timing out independently of the OAuth token entirely. Reading the actual failure response, a 401 versus an invalid_grant versus a 404, tells you which one you have before you touch any configuration. Fix the matched cause, validate across at least two expiry boundaries and under real concurrency, and know in advance which failure modes, a client with no reconnect logic, a revoked token family, a hard compliance-driven session cap, have no full fix on your side and need an escalation instead of another patch cycle.

Frequently asked questions

Why does my internal MCP server keep asking to reconnect even though the refresh token should still be valid?

This is most often a client-side defect where the MCP client never actually attempts to use the stored refresh token on its own initiative, a gap that has been filed directly against Claude Code and Gemini CLI. The server-side refresh token is often genuinely still valid; the client simply is not using it before surfacing a re-authentication prompt to the user.

What is the difference between a 401 and a 404 when an MCP server connection drops?

A 401 indicates the OAuth access token itself was rejected, pointing to a token-lifecycle cause such as expiry, a malformed refresh, or clock skew. A 404 with a session-not-found style message indicates the Streamable HTTP transport session was terminated independently of the token, which per the MCP transport specification requires the client to start a brand new session rather than refresh anything.

Can concurrent MCP tool calls actually cause an OAuth refresh token to become invalid?

Yes, when the identity provider behind the server rotates refresh tokens on every use. If two requests hit an expired access token at the same moment, the first refresh succeeds and rotates the token while every other in-flight request presents the now-spent token and gets invalid_grant, sometimes triggering reuse detection that revokes the entire token family.

Does clock skew really cause OAuth token validation failures on MCP servers?

Yes, particularly in containerized deployments where a container's clock can drift independently from its host. JWT validation checks the exp and nbf claims against the verifier's local clock, so a genuinely valid token can be rejected if that clock has drifted from the issuer's clock, which is why some implementations add a bounded grace period on top of fixing the underlying time sync.

Is a long-lived access token a real fix for MCP OAuth connection drops?

It is a mitigation, not a fix. Issuing longer-lived tokens reduces how often a client's missing or buggy refresh logic gets exposed, but it does not correct the underlying gap and it widens the window an attacker has if that token is ever exposed, so it should be treated as a temporary bridge while the actual client or server defect gets patched.

When should I stop trying to fix an MCP OAuth connection loop myself and escalate it?

Escalate once your own logs confirm the failure originates in the client's session or refresh handling rather than anything your server controls, especially if a matching issue is already filed against that client's repository, or once a refresh token family has been revoked repeatedly for the same account, which signals the underlying rotation-race fix needs vendor-level attention rather than another local patch.

Sources & references

  1. anthropics/claude-code - MCP OAuth tokens not auto-refreshing despite valid refresh tokens (Issue #28262)
  2. anthropics/claude-code - Daily "Connection expired" despite valid refresh token (Issue #65036)
  3. google-gemini/gemini-cli - MCP servers with OAuth lose authentication when access token expires mid-session (Issue #23776)
  4. openai/codex - MCP OAuth refresh omits the RFC 8707 resource parameter and breaks authenticated servers after access-token expiry (Issue #33403)
  5. modelcontextprotocol/typescript-sdk - Race condition in auth() causes refresh token invalidation when rotating tokens are used (Issue #1760)
  6. modelcontextprotocol/typescript-sdk - "Streamable HTTP" transport idle session timeout (Issue #812)
  7. docker/mcp-gateway - Streamable HTTP sessions are unstable, causing clients without auto-reconnect to fail (Issue #412)
  8. danny-avila/LibreChat - MCP OAuth connections fail permanently when server-side tokens are invalidated (Issue #12563)
  9. Model Context Protocol - Transports specification (session termination and 404 handling)
  10. WorkOS - OAuth token refresh has a race condition. Fix it with a conditional write, not a distributed lock.

Free resources

25
Free download

Critical CVE Reference Card 2025–2026

25 actively exploited vulnerabilities with CVSS scores, exploit status, and patch availability. Print it, pin it, share it with your SOC team.

No spam. Unsubscribe anytime.

Free download

Ransomware Incident Response Playbook

Step-by-step 24-hour IR checklist covering detection, containment, eradication, and recovery. Built for SOC teams, IR leads, and CISOs.

No spam. Unsubscribe anytime.

Free newsletter

Get threat intel before your inbox does.

50,000+ security professionals read Decryption Digest for early warnings on zero-days, ransomware, and nation-state campaigns. Free, daily, no spam.

Unsubscribe anytime. We never sell your data.

Eric Bang
Author

Founder & Cybersecurity Evangelist, Decryption Digest

Cybersecurity professional with expertise in threat intelligence, vulnerability research, and enterprise security. Covers zero-days, ransomware, and nation-state operations for 50,000+ security professionals every morning.

Giveaway: InfoSec World 2026 All Access Pass ($3,895 value)

Details →
Daily Briefing

Subscribe to enter the giveaway

Every subscriber is automatically entered. You also get daily threat intel every morning: zero-days, ransomware, and nation-state campaigns. Free. No spam.

Already subscribed? You're already entered.

Giveaway

Win a $3,895 InfoSec World 2026 pass.