Repository navigation
Duplicate initialize is accepted after initialization and returns a new session ID #962
Description
Bug description
After a normal Streamable HTTP initialization flow, the Java SDK server accepts later initialize requests instead of rejecting them as duplicate initialization for an already-initialized session. The duplicate request returns HTTP 200 and a normal InitializeResult; when the duplicate request uses an older protocol version, that older version is echoed in the result.
The behavior I observed is not a same-session overwrite in the Streamable HTTP transport. Instead, it behaves like fresh session creation: each duplicate initialize creates a new McpStreamableServerSession and returns a new Mcp-Session-Id, even though the client already has an active session.
From a spec perspective, the lifecycle text says initialization is the first interaction and that the operation phase should use the negotiated protocol version and capabilities. The Streamable HTTP transport also defines a session as logically related interactions beginning with initialization, and says a client should start a new session after receiving HTTP 404 for an existing session ID. I do not see text that explicitly defines how a same-endpoint duplicate initialize should be handled, so I am filing this as a lifecycle/session-boundary ambiguity rather than claiming that the spec explicitly requires rejecting duplicate initialize.
I checked the Java SDK implementation path directly:
HttpServletStreamableServerTransportProviderhandles any JSON-RPC request whose method isinitializeby callingsessionFactory.startSession(initializeRequest)and then storing the returned session underinit.session().getId().DefaultMcpStreamableServerSessionFactory#startSessioncreates a new UUID and constructs a newMcpStreamableServerSessionfrom thatInitializeRequest.McpStreamableServerSessionstoresclientCapabilitiesandclientInfofrom that request, and later createsMcpAsyncServerExchangeobjects using those values.
So the observed new session IDs are consistent with the current implementation path.
Environment
- Stable release:
v1.1.2(e9e1a2f34dedb72008d90e9919052d46eb2b701c) mainsnapshot: from 2026-05-15 (c09ee67f60260bd258b1a1aab9315a647a239d86)- Transport: Streamable HTTP server, stateful mode
- Java runtime used for repro: OpenJDK
21.0.10 - SDK build target: Java 17 (
java.version,maven.compiler.source, andmaven.compiler.targetare set to17in the SDKpom.xml)
Steps to reproduce
- Start a Java SDK Streamable HTTP server.
- Send an initial
initializerequest and save the returnedMcp-Session-Id. - Send
notifications/initializedwith that session ID. - Send a normal request such as
pingwith that session ID and observe a successful response. - Send a second
initializerequest after the server is already operational. Include the existingMcp-Session-Idheader to make clear that this is not a client intentionally starting a fresh session without a session ID. - Repeat with changed
clientInfo, changedcapabilities, or an olderprotocolVersionsuch as2024-11-05. - Observe that each later
initializereturns HTTP 200, a normalInitializeResult, and a newMcp-Session-Id.
In my logs, both the stable release and the 2026-05-15 main snapshot showed:
Second initialize after normal initialization: HTTP 200, InitializeResult returned
Duplicate initialize with changed capabilities/clientInfo: HTTP 200, InitializeResult returned
Duplicate initialize with protocolVersion 2024-11-05: HTTP 200, InitializeResult returned with protocolVersion 2024-11-05
Five repeated later initialize requests: all returned HTTP 200
A later ping still succeeded
Expected behavior
If a client already has an active Streamable HTTP session, I would expect a later initialize sent with that active Mcp-Session-Id to be rejected, for example with a JSON-RPC Invalid Request error or another clear 4xx/JSON-RPC error.
If this behavior is intentional, I would expect it to be documented explicitly, including whether servers should ignore an existing Mcp-Session-Id on an initialize request and issue a new session ID.
Minimal reproducible example
Set ENDPOINT to a Java SDK Streamable HTTP endpoint:
ENDPOINT=http://127.0.0.1:8080/mcpInitialize and copy the returned Mcp-Session-Id header into SID:
curl -i -sS --http1.1 -X POST "$ENDPOINT" \
-H 'Content-Type: application/json' \
-H 'Accept: application/json, text/event-stream' \
-H 'MCP-Protocol-Version: 2025-11-25' \
--data '{"jsonrpc":"2.0","id":"init-1","method":"initialize","params":{"protocolVersion":"2025-11-25","capabilities":{"roots":{"listChanged":true},"sampling":{}},"clientInfo":{"name":"repro-initial","version":"1.0.0"}}}'Send the initialized notification:
curl -i -sS --http1.1 -X POST "$ENDPOINT" \
-H 'Content-Type: application/json' \
-H 'Accept: application/json, text/event-stream' \
-H 'MCP-Protocol-Version: 2025-11-25' \
-H "Mcp-Session-Id: $SID" \
--data '{"jsonrpc":"2.0","method":"notifications/initialized","params":{}}'Confirm that the session is operational:
curl -i -sS --http1.1 -X POST "$ENDPOINT" \
-H 'Content-Type: application/json' \
-H 'Accept: application/json, text/event-stream' \
-H 'MCP-Protocol-Version: 2025-11-25' \
-H "Mcp-Session-Id: $SID" \
--data '{"jsonrpc":"2.0","id":"ping-1","method":"ping","params":{}}'Now send a second initialize with changed parameters on the already-operational session:
curl -i -sS --http1.1 -X POST "$ENDPOINT" \
-H 'Content-Type: application/json' \
-H 'Accept: application/json, text/event-stream' \
-H 'MCP-Protocol-Version: 2025-11-25' \
-H "Mcp-Session-Id: $SID" \
--data '{"jsonrpc":"2.0","id":"init-2","method":"initialize","params":{"protocolVersion":"2024-11-05","capabilities":{"roots":{"listChanged":false}},"clientInfo":{"name":"repro-duplicate","version":"2.0.0"}}}'Observed result:
HTTP/1.1 200 OK
Mcp-Session-Id: <new-session-id>
{"jsonrpc":"2.0","id":"init-2","result":{"protocolVersion":"2024-11-05", ...}}
I observed that the new Streamable HTTP session's clientInfo and clientCapabilities reflected the duplicate request. I did not observe a stored negotiated-protocol-version field in the Streamable HTTP session object. My concern here is the new session boundary created by a duplicate initialize after the client already had an active session.
Activity
DragonFSKY commentedon May 18, 2026
Hi, thanks for the detailed report.
I took a look at this and I think the behavior is likely caused by the Streamable HTTP initialize path handling
initializebefore checking the existingMcp-Session-Id. That means a laterinitializerequest with an active session ID can currently reachsessionFactory.startSession(...)and receive a fresh session ID.My intended fix is to keep fresh
initializerequests without a session ID working as before, but reject duplicateinitializerequests that include an existing activeMcp-Session-Idin the stateful Streamable HTTP transport, with a focused regression test for that path.If there are any concerns with this direction, please feel free to point them out.
steven-ji commentedon Aug 22, 2026
I would like to work on this issue.
I verified that the current
mainpath still handlesinitializebefore checking an existingMcp-Session-Id, and that the earlier PR #969 is closed.I will keep the change focused on rejecting a duplicate
initializerequest only when it carries an active session ID, while preserving fresh initialization without a session ID. I will also add a focused servlet-level regression test forinitialize -> initialized -> duplicate initialize -> rejected.Kehrlann commentedon Sep 4, 2026
The spec does not say anything about the initialize request and the presence of an existing session ID. What's happening here is that the session ID header is ignored entirely, and a brand new session is created.
There is no override or session leak.
Given the following tool definition:
Here's a script to verify that when you initialize a second session while passing a session ID, the initial session stays untouched: