diff --git a/mcp-spring/mcp-spring-webflux/src/main/java/io/modelcontextprotocol/client/transport/WebFluxSseClientTransport.java b/mcp-spring/mcp-spring-webflux/src/main/java/io/modelcontextprotocol/client/transport/WebFluxSseClientTransport.java index 37abe295..238e7881 100644 --- a/mcp-spring/mcp-spring-webflux/src/main/java/io/modelcontextprotocol/client/transport/WebFluxSseClientTransport.java +++ b/mcp-spring/mcp-spring-webflux/src/main/java/io/modelcontextprotocol/client/transport/WebFluxSseClientTransport.java @@ -208,8 +208,8 @@ else if (MESSAGE_EVENT_TYPE.equals(event.event())) { JSONRPCMessage message = McpSchema.deserializeJsonRpcMessage(this.objectMapper, event.data()); s.next(message); } - catch (IOException ioException) { - s.error(ioException); + catch (RuntimeException ioOrIllegalException) { + s.error(ioOrIllegalException); } } else { diff --git a/mcp-spring/mcp-spring-webflux/src/main/java/io/modelcontextprotocol/server/transport/WebFluxSseServerTransportProvider.java b/mcp-spring/mcp-spring-webflux/src/main/java/io/modelcontextprotocol/server/transport/WebFluxSseServerTransportProvider.java index 62264d9a..46c3122c 100644 --- a/mcp-spring/mcp-spring-webflux/src/main/java/io/modelcontextprotocol/server/transport/WebFluxSseServerTransportProvider.java +++ b/mcp-spring/mcp-spring-webflux/src/main/java/io/modelcontextprotocol/server/transport/WebFluxSseServerTransportProvider.java @@ -1,6 +1,7 @@ package io.modelcontextprotocol.server.transport; import java.io.IOException; +import java.io.UncheckedIOException; import java.util.Map; import java.util.concurrent.ConcurrentHashMap; @@ -323,8 +324,8 @@ private Mono handleMessage(ServerRequest request) { .bodyValue(new McpError(error.getMessage())); }); } - catch (IllegalArgumentException | IOException e) { - logger.error("Failed to deserialize message: {}", e.getMessage()); + catch (IllegalArgumentException | UncheckedIOException e) { + logger.error("Failed to deserialize message", e); return ServerResponse.badRequest().bodyValue(new McpError("Invalid message format")); } }); diff --git a/mcp-spring/mcp-spring-webmvc/src/main/java/io/modelcontextprotocol/server/transport/WebMvcSseServerTransportProvider.java b/mcp-spring/mcp-spring-webmvc/src/main/java/io/modelcontextprotocol/server/transport/WebMvcSseServerTransportProvider.java index 7bd1aa6c..114eff60 100644 --- a/mcp-spring/mcp-spring-webmvc/src/main/java/io/modelcontextprotocol/server/transport/WebMvcSseServerTransportProvider.java +++ b/mcp-spring/mcp-spring-webmvc/src/main/java/io/modelcontextprotocol/server/transport/WebMvcSseServerTransportProvider.java @@ -6,7 +6,6 @@ import java.io.IOException; import java.time.Duration; -import java.util.Map; import java.util.UUID; import java.util.concurrent.ConcurrentHashMap; @@ -300,7 +299,7 @@ private ServerResponse handleMessage(ServerRequest request) { return ServerResponse.status(HttpStatus.SERVICE_UNAVAILABLE).body("Server is shutting down"); } - if (!request.param("sessionId").isPresent()) { + if (request.param("sessionId").isEmpty()) { return ServerResponse.badRequest().body(new McpError("Session ID missing in message endpoint")); } diff --git a/mcp-spring/mcp-spring-webmvc/src/test/java/io/modelcontextprotocol/server/TomcatTestUtil.java b/mcp-spring/mcp-spring-webmvc/src/test/java/io/modelcontextprotocol/server/TomcatTestUtil.java index ccf9e2d7..024bb53e 100644 --- a/mcp-spring/mcp-spring-webmvc/src/test/java/io/modelcontextprotocol/server/TomcatTestUtil.java +++ b/mcp-spring/mcp-spring-webmvc/src/test/java/io/modelcontextprotocol/server/TomcatTestUtil.java @@ -3,10 +3,6 @@ */ package io.modelcontextprotocol.server; -import java.io.IOException; -import java.net.InetSocketAddress; -import java.net.ServerSocket; - import org.apache.catalina.Context; import org.apache.catalina.startup.Tomcat; @@ -53,15 +49,8 @@ public static TomcatServer createTomcatServer(String contextPath, int port, Clas wrapper.setAsyncSupported(true); context.addServletMappingDecoded("/*", "dispatcherServlet"); - try { - // Configure and start the connector with async support - var connector = tomcat.getConnector(); - connector.setAsyncTimeout(3000); // 3 seconds timeout for async requests - } - catch (Exception e) { - throw new RuntimeException("Failed to start Tomcat", e); - } - + var connector = tomcat.getConnector(); + connector.setAsyncTimeout(3000); // 3 seconds timeout for async requests return new TomcatServer(tomcat, appContext); } diff --git a/mcp-spring/mcp-spring-webmvc/src/test/java/io/modelcontextprotocol/server/WebMvcSseAsyncServerTransportTests.java b/mcp-spring/mcp-spring-webmvc/src/test/java/io/modelcontextprotocol/server/WebMvcSseAsyncServerTransportTests.java index 6a6ad17e..941bf6ba 100644 --- a/mcp-spring/mcp-spring-webmvc/src/test/java/io/modelcontextprotocol/server/WebMvcSseAsyncServerTransportTests.java +++ b/mcp-spring/mcp-spring-webmvc/src/test/java/io/modelcontextprotocol/server/WebMvcSseAsyncServerTransportTests.java @@ -90,10 +90,6 @@ protected McpServerTransportProvider createMcpTransportProvider() { return transportProvider; } - @Override - protected void onStart() { - } - @Override protected void onClose() { if (transportProvider != null) { diff --git a/mcp-spring/mcp-spring-webmvc/src/test/java/io/modelcontextprotocol/server/WebMvcSseSyncServerTransportTests.java b/mcp-spring/mcp-spring-webmvc/src/test/java/io/modelcontextprotocol/server/WebMvcSseSyncServerTransportTests.java index 1964703c..44d41fe5 100644 --- a/mcp-spring/mcp-spring-webmvc/src/test/java/io/modelcontextprotocol/server/WebMvcSseSyncServerTransportTests.java +++ b/mcp-spring/mcp-spring-webmvc/src/test/java/io/modelcontextprotocol/server/WebMvcSseSyncServerTransportTests.java @@ -89,10 +89,6 @@ protected WebMvcSseServerTransportProvider createMcpTransportProvider() { return transportProvider; } - @Override - protected void onStart() { - } - @Override protected void onClose() { if (transportProvider != null) { diff --git a/mcp-test/src/main/java/io/modelcontextprotocol/server/AbstractMcpSyncServerTests.java b/mcp-test/src/main/java/io/modelcontextprotocol/server/AbstractMcpSyncServerTests.java index c81e638c..ccabc44c 100644 --- a/mcp-test/src/main/java/io/modelcontextprotocol/server/AbstractMcpSyncServerTests.java +++ b/mcp-test/src/main/java/io/modelcontextprotocol/server/AbstractMcpSyncServerTests.java @@ -77,14 +77,14 @@ void testConstructorWithInvalidArguments() { void testGracefulShutdown() { var mcpSyncServer = McpServer.sync(createMcpTransportProvider()).serverInfo("test-server", "1.0.0").build(); - assertThatCode(() -> mcpSyncServer.closeGracefully()).doesNotThrowAnyException(); + assertThatCode(mcpSyncServer::closeGracefully).doesNotThrowAnyException(); } @Test void testImmediateClose() { var mcpSyncServer = McpServer.sync(createMcpTransportProvider()).serverInfo("test-server", "1.0.0").build(); - assertThatCode(() -> mcpSyncServer.close()).doesNotThrowAnyException(); + assertThatCode(mcpSyncServer::close).doesNotThrowAnyException(); } @Test @@ -93,7 +93,7 @@ void testGetAsyncServer() { assertThat(mcpSyncServer.getAsyncServer()).isNotNull(); - assertThatCode(() -> mcpSyncServer.closeGracefully()).doesNotThrowAnyException(); + assertThatCode(mcpSyncServer::closeGracefully).doesNotThrowAnyException(); } // --------------------------------------- @@ -138,7 +138,7 @@ void testAddDuplicateTool() { .isInstanceOf(McpError.class) .hasMessage("Tool with name '" + TEST_TOOL_NAME + "' already exists"); - assertThatCode(() -> mcpSyncServer.closeGracefully()).doesNotThrowAnyException(); + assertThatCode(mcpSyncServer::closeGracefully).doesNotThrowAnyException(); } @Test @@ -153,7 +153,7 @@ void testRemoveTool() { assertThatCode(() -> mcpSyncServer.removeTool(TEST_TOOL_NAME)).doesNotThrowAnyException(); - assertThatCode(() -> mcpSyncServer.closeGracefully()).doesNotThrowAnyException(); + assertThatCode(mcpSyncServer::closeGracefully).doesNotThrowAnyException(); } @Test @@ -173,9 +173,9 @@ void testRemoveNonexistentTool() { void testNotifyToolsListChanged() { var mcpSyncServer = McpServer.sync(createMcpTransportProvider()).serverInfo("test-server", "1.0.0").build(); - assertThatCode(() -> mcpSyncServer.notifyToolsListChanged()).doesNotThrowAnyException(); + assertThatCode(mcpSyncServer::notifyToolsListChanged).doesNotThrowAnyException(); - assertThatCode(() -> mcpSyncServer.closeGracefully()).doesNotThrowAnyException(); + assertThatCode(mcpSyncServer::closeGracefully).doesNotThrowAnyException(); } // --------------------------------------- @@ -186,9 +186,9 @@ void testNotifyToolsListChanged() { void testNotifyResourcesListChanged() { var mcpSyncServer = McpServer.sync(createMcpTransportProvider()).serverInfo("test-server", "1.0.0").build(); - assertThatCode(() -> mcpSyncServer.notifyResourcesListChanged()).doesNotThrowAnyException(); + assertThatCode(mcpSyncServer::notifyResourcesListChanged).doesNotThrowAnyException(); - assertThatCode(() -> mcpSyncServer.closeGracefully()).doesNotThrowAnyException(); + assertThatCode(mcpSyncServer::closeGracefully).doesNotThrowAnyException(); } @Test @@ -219,7 +219,7 @@ void testAddResourceWithNullSpecification() { .isInstanceOf(McpError.class) .hasMessage("Resource must not be null"); - assertThatCode(() -> mcpSyncServer.closeGracefully()).doesNotThrowAnyException(); + assertThatCode(mcpSyncServer::closeGracefully).doesNotThrowAnyException(); } @Test @@ -312,7 +312,7 @@ void testRemovePrompt() { assertThatCode(() -> mcpSyncServer.removePrompt(TEST_PROMPT_NAME)).doesNotThrowAnyException(); - assertThatCode(() -> mcpSyncServer.closeGracefully()).doesNotThrowAnyException(); + assertThatCode(mcpSyncServer::closeGracefully).doesNotThrowAnyException(); } @Test @@ -325,7 +325,7 @@ void testRemoveNonexistentPrompt() { assertThatThrownBy(() -> mcpSyncServer.removePrompt("nonexistent-prompt")).isInstanceOf(McpError.class) .hasMessage("Prompt with name 'nonexistent-prompt' not found"); - assertThatCode(() -> mcpSyncServer.closeGracefully()).doesNotThrowAnyException(); + assertThatCode(mcpSyncServer::closeGracefully).doesNotThrowAnyException(); } // --------------------------------------- @@ -366,7 +366,7 @@ void testRootsChangeHandlers() { .build(); assertThat(multipleConsumersServer).isNotNull(); - assertThatCode(() -> multipleConsumersServer.closeGracefully()).doesNotThrowAnyException(); + assertThatCode(multipleConsumersServer::closeGracefully).doesNotThrowAnyException(); onClose(); // Test error handling @@ -378,14 +378,14 @@ void testRootsChangeHandlers() { .build(); assertThat(errorHandlingServer).isNotNull(); - assertThatCode(() -> errorHandlingServer.closeGracefully()).doesNotThrowAnyException(); + assertThatCode(errorHandlingServer::closeGracefully).doesNotThrowAnyException(); onClose(); // Test without consumers var noConsumersServer = McpServer.sync(createMcpTransportProvider()).serverInfo("test-server", "1.0.0").build(); assertThat(noConsumersServer).isNotNull(); - assertThatCode(() -> noConsumersServer.closeGracefully()).doesNotThrowAnyException(); + assertThatCode(noConsumersServer::closeGracefully).doesNotThrowAnyException(); } } diff --git a/mcp/src/main/java/io/modelcontextprotocol/client/transport/HttpClientSseClientTransport.java b/mcp/src/main/java/io/modelcontextprotocol/client/transport/HttpClientSseClientTransport.java index 632d3844..dcac5f68 100644 --- a/mcp/src/main/java/io/modelcontextprotocol/client/transport/HttpClientSseClientTransport.java +++ b/mcp/src/main/java/io/modelcontextprotocol/client/transport/HttpClientSseClientTransport.java @@ -362,7 +362,7 @@ else if (MESSAGE_EVENT_TYPE.equals(event.type())) { logger.error("Received unrecognized SSE event type: {}", event.type()); } } - catch (IOException e) { + catch (RuntimeException e) { logger.error("Error processing SSE event", e); future.completeExceptionally(e); } diff --git a/mcp/src/main/java/io/modelcontextprotocol/server/McpAsyncServer.java b/mcp/src/main/java/io/modelcontextprotocol/server/McpAsyncServer.java index 28b63cec..e722867c 100644 --- a/mcp/src/main/java/io/modelcontextprotocol/server/McpAsyncServer.java +++ b/mcp/src/main/java/io/modelcontextprotocol/server/McpAsyncServer.java @@ -265,7 +265,7 @@ private static class AsyncServerImpl extends McpAsyncServer { private final ConcurrentHashMap prompts = new ConcurrentHashMap<>(); - // FIXME: this field is deprecated and should be remvoed together with the + // FIXME: this field is deprecated and should be removed together with the // broadcasting loggingNotification. private LoggingLevel minLoggingLevel = LoggingLevel.DEBUG; @@ -334,6 +334,7 @@ private static class AsyncServerImpl extends McpAsyncServer { mcpTransportProvider.setSessionFactory( transport -> new McpServerSession(UUID.randomUUID().toString(), requestTimeout, transport, this::asyncInitializeRequestHandler, Mono::empty, requestHandlers, notificationHandlers)); + } // --------------------------------------- diff --git a/mcp/src/main/java/io/modelcontextprotocol/server/transport/HttpServletSseServerTransportProvider.java b/mcp/src/main/java/io/modelcontextprotocol/server/transport/HttpServletSseServerTransportProvider.java index afdbff47..67d4a440 100644 --- a/mcp/src/main/java/io/modelcontextprotocol/server/transport/HttpServletSseServerTransportProvider.java +++ b/mcp/src/main/java/io/modelcontextprotocol/server/transport/HttpServletSseServerTransportProvider.java @@ -3,9 +3,9 @@ */ package io.modelcontextprotocol.server.transport; -import java.io.BufferedReader; import java.io.IOException; import java.io.PrintWriter; +import java.io.UncheckedIOException; import java.util.Map; import java.util.UUID; import java.util.concurrent.ConcurrentHashMap; @@ -234,24 +234,20 @@ protected void doGet(HttpServletRequest request, HttpServletResponse response) * and formats error responses according to the MCP specification. * @param request The HTTP servlet request * @param response The HTTP servlet response - * @throws ServletException If a servlet-specific error occurs * @throws IOException If an I/O error occurs */ @Override protected void doPost(HttpServletRequest request, HttpServletResponse response) - throws ServletException, IOException { - + throws IOException { if (isClosing.get()) { response.sendError(HttpServletResponse.SC_SERVICE_UNAVAILABLE, "Server is shutting down"); return; } - String requestURI = request.getRequestURI(); if (!requestURI.endsWith(messageEndpoint)) { response.sendError(HttpServletResponse.SC_NOT_FOUND); return; } - // Get the session ID from the request parameter String sessionId = request.getParameter("sessionId"); if (sessionId == null) { @@ -277,24 +273,29 @@ protected void doPost(HttpServletRequest request, HttpServletResponse response) writer.flush(); return; } - try { - BufferedReader reader = request.getReader(); - StringBuilder body = new StringBuilder(); - String line; - while ((line = reader.readLine()) != null) { - body.append(line); - } - - McpSchema.JSONRPCMessage message = McpSchema.deserializeJsonRpcMessage(objectMapper, body.toString()); - - // Process the message through the session's handle method + McpSchema.JSONRPCMessage message = McpSchema.deserializeJsonRpcMessage(objectMapper, request.getReader()); session.handle(message).block(); // Block for Servlet compatibility - response.setStatus(HttpServletResponse.SC_OK); } + catch (IllegalArgumentException | UncheckedIOException ex) { + try { + McpError mcpError = new McpError(ex.getMessage()); + response.setContentType(APPLICATION_JSON); + response.setCharacterEncoding(UTF_8); + response.setStatus(HttpServletResponse.SC_BAD_REQUEST); + String jsonError = objectMapper.writeValueAsString(mcpError); + PrintWriter writer = response.getWriter(); + writer.write(jsonError); + writer.flush(); + } + catch (IOException ex2) { + logger.error(FAILED_TO_SEND_ERROR_RESPONSE, ex2.getMessage()); + response.sendError(HttpServletResponse.SC_INTERNAL_SERVER_ERROR, "Error processing message"); + } + } catch (Exception e) { - logger.error("Error processing message: {}", e.getMessage()); + logger.error("Error processing message", e); try { McpError mcpError = new McpError(e.getMessage()); response.setContentType(APPLICATION_JSON); diff --git a/mcp/src/main/java/io/modelcontextprotocol/spec/McpSchema.java b/mcp/src/main/java/io/modelcontextprotocol/spec/McpSchema.java index 6eb5159f..0c11be00 100644 --- a/mcp/src/main/java/io/modelcontextprotocol/spec/McpSchema.java +++ b/mcp/src/main/java/io/modelcontextprotocol/spec/McpSchema.java @@ -4,7 +4,10 @@ package io.modelcontextprotocol.spec; +import java.io.BufferedReader; import java.io.IOException; +import java.io.Reader; +import java.io.StringReader; import java.util.ArrayList; import java.util.HashMap; import java.util.List; @@ -140,32 +143,36 @@ public sealed interface Request /** * Deserializes a JSON string into a JSONRPCMessage object. * @param objectMapper The ObjectMapper instance to use for deserialization - * @param jsonText The JSON string to deserialize + * @param inputStream The JSON string to deserialize * @return A JSONRPCMessage instance using either the {@link JSONRPCRequest}, * {@link JSONRPCNotification}, or {@link JSONRPCResponse} classes. - * @throws IOException If there's an error during deserialization * @throws IllegalArgumentException If the JSON structure doesn't match any known * message type */ - public static JSONRPCMessage deserializeJsonRpcMessage(ObjectMapper objectMapper, String jsonText) - throws IOException { - - logger.debug("Received JSON message: {}", jsonText); - - var map = objectMapper.readValue(jsonText, MAP_TYPE_REF); - - // Determine message type based on specific JSON structure - if (map.containsKey("method") && map.containsKey("id")) { - return objectMapper.convertValue(map, JSONRPCRequest.class); - } - else if (map.containsKey("method") && !map.containsKey("id")) { - return objectMapper.convertValue(map, JSONRPCNotification.class); + public static JSONRPCMessage deserializeJsonRpcMessage(ObjectMapper objectMapper, BufferedReader inputStream) { + try { + var map = objectMapper.readValue(inputStream, MAP_TYPE_REF); + // Determine message type based on specific JSON structure + if (map.containsKey("method") && map.containsKey("id")) { + return objectMapper.convertValue(map, JSONRPCRequest.class); + } + else if (map.containsKey("method") && !map.containsKey("id")) { + return objectMapper.convertValue(map, JSONRPCNotification.class); + } + else if (map.containsKey("result") || map.containsKey("error")) { + return objectMapper.convertValue(map, JSONRPCResponse.class); + } + throw new IllegalArgumentException("Cannot deserialize JSONRPCMessage: " + map); } - else if (map.containsKey("result") || map.containsKey("error")) { - return objectMapper.convertValue(map, JSONRPCResponse.class); + catch (IOException e) { + throw new java.io.UncheckedIOException(e); } + } - throw new IllegalArgumentException("Cannot deserialize JSONRPCMessage: " + jsonText); + public static JSONRPCMessage deserializeJsonRpcMessage(ObjectMapper objectMapper, String input) { + Reader inputString = new StringReader(input); + BufferedReader reader = new BufferedReader(inputString); + return deserializeJsonRpcMessage(objectMapper, reader); } // --------------------------- diff --git a/mcp/src/main/java/io/modelcontextprotocol/spec/McpServerSession.java b/mcp/src/main/java/io/modelcontextprotocol/spec/McpServerSession.java index 46c356cd..850db8d2 100644 --- a/mcp/src/main/java/io/modelcontextprotocol/spec/McpServerSession.java +++ b/mcp/src/main/java/io/modelcontextprotocol/spec/McpServerSession.java @@ -63,14 +63,15 @@ public class McpServerSession implements McpSession { * @param initHandler called when a * {@link io.modelcontextprotocol.spec.McpSchema.InitializeRequest} is received by the * server - * @param initNotificationHandler called when a - * {@link McpSchema.METHOD_NOTIFICATION_INITIALIZED} is received. + * @param initNotificationHandler called when a {@link McpSchema.METHOD_INITIALIZE } + * is received. * @param requestHandlers map of request handlers to use * @param notificationHandlers map of notification handlers to use */ public McpServerSession(String id, Duration requestTimeout, McpServerTransport transport, InitRequestHandler initHandler, InitNotificationHandler initNotificationHandler, Map> requestHandlers, Map notificationHandlers) { + this.id = id; this.requestTimeout = requestTimeout; this.transport = transport; @@ -201,6 +202,7 @@ private Mono handleIncomingRequest(McpSchema.JSONRPCR return Mono.defer(() -> { Mono resultMono; if (McpSchema.METHOD_INITIALIZE.equals(request.method())) { + // TODO handle situation where already initialized! McpSchema.InitializeRequest initializeRequest = transport.unmarshalFrom(request.params(), new TypeReference() { @@ -258,13 +260,11 @@ record MethodNotFoundError(String method, String message, Object data) { } static MethodNotFoundError getMethodNotFoundError(String method) { - switch (method) { - case McpSchema.METHOD_ROOTS_LIST: - return new MethodNotFoundError(method, "Roots not supported", - Map.of("reason", "Client does not have roots capability")); - default: - return new MethodNotFoundError(method, "Method not found: " + method, null); + if (method.equals(McpSchema.METHOD_ROOTS_LIST)) { + return new MethodNotFoundError(method, "Roots not supported", + Map.of("reason", "Client does not have roots capability")); } + return new MethodNotFoundError(method, "Method not found: " + method, null); } @Override diff --git a/mcp/src/test/java/io/modelcontextprotocol/server/AbstractMcpSyncServerTests.java b/mcp/src/test/java/io/modelcontextprotocol/server/AbstractMcpSyncServerTests.java index 0b38da85..aad67843 100644 --- a/mcp/src/test/java/io/modelcontextprotocol/server/AbstractMcpSyncServerTests.java +++ b/mcp/src/test/java/io/modelcontextprotocol/server/AbstractMcpSyncServerTests.java @@ -39,6 +39,10 @@ public abstract class AbstractMcpSyncServerTests { private static final String TEST_PROMPT_NAME = "test-prompt"; + private static final String SIMPLE_VERSION = "1.0.0"; + + public static final String TEST_SERVER = "test-server"; + abstract protected McpServerTransportProvider createMcpTransportProvider(); protected void onStart() { @@ -49,7 +53,6 @@ protected void onClose() { @BeforeEach void setUp() { - // onStart(); } @AfterEach @@ -74,25 +77,31 @@ void testConstructorWithInvalidArguments() { @Test void testGracefulShutdown() { - var mcpSyncServer = McpServer.sync(createMcpTransportProvider()).serverInfo("test-server", "1.0.0").build(); + var mcpSyncServer = McpServer.sync(createMcpTransportProvider()) + .serverInfo(TEST_SERVER, SIMPLE_VERSION) + .build(); - assertThatCode(() -> mcpSyncServer.closeGracefully()).doesNotThrowAnyException(); + assertThatCode(mcpSyncServer::closeGracefully).doesNotThrowAnyException(); } @Test void testImmediateClose() { - var mcpSyncServer = McpServer.sync(createMcpTransportProvider()).serverInfo("test-server", "1.0.0").build(); + var mcpSyncServer = McpServer.sync(createMcpTransportProvider()) + .serverInfo(TEST_SERVER, SIMPLE_VERSION) + .build(); - assertThatCode(() -> mcpSyncServer.close()).doesNotThrowAnyException(); + assertThatCode(mcpSyncServer::close).doesNotThrowAnyException(); } @Test void testGetAsyncServer() { - var mcpSyncServer = McpServer.sync(createMcpTransportProvider()).serverInfo("test-server", "1.0.0").build(); + var mcpSyncServer = McpServer.sync(createMcpTransportProvider()) + .serverInfo(TEST_SERVER, SIMPLE_VERSION) + .build(); assertThat(mcpSyncServer.getAsyncServer()).isNotNull(); - assertThatCode(() -> mcpSyncServer.closeGracefully()).doesNotThrowAnyException(); + assertThatCode(mcpSyncServer::closeGracefully).doesNotThrowAnyException(); } // --------------------------------------- @@ -107,19 +116,22 @@ void testGetAsyncServer() { } """; - @Test - void testAddTool() { - var mcpSyncServer = McpServer.sync(createMcpTransportProvider()) - .serverInfo("test-server", "1.0.0") + private McpSyncServer syncServer() { + return McpServer.sync(createMcpTransportProvider()) + .serverInfo(TEST_SERVER, "1.0.0") .capabilities(ServerCapabilities.builder().tools(true).build()) .build(); + } + @Test + void testAddTool() { + var mcpSyncServer = syncServer(); Tool newTool = new McpSchema.Tool("new-tool", "New test tool", emptyJsonSchema); assertThatCode(() -> mcpSyncServer.addTool(new McpServerFeatures.SyncToolSpecification(newTool, (exchange, args) -> new CallToolResult(List.of(), false)))) .doesNotThrowAnyException(); - assertThatCode(() -> mcpSyncServer.closeGracefully()).doesNotThrowAnyException(); + assertThatCode(mcpSyncServer::closeGracefully).doesNotThrowAnyException(); } @Test @@ -127,7 +139,7 @@ void testAddDuplicateTool() { Tool duplicateTool = new McpSchema.Tool(TEST_TOOL_NAME, "Duplicate tool", emptyJsonSchema); var mcpSyncServer = McpServer.sync(createMcpTransportProvider()) - .serverInfo("test-server", "1.0.0") + .serverInfo(TEST_SERVER, SIMPLE_VERSION) .capabilities(ServerCapabilities.builder().tools(true).build()) .tool(duplicateTool, (exchange, args) -> new CallToolResult(List.of(), false)) .build(); @@ -137,7 +149,7 @@ void testAddDuplicateTool() { .isInstanceOf(McpError.class) .hasMessage("Tool with name '" + TEST_TOOL_NAME + "' already exists"); - assertThatCode(() -> mcpSyncServer.closeGracefully()).doesNotThrowAnyException(); + assertThatCode(mcpSyncServer::closeGracefully).doesNotThrowAnyException(); } @Test @@ -145,36 +157,33 @@ void testRemoveTool() { Tool tool = new McpSchema.Tool(TEST_TOOL_NAME, "Test tool", emptyJsonSchema); var mcpSyncServer = McpServer.sync(createMcpTransportProvider()) - .serverInfo("test-server", "1.0.0") + .serverInfo(TEST_SERVER, SIMPLE_VERSION) .capabilities(ServerCapabilities.builder().tools(true).build()) .tool(tool, (exchange, args) -> new CallToolResult(List.of(), false)) .build(); assertThatCode(() -> mcpSyncServer.removeTool(TEST_TOOL_NAME)).doesNotThrowAnyException(); - assertThatCode(() -> mcpSyncServer.closeGracefully()).doesNotThrowAnyException(); + assertThatCode(mcpSyncServer::closeGracefully).doesNotThrowAnyException(); } @Test void testRemoveNonexistentTool() { - var mcpSyncServer = McpServer.sync(createMcpTransportProvider()) - .serverInfo("test-server", "1.0.0") - .capabilities(ServerCapabilities.builder().tools(true).build()) - .build(); + var mcpSyncServer = syncServer(); assertThatThrownBy(() -> mcpSyncServer.removeTool("nonexistent-tool")).isInstanceOf(McpError.class) .hasMessage("Tool with name 'nonexistent-tool' not found"); - assertThatCode(() -> mcpSyncServer.closeGracefully()).doesNotThrowAnyException(); + assertThatCode(mcpSyncServer::closeGracefully).doesNotThrowAnyException(); } @Test void testNotifyToolsListChanged() { - var mcpSyncServer = McpServer.sync(createMcpTransportProvider()).serverInfo("test-server", "1.0.0").build(); + var mcpSyncServer = syncServer(); - assertThatCode(() -> mcpSyncServer.notifyToolsListChanged()).doesNotThrowAnyException(); + assertThatCode(mcpSyncServer::notifyToolsListChanged).doesNotThrowAnyException(); - assertThatCode(() -> mcpSyncServer.closeGracefully()).doesNotThrowAnyException(); + assertThatCode(mcpSyncServer::closeGracefully).doesNotThrowAnyException(); } // --------------------------------------- @@ -183,17 +192,17 @@ void testNotifyToolsListChanged() { @Test void testNotifyResourcesListChanged() { - var mcpSyncServer = McpServer.sync(createMcpTransportProvider()).serverInfo("test-server", "1.0.0").build(); + var mcpSyncServer = syncServer(); - assertThatCode(() -> mcpSyncServer.notifyResourcesListChanged()).doesNotThrowAnyException(); + assertThatCode(mcpSyncServer::notifyResourcesListChanged).doesNotThrowAnyException(); - assertThatCode(() -> mcpSyncServer.closeGracefully()).doesNotThrowAnyException(); + assertThatCode(mcpSyncServer::closeGracefully).doesNotThrowAnyException(); } @Test void testAddResource() { var mcpSyncServer = McpServer.sync(createMcpTransportProvider()) - .serverInfo("test-server", "1.0.0") + .serverInfo(TEST_SERVER, SIMPLE_VERSION) .capabilities(ServerCapabilities.builder().resources(true, false).build()) .build(); @@ -204,13 +213,13 @@ void testAddResource() { assertThatCode(() -> mcpSyncServer.addResource(specification)).doesNotThrowAnyException(); - assertThatCode(() -> mcpSyncServer.closeGracefully()).doesNotThrowAnyException(); + assertThatCode(mcpSyncServer::closeGracefully).doesNotThrowAnyException(); } @Test void testAddResourceWithNullSpecification() { var mcpSyncServer = McpServer.sync(createMcpTransportProvider()) - .serverInfo("test-server", "1.0.0") + .serverInfo(TEST_SERVER, "1.0.0") .capabilities(ServerCapabilities.builder().resources(true, false).build()) .build(); @@ -218,13 +227,13 @@ void testAddResourceWithNullSpecification() { .isInstanceOf(McpError.class) .hasMessage("Resource must not be null"); - assertThatCode(() -> mcpSyncServer.closeGracefully()).doesNotThrowAnyException(); + assertThatCode(mcpSyncServer::closeGracefully).doesNotThrowAnyException(); } @Test void testAddResourceWithoutCapability() { var serverWithoutResources = McpServer.sync(createMcpTransportProvider()) - .serverInfo("test-server", "1.0.0") + .serverInfo(TEST_SERVER, SIMPLE_VERSION) .build(); Resource resource = new Resource(TEST_RESOURCE_URI, "Test Resource", "text/plain", "Test resource description", @@ -239,7 +248,7 @@ void testAddResourceWithoutCapability() { @Test void testRemoveResourceWithoutCapability() { var serverWithoutResources = McpServer.sync(createMcpTransportProvider()) - .serverInfo("test-server", "1.0.0") + .serverInfo(TEST_SERVER, SIMPLE_VERSION) .build(); assertThatThrownBy(() -> serverWithoutResources.removeResource(TEST_RESOURCE_URI)).isInstanceOf(McpError.class) @@ -252,17 +261,19 @@ void testRemoveResourceWithoutCapability() { @Test void testNotifyPromptsListChanged() { - var mcpSyncServer = McpServer.sync(createMcpTransportProvider()).serverInfo("test-server", "1.0.0").build(); + var mcpSyncServer = McpServer.sync(createMcpTransportProvider()) + .serverInfo(TEST_SERVER, SIMPLE_VERSION) + .build(); - assertThatCode(() -> mcpSyncServer.notifyPromptsListChanged()).doesNotThrowAnyException(); + assertThatCode(mcpSyncServer::notifyPromptsListChanged).doesNotThrowAnyException(); - assertThatCode(() -> mcpSyncServer.closeGracefully()).doesNotThrowAnyException(); + assertThatCode(mcpSyncServer::closeGracefully).doesNotThrowAnyException(); } @Test void testAddPromptWithNullSpecification() { var mcpSyncServer = McpServer.sync(createMcpTransportProvider()) - .serverInfo("test-server", "1.0.0") + .serverInfo(TEST_SERVER, SIMPLE_VERSION) .capabilities(ServerCapabilities.builder().prompts(false).build()) .build(); @@ -274,7 +285,7 @@ void testAddPromptWithNullSpecification() { @Test void testAddPromptWithoutCapability() { var serverWithoutPrompts = McpServer.sync(createMcpTransportProvider()) - .serverInfo("test-server", "1.0.0") + .serverInfo(TEST_SERVER, SIMPLE_VERSION) .build(); Prompt prompt = new Prompt(TEST_PROMPT_NAME, "Test Prompt", List.of()); @@ -289,7 +300,7 @@ void testAddPromptWithoutCapability() { @Test void testRemovePromptWithoutCapability() { var serverWithoutPrompts = McpServer.sync(createMcpTransportProvider()) - .serverInfo("test-server", "1.0.0") + .serverInfo(TEST_SERVER, SIMPLE_VERSION) .build(); assertThatThrownBy(() -> serverWithoutPrompts.removePrompt(TEST_PROMPT_NAME)).isInstanceOf(McpError.class) @@ -304,27 +315,27 @@ void testRemovePrompt() { .of(new PromptMessage(McpSchema.Role.ASSISTANT, new McpSchema.TextContent("Test content"))))); var mcpSyncServer = McpServer.sync(createMcpTransportProvider()) - .serverInfo("test-server", "1.0.0") + .serverInfo(TEST_SERVER, SIMPLE_VERSION) .capabilities(ServerCapabilities.builder().prompts(true).build()) .prompts(specification) .build(); assertThatCode(() -> mcpSyncServer.removePrompt(TEST_PROMPT_NAME)).doesNotThrowAnyException(); - assertThatCode(() -> mcpSyncServer.closeGracefully()).doesNotThrowAnyException(); + assertThatCode(mcpSyncServer::closeGracefully).doesNotThrowAnyException(); } @Test void testRemoveNonexistentPrompt() { var mcpSyncServer = McpServer.sync(createMcpTransportProvider()) - .serverInfo("test-server", "1.0.0") + .serverInfo(TEST_SERVER, SIMPLE_VERSION) .capabilities(ServerCapabilities.builder().prompts(true).build()) .build(); assertThatThrownBy(() -> mcpSyncServer.removePrompt("nonexistent-prompt")).isInstanceOf(McpError.class) .hasMessage("Prompt with name 'nonexistent-prompt' not found"); - assertThatCode(() -> mcpSyncServer.closeGracefully()).doesNotThrowAnyException(); + assertThatCode(mcpSyncServer::closeGracefully).doesNotThrowAnyException(); } // --------------------------------------- @@ -338,7 +349,7 @@ void testRootsChangeHandlers() { var consumerCalled = new boolean[1]; var singleConsumerServer = McpServer.sync(createMcpTransportProvider()) - .serverInfo("test-server", "1.0.0") + .serverInfo(TEST_SERVER, SIMPLE_VERSION) .rootsChangeHandlers(List.of((exchange, roots) -> { consumerCalled[0] = true; if (!roots.isEmpty()) { @@ -347,8 +358,10 @@ void testRootsChangeHandlers() { })) .build(); + // TODO these are null and neve read by test + // assertThat(rootsReceived[0]).isNotNull(); assertThat(singleConsumerServer).isNotNull(); - assertThatCode(() -> singleConsumerServer.closeGracefully()).doesNotThrowAnyException(); + assertThatCode(singleConsumerServer::closeGracefully).doesNotThrowAnyException(); onClose(); // Test with multiple consumers @@ -357,7 +370,7 @@ void testRootsChangeHandlers() { var rootsContent = new List[1]; var multipleConsumersServer = McpServer.sync(createMcpTransportProvider()) - .serverInfo("test-server", "1.0.0") + .serverInfo(TEST_SERVER, SIMPLE_VERSION) .rootsChangeHandlers(List.of((exchange, roots) -> { consumer1Called[0] = true; rootsContent[0] = roots; @@ -365,26 +378,28 @@ void testRootsChangeHandlers() { .build(); assertThat(multipleConsumersServer).isNotNull(); - assertThatCode(() -> multipleConsumersServer.closeGracefully()).doesNotThrowAnyException(); + assertThatCode(multipleConsumersServer::closeGracefully).doesNotThrowAnyException(); onClose(); // Test error handling var errorHandlingServer = McpServer.sync(createMcpTransportProvider()) - .serverInfo("test-server", "1.0.0") + .serverInfo(TEST_SERVER, SIMPLE_VERSION) .rootsChangeHandlers(List.of((exchange, roots) -> { throw new RuntimeException("Test error"); })) .build(); assertThat(errorHandlingServer).isNotNull(); - assertThatCode(() -> errorHandlingServer.closeGracefully()).doesNotThrowAnyException(); + assertThatCode(errorHandlingServer::closeGracefully).doesNotThrowAnyException(); onClose(); // Test without consumers - var noConsumersServer = McpServer.sync(createMcpTransportProvider()).serverInfo("test-server", "1.0.0").build(); + var noConsumersServer = McpServer.sync(createMcpTransportProvider()) + .serverInfo(TEST_SERVER, SIMPLE_VERSION) + .build(); assertThat(noConsumersServer).isNotNull(); - assertThatCode(() -> noConsumersServer.closeGracefully()).doesNotThrowAnyException(); + assertThatCode(noConsumersServer::closeGracefully).doesNotThrowAnyException(); } }