Skip to content

Duplicate initialize is accepted after initialization and returns a new session ID #962

Closed as not planned

Description

@cclabadmin

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:

  • HttpServletStreamableServerTransportProvider handles any JSON-RPC request whose method is initialize by calling sessionFactory.startSession(initializeRequest) and then storing the returned session under init.session().getId().
  • DefaultMcpStreamableServerSessionFactory#startSession creates a new UUID and constructs a new McpStreamableServerSession from that InitializeRequest.
  • McpStreamableServerSession stores clientCapabilities and clientInfo from that request, and later creates McpAsyncServerExchange objects using those values.

So the observed new session IDs are consistent with the current implementation path.

Environment

  • Stable release: v1.1.2 (e9e1a2f34dedb72008d90e9919052d46eb2b701c)
  • main snapshot: 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, and maven.compiler.target are set to 17 in the SDK pom.xml)

Steps to reproduce

  1. Start a Java SDK Streamable HTTP server.
  2. Send an initial initialize request and save the returned Mcp-Session-Id.
  3. Send notifications/initialized with that session ID.
  4. Send a normal request such as ping with that session ID and observe a successful response.
  5. Send a second initialize request after the server is already operational. Include the existing Mcp-Session-Id header to make clear that this is not a client intentionally starting a fresh session without a session ID.
  6. Repeat with changed clientInfo, changed capabilities, or an older protocolVersion such as 2024-11-05.
  7. Observe that each later initialize returns HTTP 200, a normal InitializeResult, and a new Mcp-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/mcp

Initialize 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

  1. DragonFSKY commented on May 18, 2026

    @DragonFSKY

    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 initialize before checking the existing Mcp-Session-Id. That means a later initialize request with an active session ID can currently reach sessionFactory.startSession(...) and receive a fresh session ID.

    My intended fix is to keep fresh initialize requests without a session ID working as before, but reject duplicate initialize requests that include an existing active Mcp-Session-Id in 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.

  2. added
    bugSomething isn't working
    ready for workThe goal is clear and work towards it can be commenced
    P2Moderate issues affecting some users, edge cases, potentially valuable feature
    on Aug 17, 2026
  3. added theissue type on Aug 17, 2026
  4. steven-ji commented on Aug 22, 2026

    @steven-ji

    I would like to work on this issue.

    I verified that the current main path still handles initialize before checking an existing Mcp-Session-Id, and that the earlier PR #969 is closed.

    I will keep the change focused on rejecting a duplicate initialize request 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 for initialize -> initialized -> duplicate initialize -> rejected.

  5. Kehrlann commented on Sep 4, 2026

    @Kehrlann
    Contributor

    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:

    McpServerFeatures.SyncToolSpecification.builder()
    	.tool(Tool.builder("caps", EMPTY_JSON_SCHEMA).description("Returns client capabilities").build())
    	.callHandler((exchange, request) -> {
    		return CallToolResult.builder()
    			.addTextContent(exchange.getClientInfo().name())
    			.isError(false)
    			.build();
    	})
    	.build();

    Here's a script to verify that when you initialize a second session while passing a session ID, the initial session stays untouched:

    #!/usr/bin/env bash
    
    ENDPOINT=http://localhost:8080/mcp
    
    INIT_RESULT_ONE=$(curl -i -sS --http1.1 -X POST "$ENDPOINT" \
      -H 'Content-Type: application/json' \
      -H 'Accept: application/json, text/event-stream' \
      --data '{"jsonrpc":"2.0","id":"init-1","method":"initialize","params":{"protocolVersion":"2025-11-25","capabilities":{"roots":{"listChanged":true},"sampling":{}},"clientInfo":{"name":"repro-one","version":"1.0.0"}}}')
    echo "Initialization one"
    SID_ONE=$(echo "$INIT_RESULT_ONE" | sed -n 's/Mcp-Session-Id: \(.*\)\r/\1/p')
    echo "SID one: ${SID_ONE}"
    echo "~~~~~~~"
    
    curl -sS --http1.1 -X POST "$ENDPOINT" \
      -H 'Content-Type: application/json' \
      -H 'Accept: application/json, text/event-stream' \
      -H "Mcp-Session-Id: ${SID_ONE}" \
      --data '{"jsonrpc":"2.0","method":"notifications/initialized","params":{}}'
    echo "Initialized one"
    echo "~~~~~~~"
    
    TC_ONE=$(curl -i -sS --http1.1 -X POST "$ENDPOINT" \
      -H 'Content-Type: application/json' \
      -H 'Accept: application/json, text/event-stream' \
      -H "Mcp-Session-Id: ${SID_ONE}" \
      --data '{"jsonrpc":"2.0", "id": "tc-1", "method":"tools/call","params":{"name":"caps"}}')
    TC_ONE_SESSION_NAME=$(echo "$TC_ONE" | sed -n 's/data: \(.*\)/\1/p' | jq -r ".result.content[0].text")
    echo "Session one was initialized with name: $TC_ONE_SESSION_NAME"
    echo "~~~~~~~"
    
    
    INIT_RESULT_TWO=$(curl -i -sS --http1.1 -X POST "$ENDPOINT" \
      -H 'Content-Type: application/json' \
      -H 'Accept: application/json, text/event-stream' \
      -H "Mcp-Session-Id: ${SID_ONE}" \
      --data '{"jsonrpc":"2.0","id":"init-1","method":"initialize","params":{"protocolVersion":"2025-11-25","capabilities":{"roots":{"listChanged":true},"sampling":{}},"clientInfo":{"name":"repro-two","version":"1.0.0"}}}')
    echo "Initialization two"
    SID_TWO=$(echo "$INIT_RESULT_TWO" | sed -n 's/Mcp-Session-Id: \(.*\)\r/\1/p')
    echo "SID two: ${SID_TWO}"
    echo "~~~~~~~"
    
    curl -sS --http1.1 -X POST "$ENDPOINT" \
      -H 'Content-Type: application/json' \
      -H 'Accept: application/json, text/event-stream' \
      -H "Mcp-Session-Id: ${SID_TWO}" \
      --data '{"jsonrpc":"2.0","method":"notifications/initialized","params":{}}'
    echo "Initialized two"
    echo "~~~~~~~"
    
    TC_TWO=$(curl -i -sS --http1.1 -X POST "$ENDPOINT" \
      -H 'Content-Type: application/json' \
      -H 'Accept: application/json, text/event-stream' \
      -H "Mcp-Session-Id: ${SID_TWO}" \
      --data '{"jsonrpc":"2.0", "id": "tc-1", "method":"tools/call","params":{"name":"caps"}}')
    TC_ONE=$(curl -i -sS --http1.1 -X POST "$ENDPOINT" \
      -H 'Content-Type: application/json' \
      -H 'Accept: application/json, text/event-stream' \
      -H "Mcp-Session-Id: ${SID_ONE}" \
      --data '{"jsonrpc":"2.0", "id": "tc-1", "method":"tools/call","params":{"name":"caps"}}')
    TC_ONE_SESSION_NAME=$(echo "$TC_ONE" | sed -n 's/data: \(.*\)/\1/p' | jq -r ".result.content[0].text")
    TC_TWO_SESSION_NAME=$(echo "$TC_TWO" | sed -n 's/data: \(.*\)/\1/p' | jq -r ".result.content[0].text")
    echo "Session 1 was initialized with name: $TC_ONE_SESSION_NAME"
    echo "Session 2 was initialized with name: $TC_TWO_SESSION_NAME"
    echo "~~~~~~~"
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    P2Moderate issues affecting some users, edge cases, potentially valuable featurebugSomething isn't workinginvalidThis doesn't seem rightready for workThe goal is clear and work towards it can be commenced

    Type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions