No session/conversation identifier sent to MCP servers — cannot distinguish concurrent sessions
Status Open
Maintainer reply None cached
Workaround ✓ Mentioned in thread ↓
Activity 17 comments · opened Apr 1, 2026
Description
When Claude Code, Claude Desktop, or claude.ai connects to an HTTP MCP server, there is no way for the server to identify which conversation or session a request belongs to. This makes it impossible to maintain per-conversation state on the server side.
The Problem
Mcp-Session-Idnot sent back: The server returnsmcp-session-idin theinitializeresponse header (per MCP spec), but Claude does not echo it back on subsequent requests. Server logs showsession_id_from_header=Noneon all post-init requests.
- No conversation/instance identifier: Request headers contain only standard HTTP headers (
accept,content-type,mcp-protocol-version,user-agent: Claude-User) plus tracing headers (traceparent,baggage). No conversation ID, instance ID, or session reference.
clientInfois generic: Theinitializerequest'sclientInfois always{"name": "claude-ai", "version": "0.1.0"}with no per-instance differentiation. The MCPImplementationschema doesn't include session/instance fields.
Impact
- Multiple concurrent users/conversations: With multiple Claude instances hitting the same MCP server (common in team settings), the server cannot distinguish requests from different conversations
- Per-conversation state: Cannot track what context has been delivered to which conversation (e.g., instructions, onboarding flows)
- Session management: The MCP spec defines
Mcp-Session-Idfor this purpose, but it doesn't work because the client doesn't send it back - Fargate/Lambda deployments: Without session affinity, requests from the same conversation may hit different server instances, making in-memory state useless
Expected Behavior
At minimum, Claude should echo back the Mcp-Session-Id header that the server assigns during initialize, as required by the MCP Streamable HTTP transport specification:
"If the server responds with a Mcp-Session-Id header, the client MUST include it on all subsequent requests."
Ideally, Claude would also include a conversation/instance identifier (e.g., X-Claude-Conversation-Id or a field in clientInfo) so servers can distinguish concurrent sessions even across server restarts.
Environment
- Claude Code v2.1.87
- Claude Desktop (claude.ai)
- MCP Protocol Version: 2025-11-25
- Confirmed by inspecting all request headers server-side
Related Issues
- modelcontextprotocol/modelcontextprotocol#231 (API/Client-defined session IDs)
- anthropics/claude-code#25642 (Expose $CLAUDE_SESSION_ID)
- anthropics/claude-code#27142 (Stale Mcp-Session-Id bug)
🤖 Generated with Claude Code
17 Comments
This is much needed. ChatGPT uses meta fields with openai/session to track each session/conversation.
I think this is the much needed ticket.
This is so needed. I'm also running a skill to write history per session, and Claude cannot reliably get the sessionID
I need this desperately. Core part of the MCP spec
I found that the HTTP headers contain an mcp-session-id BUT this is more like a user sessionId over all platforms. I tested the same MCP via Desktop, Web App, and mobile. And the MCP session ID was the same for all different chats, only changed if I did a hard refresh or logged out and in again.
OpenAI and ChatGPT cover session and user id properly, why doesn't Anthropic with Claude?
+1 with empirical update — Claude Code CLI 2.1.128 DOES echo
Mcp-Session-Idper request (May 12, 2026)Building a remote MCP server with a SessionStart hook that opens a server-side audit-row carrying the conversation UUID (Claude Code passes it to the hook via stdin). Hit the same identification gap. Deployed a debug header logger today to verify current behavior:
So part of the original report may be out-of-date or platform-specific — Claude Code CLI does echo
mcp-session-idon every request per spec. Distinct MCP clients get distinct session-ids in our testing: this conversation gotd9be0553...; our separate Python-based bot got21af5589....Unresolved: @jdugueto's observation about Desktop/Web/mobile sharing
mcp-session-idacross chats is critical for server-side correlation strategies. CLI's cross-conversation behavior unverified (eachclaudeinvocation is a new process, presumed unique). If anyone has tested two simultaneous Claude Code CLI conversations against the same MCP server, would love confirmation.Server-side workaround we shipped today: first-write binding — server stashes the row's
mcp_session_idon the agent's first audited write (when exactly ONE recent unbound operator row exists). One-shot, ambiguity-refusing. Works for the per-conversation-unique-sid case; would break under the shared-across-chats pattern.What would solve this without server-side heuristics: a per-request
X-Claude-Conversation-Idheader carrying the UUID Claude Code already passes to SessionStart hook stdin. Stable handle, survives MCP reconnections, no spec changes needed.Any update on this? our mcp depends on sessions being per conversation, and this is really making claude harder to adopt
This is an important distinction:
Mcp-Session-Idand "conversation/session identity" are related but not the same contract.Mcp-Session-Idcan solve transport affinity for one MCP client connection. It does not automatically give the server a stable product-level conversation id, nor does it identify which Claude Code process, web tab, desktop window, or mobile client is making the request.For server-side state, I would want at least two concepts to be explicit:
If Desktop/Web/mobile share an MCP session id across different chats, that is dangerous for per-conversation state. If every reconnect gets a new id, that is also hard for resumability. The clean contract is probably: echo MCP session id for protocol/session affinity, and separately expose a stable conversation/run id when product semantics require state isolation.
Wanted to check in on this issue again. Currently, this makes it impossible to store session metadata associated with a specific conversation. It's especially problematic in multi-tenancy apps where the active tenant can change without any indication because of a separate session on the device.
The
Mcp-Session-Idgap you've documented is one of the harder operational problems in multi-agent setups — especially when multiple concurrent Claude instances all look identical to your server. We hit the same wall building the Claudiverse polling runner.Our current workaround: inject a synthetic
X-Cycle-Idheader via a lightweight HTTP proxy that sits between Claude Code and our MCP servers. The proxy reads thecycle_idwe embed in every agent's environment at spawn time and forwards it as a custom header. Not elegant, but it gives the server enough correlation to maintain per-session state without waiting for the spec to land.The spec argument (Streamable HTTP transport requiring the client to echo
Mcp-Session-Idback) is correct and should be the real fix. The proxy approach is a stopgap for teams who need session affinity today.Two concrete use cases where this matters practically:
@kylegwalsh's June 8 comment about session metadata storage maps to exactly this — the round-trip contract is broken even before you get to multi-instance scenarios.
Flagging for visibility: this effectively makes HTTP MCP servers stateless-only, which rules out a large class of useful server designs until the spec compliance fix lands.
Any update on this?
We use a system whereby an admin user can "assume the identity" of a customer within the context of a conversation. Without a proper session ID, a user attempting to work in parallel would end up with cross-talk between those two sessions, and end up modifying the wrong account.
So my users have to choose: Work with AI, but only with a single customer at a time, or work without AI, across as many as you like.
This is a severe limitation to the usefulness of our MCP and other tooling.
Filed anthropics/claude-ai-mcp#676 as a concrete, higher-severity consequence of this same gap: on claude.ai web, when two chats invoke the same remote-connector tool in parallel across tabs, one chat's tool result is rendered in the other chat — a silent wrong answer, no error.
On our side we confirmed each tool call was processed independently and every stored session is single-topic and correct; our server is request-isolated and echoes the JSON-RPC request
idunchanged. So the crossing is purely client-side response routing. Because concurrent conversations aren't distinguishable (no echoedMcp-Session-Id, no per-conversation identifier as described here) and it only manifests when the same tool is called concurrently, correlation appears to be keyed on something other than the JSON-RPCid. +1 — this needs the conversation identifier the spec requires.So there is an extremely ugly workaround to Claude Code not sending session id in HTTP MCP requests:
$MCP_STARTUP_IDon viaheadersHelper.headersHelperwrite$MCP_STARTUP_IDand$PPID(parent process id i.e. the claude code process) to a fileheadersHelperreturn{"x-claude-code-mcp-startup-id":$MCP_STARTUP_ID}, so that thex-claude-code-mcp-startup-idheader is set on every http request from the MCPSessionStarthook which$PPID(also the ID of the claude code process)session_idfrom the payload on stdin (e.g.CLAUDE_CODE_SESSION_ID=$(echo "$STDIN" | jq -r .session_id))$MCP_STARTUP_IDwith$CLAUDE_CODE_SESSION_ID.$MCP_STARTUP_IDwith$CLAUDE_CODE_SESSION_IDon your HTTP MCP serverNote that
/compact,/clear, and/resumeeach do fire offSessionStartbut do not refresh MCP servers - the Claude Code session associated with an MCP_STARTUP_ID is the most recent one which shares a PPID.This is an awful janky workaround that only works because Claude Code happens to directly launch the
headersHelperand the SessionStart hook handler directly, so they both share a parent process id. I will be extremely happy to get rid of this hack if Anthropic ever fixes this, but I'm not optimistic that Anthropic will ever fix this.I have a repository here with a plugin and an MCP server which, together, demonstrate that the MCP server can correlate tool calls with the session id they came from when the Claude Code session using the MCP loads the plugin. If you need this, I recommend having your Claude Code instance look at this example repository and then build to your needs.
+1 with a concrete data-loss case from a stdio MCP server — so the
Mcp-Session-IdHTTP-header discussion above doesn't apply to us; we need a stdio-visible identifier.biff is a team-communication MCP server. It routes directed messages on a per-session address,
{user}:{tty}, wherettyis assigned at server startup. Onclaude --resumethe server is a new process and re-derives a differenttty, because it has no stable session identity to key on:CLAUDE_PROJECT_DIR(noCLAUDE_SESSION_ID);initializeclientInfocarries only name + version;session_idis available only to theSessionStarthook's stdin, never to the server itself.The result is silent, hard-to-debug message loss and misrouting across a resume:
{user}:tty1lands in an inbox that no live session drains.ttyvalues are recycled, so a later, different session can claim the freed number and receive messages meant for the prior occupant.Ask: expose to MCP servers — stdio included, via an env var or the
initializeparams — a conversation/run identifier that is stable acrossclaude --resume; or, failing that, document whether the existingsession_idis guaranteed stable across--resume. Either would let theSessionStart-hook + shared-PPID correlation (@JoshuaDavid's technique above) deterministically reclaim the prior identity. Today the server cannot know it is the same logical session after a resume, which turns a routing key into a source of lost mail.@jmf-pobox stdio mcp servers do not restart when you
/compact,/resume, or/clear, and you can't reliably change the env of a running process (especially since many mcps probably read env on startup and never again).@JoshuaDavid Thank you... We ended up implementing your SessionStart-hook + shared-parent technique, and it resolves it:
So we're unblocked today — thanks for the recipe. The remaining ask is just to remove the awkwardness: a stdio-visible, resume-stable identifier (an env var refreshed at each SessionStart, or a field in the initialize params) would let servers skip the hook + PPID-walk bridge entirely. A nice-to-have, not a blocker.