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:
- Request header logging — Every incoming request is logged with
mcp_session_id (value or null), is_initialize flag, user_agent, and timestamp.
- Session lifecycle events —
session_created, session_closed, session_removed with active_sessions count.
- SSE disconnect tracking —
client_disconnected with elapsed_ms showing how long the SSE stream lived.
- Response header verification —
mcp_response_headers event on every res.finish() proving mcp-session-id was present in the outgoing response.
- Diagnostic response headers —
X-Lacard-Session-Active, X-Lacard-Session-Age-Ms, X-Lacard-Request-Id on every response for Cloudflare-layer visibility.
- Cache-Control hardening —
Cache-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:
POST /mcp with mcp-session-id: null, body = initialize JSON-RPC
- Server responds with
mcp-session-id: <uuid> in response header
POST /mcp with mcp-session-id: <uuid>, body = tools/list
POST /mcp with mcp-session-id: <uuid>, body = tools/call
- SSE stream closes (195-355ms after open)
- Session ID is forgotten
- 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:
- Connector sends
initialize with no mcp-session-id (because it forgot)
- If the server has session validation, it may reject the request (400 or require re-init)
- ChatGPT interprets this as an auth failure and shows the “Continue” / re-auth prompt
- 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:
- Extract
mcp-session-id from the initialize response — it already does this (proven by within-batch tool calls working).
- Store it at conversation scope — NOT on the transport/connection object. It must survive SSE disconnects.
- Replay it on ALL subsequent requests — including the next batch’s tool calls, without re-initializing.
- Send
DELETE /mcp with mcp-session-id when the conversation ends or the connector is torn down — currently never happens.
- Handle
mcp-session-id across SSE reconnections — reconnect with the existing session ID rather than starting fresh.
Reproduction Steps for OpenAI Engineering
- Set up any MCP server that logs
mcp-session-id from incoming request headers and logs session_created events with active_sessions count.
- Connect it as a ChatGPT connector (Developer Mode is fine).
- 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).
- 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.