From 75934a33f88e5e288bfd1533bfda181444b2eb08 Mon Sep 17 00:00:00 2001 From: Minsung Son Date: Wed, 25 Feb 2026 05:35:59 +0900 Subject: [PATCH 1/2] refactor: add notification handler to stateless servers. --- .../server/McpStatelessAsyncServer.java | 12 +++- .../HttpServletStatelessIntegrationTests.java | 58 +++++++++++++++++++ 2 files changed, 69 insertions(+), 1 deletion(-) diff --git a/mcp-core/src/main/java/io/modelcontextprotocol/server/McpStatelessAsyncServer.java b/mcp-core/src/main/java/io/modelcontextprotocol/server/McpStatelessAsyncServer.java index 4eaee01fb..bf5dac201 100644 --- a/mcp-core/src/main/java/io/modelcontextprotocol/server/McpStatelessAsyncServer.java +++ b/mcp-core/src/main/java/io/modelcontextprotocol/server/McpStatelessAsyncServer.java @@ -133,10 +133,20 @@ public class McpStatelessAsyncServer { this.protocolVersions = new ArrayList<>(mcpTransport.protocolVersions()); - McpStatelessServerHandler handler = new DefaultMcpStatelessServerHandler(requestHandlers, Map.of()); + Map notificationHandlers = prepareNotificationHandlers(); + McpStatelessServerHandler handler = new DefaultMcpStatelessServerHandler(requestHandlers, notificationHandlers); mcpTransport.setMcpHandler(handler); } + private Map prepareNotificationHandlers() { + Map notificationHandlers = new HashMap<>(); + + notificationHandlers.put(McpSchema.METHOD_NOTIFICATION_INITIALIZED, (exchange, params) -> Mono.empty()); + notificationHandlers.put(McpSchema.METHOD_NOTIFICATION_ROOTS_LIST_CHANGED, (exchange, params) -> Mono.empty()); + + return notificationHandlers; + } + // --------------------------------------- // Lifecycle Management // --------------------------------------- diff --git a/mcp-test/src/test/java/io/modelcontextprotocol/server/HttpServletStatelessIntegrationTests.java b/mcp-test/src/test/java/io/modelcontextprotocol/server/HttpServletStatelessIntegrationTests.java index a792ff5e0..b8dc8c702 100644 --- a/mcp-test/src/test/java/io/modelcontextprotocol/server/HttpServletStatelessIntegrationTests.java +++ b/mcp-test/src/test/java/io/modelcontextprotocol/server/HttpServletStatelessIntegrationTests.java @@ -765,6 +765,64 @@ void testThrownMcpErrorAndJsonRpcError() throws Exception { mcpServer.close(); } + @Test + void testInitializedNotificationCallReturnsAccepted() throws Exception { + var mcpServer = McpServer.sync(mcpStatelessServerTransport) + .serverInfo("test-server", "1.0.0") + .capabilities(ServerCapabilities.builder().build()) + .build(); + + McpSchema.JSONRPCNotification notification = new McpSchema.JSONRPCNotification(McpSchema.JSONRPC_VERSION, + McpSchema.METHOD_NOTIFICATION_INITIALIZED, null); + + MockHttpServletRequest request = new MockHttpServletRequest("POST", CUSTOM_MESSAGE_ENDPOINT); + MockHttpServletResponse response = new MockHttpServletResponse(); + + byte[] content = JSON_MAPPER.writeValueAsBytes(notification); + 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("Cache-Control", "no-cache"); + request.addHeader(HttpHeaders.PROTOCOL_VERSION, ProtocolVersions.MCP_2025_03_26); + + mcpStatelessServerTransport.service(request, response); + + assertThat(response.getStatus()).isEqualTo(202); + assertThat(response.getContentAsByteArray()).isEmpty(); + + mcpServer.close(); + } + + @Test + void testRootsListChangedNotificationCallReturnsAccepted() throws Exception { + var mcpServer = McpServer.sync(mcpStatelessServerTransport) + .serverInfo("test-server", "1.0.0") + .capabilities(ServerCapabilities.builder().build()) + .build(); + + McpSchema.JSONRPCNotification notification = new McpSchema.JSONRPCNotification(McpSchema.JSONRPC_VERSION, + McpSchema.METHOD_NOTIFICATION_ROOTS_LIST_CHANGED, null); + + MockHttpServletRequest request = new MockHttpServletRequest("POST", CUSTOM_MESSAGE_ENDPOINT); + MockHttpServletResponse response = new MockHttpServletResponse(); + + byte[] content = JSON_MAPPER.writeValueAsBytes(notification); + 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("Cache-Control", "no-cache"); + request.addHeader(HttpHeaders.PROTOCOL_VERSION, ProtocolVersions.MCP_2025_03_26); + + mcpStatelessServerTransport.service(request, response); + + assertThat(response.getStatus()).isEqualTo(202); + assertThat(response.getContentAsByteArray()).isEmpty(); + + mcpServer.close(); + } + private double evaluateExpression(String expression) { // Simple expression evaluator for testing return switch (expression) { From 18fef50579a74f0d001dc0cb553f7d1f07b32533 Mon Sep 17 00:00:00 2001 From: Minsung Son Date: Sun, 5 Jul 2026 09:23:16 +0900 Subject: [PATCH 2/2] test: rework stateless server notification tests to use bundled client and assert log output --- .../HttpServletStatelessIntegrationTests.java | 103 ++++++++++-------- 1 file changed, 56 insertions(+), 47 deletions(-) diff --git a/mcp-test/src/test/java/io/modelcontextprotocol/server/HttpServletStatelessIntegrationTests.java b/mcp-test/src/test/java/io/modelcontextprotocol/server/HttpServletStatelessIntegrationTests.java index b8dc8c702..42aa51200 100644 --- a/mcp-test/src/test/java/io/modelcontextprotocol/server/HttpServletStatelessIntegrationTests.java +++ b/mcp-test/src/test/java/io/modelcontextprotocol/server/HttpServletStatelessIntegrationTests.java @@ -10,6 +10,12 @@ import java.util.concurrent.atomic.AtomicReference; import java.util.function.BiFunction; +import org.slf4j.LoggerFactory; + +import ch.qos.logback.classic.Level; +import ch.qos.logback.classic.Logger; +import ch.qos.logback.classic.spi.ILoggingEvent; +import ch.qos.logback.core.read.ListAppender; import io.modelcontextprotocol.client.McpClient; import io.modelcontextprotocol.client.transport.HttpClientStreamableHttpTransport; import io.modelcontextprotocol.common.McpTransportContext; @@ -41,7 +47,6 @@ import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; import org.junit.jupiter.api.Timeout; - import org.springframework.mock.web.MockHttpServletRequest; import org.springframework.mock.web.MockHttpServletResponse; import org.springframework.web.client.RestClient; @@ -766,61 +771,65 @@ void testThrownMcpErrorAndJsonRpcError() throws Exception { } @Test - void testInitializedNotificationCallReturnsAccepted() throws Exception { - var mcpServer = McpServer.sync(mcpStatelessServerTransport) - .serverInfo("test-server", "1.0.0") - .capabilities(ServerCapabilities.builder().build()) - .build(); - - McpSchema.JSONRPCNotification notification = new McpSchema.JSONRPCNotification(McpSchema.JSONRPC_VERSION, - McpSchema.METHOD_NOTIFICATION_INITIALIZED, null); - - MockHttpServletRequest request = new MockHttpServletRequest("POST", CUSTOM_MESSAGE_ENDPOINT); - MockHttpServletResponse response = new MockHttpServletResponse(); - - byte[] content = JSON_MAPPER.writeValueAsBytes(notification); - 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("Cache-Control", "no-cache"); - request.addHeader(HttpHeaders.PROTOCOL_VERSION, ProtocolVersions.MCP_2025_03_26); + void testInitializedNotificationDoesNotLogWarn() { + Logger handlerLogger = (Logger) LoggerFactory + .getLogger("io.modelcontextprotocol.server.DefaultMcpStatelessServerHandler"); + ListAppender logAppender = new ListAppender<>(); + logAppender.start(); + handlerLogger.addAppender(logAppender); - mcpStatelessServerTransport.service(request, response); + try { + var mcpServer = McpServer.sync(mcpStatelessServerTransport) + .serverInfo("test-server", "1.0.0") + .capabilities(ServerCapabilities.builder().build()) + .build(); - assertThat(response.getStatus()).isEqualTo(202); - assertThat(response.getContentAsByteArray()).isEmpty(); + try (var mcpClient = clientBuilder.build()) { + mcpClient.initialize(); // automatically sends notifications/initialized + } + finally { + mcpServer.close(); + } + } + finally { + handlerLogger.detachAppender(logAppender); + logAppender.stop(); + } - mcpServer.close(); + assertThat(logAppender.list).noneMatch(event -> event.getLevel() == Level.WARN + && (event.getFormattedMessage().contains(McpSchema.METHOD_NOTIFICATION_INITIALIZED) + || event.getFormattedMessage().contains("Missing handler for request type"))); } @Test - void testRootsListChangedNotificationCallReturnsAccepted() throws Exception { - var mcpServer = McpServer.sync(mcpStatelessServerTransport) - .serverInfo("test-server", "1.0.0") - .capabilities(ServerCapabilities.builder().build()) - .build(); - - McpSchema.JSONRPCNotification notification = new McpSchema.JSONRPCNotification(McpSchema.JSONRPC_VERSION, - McpSchema.METHOD_NOTIFICATION_ROOTS_LIST_CHANGED, null); - - MockHttpServletRequest request = new MockHttpServletRequest("POST", CUSTOM_MESSAGE_ENDPOINT); - MockHttpServletResponse response = new MockHttpServletResponse(); - - byte[] content = JSON_MAPPER.writeValueAsBytes(notification); - 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("Cache-Control", "no-cache"); - request.addHeader(HttpHeaders.PROTOCOL_VERSION, ProtocolVersions.MCP_2025_03_26); + void testRootsListChangedNotificationDoesNotLogWarn() { + Logger handlerLogger = (Logger) LoggerFactory.getLogger(DefaultMcpStatelessServerHandler.class); + ListAppender logAppender = new ListAppender<>(); + logAppender.start(); + handlerLogger.addAppender(logAppender); - mcpStatelessServerTransport.service(request, response); + try { + var mcpServer = McpServer.sync(mcpStatelessServerTransport) + .serverInfo("test-server", "1.0.0") + .capabilities(ServerCapabilities.builder().build()) + .build(); - assertThat(response.getStatus()).isEqualTo(202); - assertThat(response.getContentAsByteArray()).isEmpty(); + try (var mcpClient = clientBuilder.build()) { + mcpClient.initialize(); + mcpClient.rootsListChangedNotification(); + } + finally { + mcpServer.close(); + } + } + finally { + handlerLogger.detachAppender(logAppender); + logAppender.stop(); + } - mcpServer.close(); + assertThat(logAppender.list).noneMatch(event -> event.getLevel() == Level.WARN + && (event.getFormattedMessage().contains(McpSchema.METHOD_NOTIFICATION_ROOTS_LIST_CHANGED) + || event.getFormattedMessage().contains("Missing handler for request type"))); } private double evaluateExpression(String expression) {