Bug: ChatGPT MCP connector prompts for reauthentication despite valid tokens with offline_access scope

Summary

ChatGPT’s MCP connector intermittently prompts for reauthentication after 4-5 consecutive tool calls, despite the server receiving valid tokens with proper scopes including offline_access. After providing auth through the “Continue” prompt, ChatGPT stops using tool calls entirely and responds with text only.

Environment

  • Custom MCP server with OAuth 2.0/OIDC (Keycloak)
    • Using @modelcontextprotocol/sdk for Node.js
      • All OAuth metadata endpoints returning correct values
        • offline_access scope properly configured and granted
      • Server-Side Evidence

    • Server logs show all token verifications succeed - no 401 errors:
  • json
    
  • {
  • “event”: “token_verified”,
  • “client_id”: “chatgpt-mcp-connector”,
  • “oauth_scopes”: [“openid”, “mcp:tools”, “email”, “offline_access”, “profile”],
  • “expires_at”: 1768959536
  • }
  • 
    
    
    • Tokens include offline_access scope ✓
      • Token expiry is 12+ hours in the future ✓
        • No authentication failures logged ✓
          • Server health remains OK throughout ✓
        • Observed Behavior

        1. MCP connector works for 4-5 tool calls
          1. ChatGPT shows “Continue” / reauthentication prompt
            1. User clicks Continue and re-authenticates
              1. ChatGPT immediately responds with text instead of using tools
              2. Related Reports

            2. This appears related to other reported OIDC issues:
            3. Stateless server: token refresh works after short idle, but reconnect loop after long idle
            4. ChatGPT’s MCP connector intermittently prompts for reauthentication after 4-5 consecutive tool calls, despite the server receiving valid tokens with proper scopes including offline_access. After providing auth through the “Continue” prompt, ChatGPT stops using tool calls entirely and responds with text only.
          2. Environment

          • Custom MCP server with OAuth 2.0/OIDC (Keycloak)
            • Using @modelcontextprotocol/sdk for Node.js
              • All OAuth metadata endpoints returning correct values
                • offline_access scope properly configured and granted
              • Server-Side Evidence

            • Server logs show all token verifications succeed - no 401 errors:
          • json
            
          • {
          • “event”: “token_verified”,
          • “client_id”: “chatgpt-mcp-connector”,
          • “oauth_scopes”: [“openid”, “mcp:tools”, “email”, “offline_access”, “profile”],
          • “expires_at”: 1768959536
          • }
        • Tokens include offline_access scope ✓
          • Token expiry is 12+ hours in the future ✓
            • No authentication failures logged ✓
              • Server health remains OK throughout ✓
            • Observed Behavior

            1. MCP connector works for 4-5 tool calls
              1. ChatGPT shows “Continue” / reauthentication prompt
                1. User clicks Continue and re-authenticates
                  1. ChatGPT immediately responds with text instead of using tools

Related Reports

This appears related to other reported OIDC issues:

  • Token refresh works after short idle, but reconnect loop after long idle
  • OIDC Authentication Failure
  • Request

Could the ChatGPT team investigate the client-side OAuth/session handling? The server-side implementation is working correctly (tokens valid, no errors), suggesting the issue is in how ChatGPT manages MCP sessions or token refresh.

Update: Root Cause Identified

After implementing comprehensive request header logging on the MCP server, I’ve identified the root cause of this issue.

The Problem

ChatGPT is not persisting the Mcp-Session-Id header between tool call batches. After approximately 30 seconds, ChatGPT drops the session ID and sends a fresh initialize request, which triggers the “Continue / Do not continue” prompt on the client side.

Evidence from Server Logs

Here’s what the logs show:

13:08:37 - Tool calls #1 and #2 (successful):slight_smile: ```json

{

“event”: “mcp_request_headers”,

“headers”: {

"mcp_session_id": "3c0708d1-e974-4383-a91c-b97b50235ad1"
  },
  "is_initialize": false
}
```

**13:09:08** (31 seconds later) - Tool call #3:
```json
{
  "event": "mcp_request_headers",
  "headers": {
    "mcp_session_id": null
  },
  "is_initialize": true
}
```

### Key Findings

1. **Authentication is working perfectly** - All token verifications succeed, no 401 errors
2. **Server sessions persist** - The original session (`3c0708d1-...`) still existed and was valid
3. **ChatGPT dropped it** - ChatGPT sent `null` instead of the existing session ID
4. **This triggers re-auth prompts** - When ChatGPT sends an `initialize` request (creating a new session), its client interprets this as "requesting new permissions"

### Why This Happens

The ~30 second interval suggests ChatGPT either has:
- An internal session timeout
- A connection reconnection behavior that clears session storage
- A bug in how it persists the `Mcp-Session-Id` header across requests

### Conclusion

**This is a ChatGPT MCP client bug, not an OAuth/OIDC configuration issue.** The server-side implementation is working correctly - tokens verify, sessions persist, and everything is healthy. The issue is entirely on ChatGPT's side failing to remember the session ID it received from the MCP server.

Unfortunately, there's nothing that can be fixed on the MCP server side - this needs to be addressed in the ChatGPT MCP connector implementation.

Thanks for the clear write-up and logs — this is very well investigated. The timing and header evidence make it pretty clear that authentication and server-side session handling are working as expected, and that the issue is the MCP client dropping Mcp-Session-Id between batches. The ~30s gap strongly suggests a client-side persistence or reconnect bug rather than anything fixable on the server. This should be valuable for the ChatGPT MCP connector team to look into.

Update: Root Cause Identified with Full Forensic Evidence (Two Independent Captures)

Since my original report, I’ve instrumented my MCP server extensively and captured two independent forensic sessions that conclusively prove this is a client-side session management bug in openai-mcp/1.0.0. Sharing full evidence here so the engineering team has everything needed to reproduce and fix.


Executive Summary

ChatGPT’s MCP connector never persists mcp-session-id across tool-call batches. Every batch triggers a fresh initialize request with no session header, creating orphaned server-side sessions that accumulate without bound. The connector does correctly use the session ID for tool calls within a single batch — proving it extracts the header from the initialize response — but discards it when the SSE connection closes (~200ms after the last response). No DELETE is ever sent.

This is the same bug reported by HungryHippo5 (Nov 2025), dannyyy.jimenez (Nov-Dec 2025), and Alokai (Jan 2026). It has been open for 4+ months without a fix. The reauthentication prompt I originally reported is a downstream consequence of this session loss.


Server-Side Methodology

I’m running a custom MCP server (Node.js, @modelcontextprotocol/sdk, behind Keycloak OIDC + Cloudflare Tunnel). To capture evidence, I added:

  1. Request header logging — Every incoming request is logged with mcp_session_id (value or null), is_initialize flag, user_agent, and timestamp.
  2. Session lifecycle eventssession_created, session_closed, session_removed with active_sessions count.
  3. SSE disconnect trackingclient_disconnected with elapsed_ms showing how long the SSE stream lived.
  4. Response header verificationmcp_response_headers event on every res.finish() proving mcp-session-id was present in the outgoing response.
  5. Diagnostic response headersX-Lacard-Session-Active, X-Lacard-Session-Age-Ms, X-Lacard-Request-Id on every response for Cloudflare-layer visibility.
  6. Cache-Control hardeningCache-Control: no-cache, no-store, no-transform on all response paths (including JSON-mode, not just SSE), ruling out intermediary caching.

Every gap I could think of on the server side has been closed. The mcp-session-id header is verifiably present on 100% of outgoing responses, including error responses.


Forensic Capture 1 — Session Survival Then Loss (2026-02-27, ~07:30 UTC)

This capture is interesting because it shows ChatGPT successfully reusing a session across SSE disconnects early in the conversation, then losing it entirely later.

07:33:31Z  SESSION 960cec79 created (active=1)
07:33:xx   8 requests with sid=960cec79                    ← normal operation
07:34:xx   REQUEST sid=NULL                                ← SESSION DROPPED
07:34:41Z  SESSION c26e7887 created (active=2)             ← 1.2 min gap
07:34:xx   2 requests with sid=c26e7887
07:35:xx   SSE DISCONNECT 960cec79 (elapsed=125,004ms)
07:35:xx   SSE DISCONNECT c26e7887 (elapsed=124,999ms)
07:3x:xx   11 requests with sid=960cec79                   ← SURVIVED SSE disconnect!
07:3x:xx   SSE DISCONNECT 960cec79 (elapsed=125,006ms)
07:xx:xx   2 requests with sid=960cec79                    ← survived again
07:xx:xx   SSE DISCONNECT 960cec79 (elapsed=124,662ms)
           ~~~ 44.6 minute idle gap ~~~
08:19:17Z  REQUEST sid=NULL                                ← SESSION DROPPED
08:19:17Z  SESSION 051dcb09 created (active=3)
           ~~~ 9.5 minute idle gap ~~~
08:28:46Z  REQUEST sid=NULL                                ← SESSION DROPPED
08:28:46Z  SESSION 59c7c2b5 created (active=1)             ← server restarted
08:28:47Z  REQUEST sid=NULL                                ← DROPPED AGAIN 0.4s LATER
08:28:47Z  SESSION 3602c17d created (active=2)
08:28:47   2 requests with sid=3602c17d
08:28:47   SSE DISCONNECT 3602c17d (elapsed=195ms)         ← 195ms SSE lifetime

Key observations from Capture 1:

  • Session 960cec79 survived three SSE disconnect/reconnect cycles (at ~125s each). This proves the connector can persist session state across SSE reconnections — at least sometimes.
  • The same session was then lost after a 44.6-minute idle gap — but also lost after a 0.4-second gap (08:28:46→08:28:47), ruling out any consistent timeout threshold.
  • SSE streams disconnected by the client at ~125 seconds — likely a keepalive timeout. This is fine and expected. The bug is that session state doesn’t always survive the reconnection.

Forensic Capture 2 — Total Session Loss, Every Batch (2026-02-27, ~08:28 UTC)

This capture, taken ~1 hour later, shows the connector in a fully degenerate state: every single tool-call batch creates a new session. No session is ever reused.

Time (UTC)    Session ID   Active Sessions   Gap Since Previous
─────────────────────────────────────────────────────────────────
08:28:46Z     59c7c2b5     1                 -
08:28:47Z     3602c17d     2                 0.4s
08:31:06Z     67cd9106     3                 138.9s  (2.3 min)
08:31:20Z     6c2778bb     4                 14.6s
08:31:27Z     011ef8ed     5                 7.1s
08:31:34Z     4f2b6a05     6                 6.6s
08:31:40Z     8286125c     7                 6.6s
08:36:42Z     fb2c9bf5     8                 301.9s  (5.0 min)
08:44:13Z     892a482e     9                 450.2s  (7.5 min)
08:44:23Z     cb26df0c     10                10.1s
08:44:39Z     f1b6cf4e     11                16.5s
08:44:51Z     efb85bbe     12                12.0s
08:51:00Z     9858e4a2     13                368.3s  (6.1 min)

Aggregate statistics:

  • 13 sessions created in 22 minutes
  • 37 total requests, of which 13 had mcp_session_id: null (the initialize requests) and 24 had valid session IDs (tool calls within each batch)
  • 0 DELETE requests — ChatGPT never closes sessions
  • SSE connection lifetime: 125ms to 355ms (median: 217ms) — the connector disconnects almost immediately after receiving the JSON response
  • Peak active_sessions: 13 (and climbing)

The pattern within each batch is:

  1. POST /mcp with mcp-session-id: null, body = initialize JSON-RPC
  2. Server responds with mcp-session-id: <uuid> in response header
  3. POST /mcp with mcp-session-id: <uuid>, body = tools/list
  4. POST /mcp with mcp-session-id: <uuid>, body = tools/call
  5. SSE stream closes (195-355ms after open)
  6. Session ID is forgotten
  7. Next batch: back to step 1 with a fresh initialize

The connector correctly stores and uses the session ID for steps 2-4 but loses it between steps 5 and 7.


What This Rules Out

Hypothesis Ruled Out By
Server not sending mcp-session-id Response header logging proves it’s on every response, including errors
Cloudflare stripping the header Cache-Control: no-cache, no-store, no-transform on all paths; diagnostic headers (X-Lacard-*) survive round-trip
Idle timeout on server side Capture 1 shows session surviving 3 SSE cycles over minutes; Capture 2 shows drops at 0.4s gaps
SSE disconnect causing loss Capture 1 proves sessions survive SSE disconnects (960cec79 survived 3 cycles)
Token expiry Tokens verified successfully on every request; expires_at is always hours in the future
Server-side session eviction active_sessions count climbs monotonically — no sessions are removed

Most Likely Root Cause

The session ID is stored on a connection-scoped or transport-scoped object that is disposed when the SSE connection closes.

Evidence:

  • Session ID IS extracted from the response header (tool calls within a batch prove this)
  • Session ID IS used correctly while the connection lives
  • Session ID IS lost when the SSE stream closes
  • The loss is structural, not timeout-based (0.4s and 6.6s gaps trigger it)
  • The connector user-agent (openai-mcp/1.0.0) suggests a purpose-built client, not the reference SDK

The fix is to persist the session ID at conversation scope (or whatever scope outlives the SSE connection), not on the transport/connection object.

An alternative hypothesis: if the connector runs in a serverless/ephemeral context (Lambda, Cloud Function, etc.), the session ID may only live in function memory. Auth tokens persist because they’re in a database; session IDs don’t because they’re stored in-memory. This would explain why gap duration is irrelevant.


The Reauthentication Cascade (Why This Bug Manifests as an Auth Prompt)

The reauthentication prompt I originally reported is a downstream effect of the session loss:

  1. Connector sends initialize with no mcp-session-id (because it forgot)
  2. If the server has session validation, it may reject the request (400 or require re-init)
  3. ChatGPT interprets this as an auth failure and shows the “Continue” / re-auth prompt
  4. After re-auth, ChatGPT degrades to text-only mode (no more tool calls for the rest of the conversation)

Servers that don’t validate sessions (or that auto-create sessions on every request) won’t see the re-auth prompt — but they’ll still accumulate orphaned sessions. The session loss is the root bug; the re-auth prompt is a symptom.


Server-Side Session Accumulation Risk

Without the DELETE lifecycle event, MCP servers have no signal that a session is finished. Servers must implement their own cleanup:

  • My server now runs a 24-hour session TTL cleanup timer
  • Without this, active_sessions would grow unboundedly for the lifetime of the process
  • Each session holds a StreamableHTTPServerTransport object with associated SSE state — this is a memory leak

Related Community Reports

This bug has been reported independently by multiple developers over 4+ months:

  • HungryHippo5 — “Connector tool calls generating fresh MCP session each invocation” (Nov 3, 2025) — Earliest report. Noted it “seemed to be working” briefly, then regressed.
  • dannyyy.jimenez (Nov 20, 2025) — Confirmed regression after initial fix.
  • Alokai (Jan 15, 2026) — “Every request produces a completely new ‘mcp-session-id’ header… Despite no code changes in the MCP server itself.”
  • Tarun Agarwal — “ChatGPT does not re-trigger OAuth on 401” (Feb 13, 2026) — Likely the same root cause: session loss → 401 → no re-auth.
  • Various OAuth/scope issues in sergio.delamo;s thread (Sept 2025 onward) — Related connector maturity issues.

What Would Fix This

Per the MCP Streamable HTTP transport specification:

After receiving a session ID, the client MUST include it in the Mcp-Session-Id header on all subsequent HTTP requests to the server.

The connector needs to:

  1. Extract mcp-session-id from the initialize response — it already does this (proven by within-batch tool calls working).
  2. Store it at conversation scope — NOT on the transport/connection object. It must survive SSE disconnects.
  3. Replay it on ALL subsequent requests — including the next batch’s tool calls, without re-initializing.
  4. Send DELETE /mcp with mcp-session-id when the conversation ends or the connector is torn down — currently never happens.
  5. Handle mcp-session-id across SSE reconnections — reconnect with the existing session ID rather than starting fresh.

Reproduction Steps for OpenAI Engineering

  1. Set up any MCP server that logs mcp-session-id from incoming request headers and logs session_created events with active_sessions count.
  2. Connect it as a ChatGPT connector (Developer Mode is fine).
  3. Have a conversation that triggers 2+ separate batches of tool calls (ask a question, wait for response, ask a follow-up that also triggers tools).
  4. Observe: each batch sends mcp-session-id: null with a fresh initialize. active_sessions climbs. No DELETE is ever sent.

Minimal logging middleware for verification:

app.use("/mcp", (req, res, next) => {
  console.log(JSON.stringify({
    event: "mcp_request_headers",
    mcp_session_id: req.headers["mcp-session-id"] || null,
    is_initialize: /* check JSON-RPC body for "initialize" method */,
    timestamp: new Date().toISOString(),
  }));
  next();
});

My Environment

  • Server: Node.js 22, @modelcontextprotocol/sdk (latest), Express
  • Auth: Keycloak 26 (OIDC), offline_access scope granted, tokens valid for 5+ minutes
  • Network: Cloudflare Tunnel → 127.0.0.1:7052
  • Client: ChatGPT connector, user-agent openai-mcp/1.0.0
  • All response headers verified via server-side logging
  • All intermediary caching ruled out via Cache-Control headers

I’m happy to provide raw JSONL logs, additional captures, or any other evidence that would help. This bug makes stateful MCP servers unusable from ChatGPT — every conversation creates dozens of orphaned sessions, and the re-auth prompt breaks the user experience after the first idle gap.

Update 3: Third Independent Capture — 84 Token Verifications for 12 Tool Calls

I just ran another query through ChatGPT with my MCP server connected. I was prompted to re-authenticate seven times during a single conversation turn. The server-side telemetry for that turn is below.

This is a third independent forensic capture, taken hours after the captures in my previous post. The connector behavior has not changed.


The Numbers

┌────────────────────────────────────┬──────────────┐
│ Metric │ Value │
├────────────────────────────────────┼──────────────┤
│ Wall-clock duration │ 14.5 minutes │
├────────────────────────────────────┼──────────────┤
│ MCP sessions created │ 20 │
├────────────────────────────────────┼──────────────┤
│ Active session count (start → end) │ 55 → 73 │
├────────────────────────────────────┼──────────────┤
│ Actual tool calls completed │ 12 │
├────────────────────────────────────┼──────────────┤
│ OIDC token verifications │ 84 │
├────────────────────────────────────┼──────────────┤
│ DELETE requests sent │ 0 │
├────────────────────────────────────┼──────────────┤
│ Session IDs reused across batches │ 0 │
└────────────────────────────────────┴──────────────┘

Let that ratio sink in: 84 token verifications for 12 tool calls. The connector spent 7x more effort on authentication ceremony than on actual work. Every one of those 84 verifications succeeded — valid token, valid scopes, offline_access present, expiry hours in the future. The tokens are fine. They have always
been fine.


Burst Pattern: 5 Re-Initializations in 34 Seconds

The worst burst during this session:

22:46:12Z initialize → new session (active: 63)
22:46:22Z initialize → new session (active: 64) +10s
22:46:31Z initialize → new session (active: 65) +9s
22:46:35Z initialize → new session (active: 66) +4s
22:46:46Z initialize → new session (active: 67) +11s

Five new MCP sessions in 34 seconds. Each one sends mcp_session_id: null, gets session_found: false, creates a new session, makes 0–2 tool calls, then discards the session and starts over. This is what “re-authenticate 7 times in a single query” looks like from the server side.


Session Leak at Scale

By the end of this session, the server was tracking 73 active sessions — all from chatgpt-mcp-connector, all orphaned, zero DELETE requests received. The session count only went down when my server’s 24-hour TTL cleanup ran. Without that cleanup, this would be an unbounded memory leak against every MCP server
OpenAI connects to.

For context, my Claude Code MCP client created 1 session during the same window and reused it for every tool call. Session reuse is not a novel engineering challenge.


This Bug Is Systemic Across OpenAI’s MCP Integration

I’ve now spent considerable time investigating whether there’s anything I can do server-side to work around this. The answer is no — but the investigation revealed something worse.

OpenAI’s own Agents Python SDK has the same bug. Issue #924 (open since June 2025): MCPServerStreamableHttp discards the get_session_id callback from the underlying MCP SDK, retaining only the read/write streams. This makes stateful sessions impossible for anyone using OpenAI’s own SDK to connect to MCP servers.

This means session persistence isn’t just broken in the ChatGPT connector — it was never implemented in the Agents SDK either. The pattern is consistent: OpenAI’s MCP integration treats sessions as disposable at every level of their stack.

The MCP specification (2025-11-25) is unambiguous:

After receiving a session ID, the client MUST include it in the Mcp-Session-Id header on all subsequent HTTP requests to the server.

There is no server-side mechanism in the spec to force a non-compliant client to reuse sessions. A server cannot reject a valid initialize request. The only community “workaround” is stateless_http=True — abandoning MCP sessions entirely, which defeats the purpose of the protocol’s session management.

Timeline
│ Date │ Event │
│ Nov 3, 2025 │ First report (HungryHippo5): “fresh MCP session each invocation”
│ Nov 20, 2025 │ Confirmed regression after brief fix (dannyyy.jimenez)
│ Jan 15, 2026 │ Independent confirmation (Alokai): “every request produces a completely new mcp-session-id”
│ Jan 20, 2026 │ My first report in this thread
│ Jan 21, 2026 │ Root cause identified with header logging evidence
│ Feb 13, 2026 │ Related report (Tarun Agarwal): “ChatGPT does not re-trigger OAuth on 401”
│ Feb 27, 2026 │ Three independent forensic captures with full telemetry
│ Jun 23, 2025 │ OpenAI Agents SDK Issue #924 filed — same root cause in their own SDK

Six reports from independent developers over four months. The user-agent string is still openai-mcp/1.0.0 — unchanged since the first report. No official response from OpenAI on any of these threads.


What I Need From OpenAI

  1. Acknowledgment that this is a known bug. Four months of silence on multiple independent reports is not acceptable.
  2. Persist Mcp-Session-Id at conversation scope, not on the SSE transport object. My Capture 1 proved the connector can survive SSE reconnections — the session ID just isn’t stored durably.
  3. Send DELETE when conversations end. Zero cleanup signals means every MCP server is leaking sessions.
  4. Fix openai-agents-python Issue #924. The Agents SDK discards get_session_id entirely — this needs to be stored and exposed so that agent developers can build stateful MCP interactions.

I have raw JSONL audit logs for all three captures. I’m happy to provide them, set up a test endpoint, or get on a call. This bug makes stateful MCP servers unusable from ChatGPT, and the re-authentication prompts make even stateless servers painful to use.

I’ve been noticing this too. @SarahLacard is this still occurring for you?

I’m forced to to go through the oauth flow every few tool calls. It’s tiring.

It has gotten worse

Woah, I assumed it was a bug with my code. Now I’m sort of wishing it was
Thanks for the quick reply