No session/conversation identifier sent to MCP servers — cannot distinguish concurrent sessions

Status Open
Maintainer reply None cached
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

  1. Mcp-Session-Id not sent back: The server returns mcp-session-id in the initialize response header (per MCP spec), but Claude does not echo it back on subsequent requests. Server logs show session_id_from_header=None on all post-init requests.
  1. 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.
  1. clientInfo is generic: The initialize request's clientInfo is always {"name": "claude-ai", "version": "0.1.0"} with no per-instance differentiation. The MCP Implementation schema 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-Id for 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

View original on GitHub ↗

17 Comments

jdugueto · 5 months ago

This is much needed. ChatGPT uses meta fields with openai/session to track each session/conversation.

hbsardhara09 · 4 months ago

I think this is the much needed ticket.

lucasmaffazioli · 4 months ago

This is so needed. I'm also running a skill to write history per session, and Claude cannot reliably get the sessionID

chase-crumbaugh · 4 months ago

I need this desperately. Core part of the MCP spec

jdugueto · 4 months ago

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?

vinjaune89 · 3 months ago

+1 with empirical update — Claude Code CLI 2.1.128 DOES echo Mcp-Session-Id per 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:

GET /mcp
user-agent: claude-code/2.1.128 (claude-desktop, agent-sdk/0.2.128)
mcp-protocol-version: 2025-11-25
mcp-session-id: d9be0553f3224cb79152a1d5c6b7f902

So part of the original report may be out-of-date or platform-specific — Claude Code CLI does echo mcp-session-id on every request per spec. Distinct MCP clients get distinct session-ids in our testing: this conversation got d9be0553...; our separate Python-based bot got 21af5589....

Unresolved: @jdugueto's observation about Desktop/Web/mobile sharing mcp-session-id across chats is critical for server-side correlation strategies. CLI's cross-conversation behavior unverified (each claude invocation 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_id on 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-Id header carrying the UUID Claude Code already passes to SessionStart hook stdin. Stable handle, survives MCP reconnections, no spec changes needed.

martinbeiro · 3 months ago

Any update on this? our mcp depends on sessions being per conversation, and this is really making claude harder to adopt

Snailflyer · 3 months ago

This is an important distinction: Mcp-Session-Id and "conversation/session identity" are related but not the same contract.

Mcp-Session-Id can 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:

  1. conversation/run identity: stable across requests that belong to the same Claude conversation or Claude Code run;
  2. client instance identity: distinct for web, Desktop, mobile, CLI, or restarted clients attached to that conversation.

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.

kylegwalsh · 2 months ago

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.

kcarriedo · 2 months ago

The Mcp-Session-Id gap 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-Id header via a lightweight HTTP proxy that sits between Claude Code and our MCP servers. The proxy reads the cycle_id we 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-Id back) 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:

  • Stateful tool servers (e.g., a search server that tracks what context has been sent to which conversation to avoid redundant retrieval)
  • Team/org deployments where N developers hit the same MCP server concurrently and per-user state isolation is a security boundary, not just a UX nicety

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

RachelNeriyaSchifter · 1 month ago

Any update on this?

antony · 1 month ago

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.

SushanthK07 · 1 month ago

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 id unchanged. So the crossing is purely client-side response routing. Because concurrent conversations aren't distinguishable (no echoed Mcp-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-RPC id. +1 — this needs the conversation identifier the spec requires.

JoshuaDavid · 1 month ago

So there is an extremely ugly workaround to Claude Code not sending session id in HTTP MCP requests:

  1. Have your HTTP mcp mint a random unique identifier $MCP_STARTUP_ID on via headersHelper.
  2. Have headersHelper write $MCP_STARTUP_ID and $PPID (parent process id i.e. the claude code process) to a file
  3. Have headersHelper return {"x-claude-code-mcp-startup-id":$MCP_STARTUP_ID}, so that the x-claude-code-mcp-startup-id header is set on every http request from the MCP
  4. Add a SessionStart hook which
  5. Reads $PPID (also the ID of the claude code process)
  6. Reads session_id from the payload on stdin (e.g. CLAUDE_CODE_SESSION_ID=$(echo "$STDIN" | jq -r .session_id))
  7. Sends an HTTP request to the server handling MCP correlating $MCP_STARTUP_ID with $CLAUDE_CODE_SESSION_ID.
  8. Handle the requests which correlate $MCP_STARTUP_ID with $CLAUDE_CODE_SESSION_ID on your HTTP MCP server

Note that /compact, /clear, and /resume each do fire off SessionStart but 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 headersHelper and 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.

jmf-pobox · 1 month ago

+1 with a concrete data-loss case from a stdio MCP server — so the Mcp-Session-Id HTTP-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}, where tty is assigned at server startup. On claude --resume the server is a new process and re-derives a different tty, because it has no stable session identity to key on:

  • env exposes only CLAUDE_PROJECT_DIR (no CLAUDE_SESSION_ID);
  • the MCP initialize clientInfo carries only name + version;
  • session_id is available only to the SessionStart hook's stdin, never to the server itself.

The result is silent, hard-to-debug message loss and misrouting across a resume:

  • Lost — a message addressed to the pre-resume {user}:tty1 lands in an inbox that no live session drains.
  • Misroutedtty values 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 initialize params — a conversation/run identifier that is stable across claude --resume; or, failing that, document whether the existing session_id is guaranteed stable across --resume. Either would let the SessionStart-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.

JoshuaDavid · 1 month ago

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

jmf-pobox · 1 month ago

@JoshuaDavid Thank you... We ended up implementing your SessionStart-hook + shared-parent technique, and it resolves it:

  • The SessionStart hook reads session_id from stdin and writes ~/.../sessions/{claude_pid}.json; the MCP server walks the process tree to the topmost claude ancestor (Claude Code interposes an intermediate process, so the direct PPID isn't reliable) and reads the hint back.
  • We route on the session_id itself rather than a startup-assigned name, so resume keeps the same inbox and a fork gets a new one for free.
  • Stale-hint safety without a time window: the hint is validated against (pid, process-start-time) via psutil's create_time(), so a reused PID with a different start time is rejected — which matters because a long-gap resume is legitimate, not stale.

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.