From 7e2a7a5a63bed0b868f8e04e550e9f212b219975 Mon Sep 17 00:00:00 2001 From: dragonfsky Date: Mon, 18 May 2026 14:13:53 +0800 Subject: [PATCH] fix: reject duplicate streamable HTTP initialize requests --- ...vletStreamableServerTransportProvider.java | 9 ++ ...treamableServerTransportProviderTests.java | 87 +++++++++++++++++++ 2 files changed, 96 insertions(+) create mode 100644 mcp-test/src/test/java/io/modelcontextprotocol/server/transport/HttpServletStreamableServerTransportProviderTests.java diff --git a/mcp-core/src/main/java/io/modelcontextprotocol/server/transport/HttpServletStreamableServerTransportProvider.java b/mcp-core/src/main/java/io/modelcontextprotocol/server/transport/HttpServletStreamableServerTransportProvider.java index 9a785e150..b0b059fa4 100644 --- a/mcp-core/src/main/java/io/modelcontextprotocol/server/transport/HttpServletStreamableServerTransportProvider.java +++ b/mcp-core/src/main/java/io/modelcontextprotocol/server/transport/HttpServletStreamableServerTransportProvider.java @@ -447,6 +447,15 @@ protected void doPost(HttpServletRequest request, HttpServletResponse response) return; } + String sessionId = request.getHeader(HttpHeaders.MCP_SESSION_ID); + if (sessionId != null && this.sessions.containsKey(sessionId)) { + this.responseError(response, HttpServletResponse.SC_BAD_REQUEST, + McpError.builder(McpSchema.ErrorCodes.INVALID_REQUEST) + .message("Duplicate initialize request for active session: " + sessionId) + .build()); + return; + } + McpSchema.InitializeRequest initializeRequest = jsonMapper.convertValue(jsonrpcRequest.params(), new TypeRef() { }); diff --git a/mcp-test/src/test/java/io/modelcontextprotocol/server/transport/HttpServletStreamableServerTransportProviderTests.java b/mcp-test/src/test/java/io/modelcontextprotocol/server/transport/HttpServletStreamableServerTransportProviderTests.java new file mode 100644 index 000000000..9d69eac1b --- /dev/null +++ b/mcp-test/src/test/java/io/modelcontextprotocol/server/transport/HttpServletStreamableServerTransportProviderTests.java @@ -0,0 +1,87 @@ +/* + * Copyright 2026-2026 the original author or authors. + */ + +package io.modelcontextprotocol.server.transport; + +import io.modelcontextprotocol.server.McpServer; +import io.modelcontextprotocol.spec.HttpHeaders; +import io.modelcontextprotocol.spec.McpSchema; +import io.modelcontextprotocol.spec.ProtocolVersions; +import org.junit.jupiter.api.Test; + +import org.springframework.mock.web.MockHttpServletRequest; +import org.springframework.mock.web.MockHttpServletResponse; + +import static io.modelcontextprotocol.server.transport.HttpServletStreamableServerTransportProvider.APPLICATION_JSON; +import static io.modelcontextprotocol.server.transport.HttpServletStreamableServerTransportProvider.TEXT_EVENT_STREAM; +import static io.modelcontextprotocol.util.McpJsonMapperUtils.JSON_MAPPER; +import static org.assertj.core.api.Assertions.assertThat; + +class HttpServletStreamableServerTransportProviderTests { + + private static final String MESSAGE_ENDPOINT = "/mcp/message"; + + @Test + void shouldRejectDuplicateInitializeForActiveSession() throws Exception { + var transportProvider = HttpServletStreamableServerTransportProvider.builder() + .mcpEndpoint(MESSAGE_ENDPOINT) + .build(); + var server = McpServer.sync(transportProvider).serverInfo("test-server", "1.0.0").build(); + + try { + MockHttpServletResponse initialResponse = initialize(transportProvider, null, "init-1", + ProtocolVersions.MCP_2025_11_25, "initial-client"); + assertThat(initialResponse.getStatus()).isEqualTo(200); + + String sessionId = initialResponse.getHeader(HttpHeaders.MCP_SESSION_ID); + assertThat(sessionId).isNotBlank(); + + MockHttpServletResponse initializedResponse = sendMessage(transportProvider, sessionId, + new McpSchema.JSONRPCNotification(McpSchema.METHOD_NOTIFICATION_INITIALIZED), + ProtocolVersions.MCP_2025_11_25); + assertThat(initializedResponse.getStatus()).isEqualTo(202); + + MockHttpServletResponse duplicateResponse = initialize(transportProvider, sessionId, "init-2", + ProtocolVersions.MCP_2024_11_05, "duplicate-client"); + + assertThat(duplicateResponse.getStatus()).isEqualTo(400); + assertThat(duplicateResponse.getHeader(HttpHeaders.MCP_SESSION_ID)).isNull(); + assertThat(duplicateResponse.getContentAsString()).contains("Duplicate initialize"); + } + finally { + server.closeGracefully(); + } + } + + private static MockHttpServletResponse initialize(HttpServletStreamableServerTransportProvider transportProvider, + String sessionId, String requestId, String protocolVersion, String clientName) throws Exception { + McpSchema.InitializeRequest initializeRequest = McpSchema.InitializeRequest + .builder(protocolVersion, McpSchema.ClientCapabilities.builder().roots(true).build(), + McpSchema.Implementation.builder(clientName, "1.0.0").build()) + .build(); + McpSchema.JSONRPCRequest jsonRpcRequest = new McpSchema.JSONRPCRequest(McpSchema.METHOD_INITIALIZE, requestId, + initializeRequest); + return sendMessage(transportProvider, sessionId, jsonRpcRequest, protocolVersion); + } + + private static MockHttpServletResponse sendMessage(HttpServletStreamableServerTransportProvider transportProvider, + String sessionId, McpSchema.JSONRPCMessage message, String protocolVersion) throws Exception { + byte[] content = JSON_MAPPER.writeValueAsBytes(message); + + MockHttpServletRequest request = new MockHttpServletRequest("POST", MESSAGE_ENDPOINT); + request.setContent(content); + request.addHeader("Content-Type", APPLICATION_JSON); + request.addHeader("Content-Length", Integer.toString(content.length)); + request.addHeader("Accept", APPLICATION_JSON + ", " + TEXT_EVENT_STREAM); + request.addHeader(HttpHeaders.PROTOCOL_VERSION, protocolVersion); + if (sessionId != null) { + request.addHeader(HttpHeaders.MCP_SESSION_ID, sessionId); + } + + MockHttpServletResponse response = new MockHttpServletResponse(); + transportProvider.service(request, response); + return response; + } + +}