anthropics / anthropics/claude-ai-mcp

Remote MCP connector starts a new session (fresh Mcp-Session-Id) on every tool call; session-scoped state never persists

Aberta
#583 5 comentários 5 reações 0 responsáveis Ver no GitHub
bug
Linguagem predominante
Sem dados de linguagem
Estrelas
471
Forks
76
Métricas de merge de PRs
Nenhum PR com merge em 30d

Descrição

### What happened?

When a stateful Streamable HTTP MCP server is connected via the Claude web, mobile, or desktop MCP connector, the client does not reuse the `Mcp-Session-Id` it receives at initialization. Before essentially every tool call it performs a fresh `initialize` handshake with no `Mcp-Session-Id` header, so the server allocates a brand new session each time.

Because the session is new on every call, any state the server scopes to the MCP session is silently lost between calls. A value written by one tool call lives in a session that the connector discards before the next call runs, so the next call sees empty session state.

Server-side request logging confirms a different, freshly generated session ID for each logical operation, and each operation is preceded by an `initialize` POST that carries no session header. The server is spec-compliant: it returns `Mcp-Session-Id` on the initialize response as defined by the Streamable HTTP transport, and it reuses that ID correctly within a single initialize-to-call round trip. The connector just never carries the ID forward into the next operation.

This looks like the same underlying problem reported in #99 (closed, "claude.ai does not forward the MCP session id to following requests"), still reproducing as of 2026-07-08. It is distinct from #450 (the inverse case: client keeps sending a stale session ID after a 404 instead of re-initializing) and from #539 (client pooling a single session across chats).

### What did you expect to happen?

Per the MCP Streamable HTTP transport spec, once the server returns an `Mcp-Session-Id` on the `initialize` response, the client should send that same `Mcp-Session-Id` header on all subsequent requests for the life of the connection, so that session-scoped server state persists across tool calls. A fresh `initialize` without a session ID should happen only for a genuinely new connection, or after the server returns 404 for a terminated session, not before every tool call.

### Steps to reproduce

1. Deploy a stateful Streamable HTTP MCP server that assigns a session ID at initialization (for example, the @modelcontextprotocol/sdk StreamableHTTPServerTransport with a sessionIdGenerator) and keeps per-session state in memory keyed by that ID.
2. Add it as a remote MCP connector in the Claude desktop or mobile app.
3. From a chat, call a tool that writes a value into session-scoped state.
4. Call a second tool that reads that value back.
5. Observe that the second call arrives under a different session ID, with no Mcp-Session-Id header sent, so the value written in step 3 is gone. Server logs show each call preceded by a new initialize with no session header.

### Area

Session / Connection Management

### MCP Server (if applicable)

Custom self-hosted remote server: Node.js, @modelcontextprotocol/sdk StreamableHTTPServerTransport (Streamable HTTP), OAuth-authenticated. The identical server works correctly from a long-lived client (Claude Code), which holds a single session.

### Error messages or logs

```shell
# Each block is ONE logical user action. Every block opens with a POST carrying
# no session (Session: none, a fresh initialize); the server issues a NEW
# session UUID; the tool call runs under it. The NEXT action repeats with a
# different UUID, so the previous session's state is never reused.

# Action 1: a tool call that writes session-scoped state
POST /mcp Session: none <- fresh initialize
POST /mcp Session: 93b76e6e-7306-4f70-8a8e-7ba2ca117ee9 <- server-issued id
tool_call -> writes state, ok

# Action 2 (~3s later): a tool call that reads it back
POST /mcp Session: none <- fresh initialize again
POST /mcp Session: f8e5c68a-68e5-402e-a9f5-79ee11cdf3fc <- DIFFERENT id, not carried over
tool_call -> session-scoped state is empty

# Every later action repeats the same way, each under a brand new UUID:
# 57a80033-... then 08c0c56c-... then fc5e0d48-... then ...
```

### Additional context

- Server transport: StreamableHTTPServerTransport with sessionIdGenerator: () => crypto.randomUUID(); per-session state kept in a Map keyed by session ID. This is the SDK's standard stateful pattern.
- Within a single initialize-to-call round trip, the two or three POSTs share the same UUID, so the server is issuing and echoing the header correctly. The connector simply does not persist it beyond that one round trip.
- Reproduces on both the Claude mobile and desktop connectors. A long-lived client (Claude Code) against the identical server keeps one session and works correctly, which points at the remote connector layer rather than the server.
- Impact: any remote MCP server that relies on session-scoped state (per-session context, multi-step handshakes, cursors, or any value set by one tool and read by another) is effectively unusable from the web, mobile, and desktop apps. Binding such state to the connection identity instead of the session is a possible workaround, but it should not be necessary if the session ID were persisted per spec.
- Related: #99 (closed, same root cause), #450 (inverse re-initialization case), #539 (session pooling across chats).

Guia de contribuição

Nenhum guia de contribuição indexado para este repositório

Avaliação

Esta issue ainda não foi avaliada.

Receba novas issues na sua caixa de entrada

Um resumo curto de issues do GitHub para quem está começando.