|
9 | 9 | import java.util.Map; |
10 | 10 | import java.util.concurrent.atomic.AtomicReference; |
11 | 11 | import java.util.function.BiFunction; |
| 12 | +import java.util.function.Function; |
12 | 13 |
|
| 14 | +import ch.qos.logback.classic.Level; |
| 15 | +import ch.qos.logback.classic.Logger; |
| 16 | +import ch.qos.logback.classic.spi.ILoggingEvent; |
| 17 | +import ch.qos.logback.core.read.ListAppender; |
13 | 18 | import io.modelcontextprotocol.client.McpClient; |
14 | 19 | import io.modelcontextprotocol.client.transport.HttpClientStreamableHttpTransport; |
15 | 20 | import io.modelcontextprotocol.common.McpTransportContext; |
|
41 | 46 | import org.junit.jupiter.api.BeforeEach; |
42 | 47 | import org.junit.jupiter.api.Test; |
43 | 48 | import org.junit.jupiter.api.Timeout; |
| 49 | +import org.slf4j.LoggerFactory; |
| 50 | +import reactor.core.publisher.Mono; |
| 51 | +import reactor.test.StepVerifier; |
44 | 52 |
|
45 | 53 | import org.springframework.mock.web.MockHttpServletRequest; |
46 | 54 | import org.springframework.mock.web.MockHttpServletResponse; |
47 | 55 | import org.springframework.web.client.RestClient; |
48 | | - |
49 | 56 | import static io.modelcontextprotocol.server.transport.HttpServletStatelessServerTransport.APPLICATION_JSON; |
50 | 57 | import static io.modelcontextprotocol.server.transport.HttpServletStatelessServerTransport.TEXT_EVENT_STREAM; |
51 | 58 | import static io.modelcontextprotocol.util.McpJsonMapperUtils.JSON_MAPPER; |
@@ -448,7 +455,7 @@ void testStructuredOutputOfObjectArrayValidationSuccess() { |
448 | 455 | "type", "object", |
449 | 456 | "properties", Map.of( |
450 | 457 | "name", Map.of("type", "string"), |
451 | | - "age", Map.of("type", "number")), |
| 458 | + "age", Map.of("type", "number")), |
452 | 459 | "required", List.of("name", "age"))); // @formatter:on |
453 | 460 |
|
454 | 461 | Tool calculatorTool = Tool.builder("getMembers") |
@@ -765,6 +772,105 @@ void testThrownMcpErrorAndJsonRpcError() throws Exception { |
765 | 772 | mcpServer.close(); |
766 | 773 | } |
767 | 774 |
|
| 775 | + @Test |
| 776 | + void testMissingHandlerReturnsMethodNotFoundError() { |
| 777 | + var mcpServer = McpServer.sync(mcpStatelessServerTransport) |
| 778 | + .serverInfo("test-server", "1.0.0") |
| 779 | + .capabilities(ServerCapabilities.builder().build()) |
| 780 | + .build(); |
| 781 | + var clientTransport = HttpClientStreamableHttpTransport.builder("http://localhost:" + PORT) |
| 782 | + .endpoint(CUSTOM_MESSAGE_ENDPOINT) |
| 783 | + .build(); |
| 784 | + |
| 785 | + try (var mcpClient = McpClient.sync(clientTransport).build()) { |
| 786 | + // Create a session using an MCP client |
| 787 | + McpSchema.InitializeResult initResult = mcpClient.initialize(); |
| 788 | + assertThat(initResult).isNotNull(); |
| 789 | + |
| 790 | + // Override the response handler in the client to capture responses |
| 791 | + AtomicReference<McpSchema.JSONRPCResponse> response = new AtomicReference<>(); |
| 792 | + var handler = (Function<Mono<McpSchema.JSONRPCMessage>, Mono<McpSchema.JSONRPCMessage>>) ( |
| 793 | + message) -> message.doOnNext(r -> { |
| 794 | + if (r instanceof McpSchema.JSONRPCResponse resp) { |
| 795 | + response.set(resp); |
| 796 | + } |
| 797 | + }); |
| 798 | + StepVerifier.create(clientTransport.connect(handler)).verifyComplete(); |
| 799 | + |
| 800 | + // Send a request for a non-existent method through the transport, bypassing |
| 801 | + // the client's capability checks |
| 802 | + StepVerifier |
| 803 | + .create(clientTransport.sendMessage(new McpSchema.JSONRPCRequest("foo/bar", "test-request-123"))) |
| 804 | + .verifyComplete(); |
| 805 | + |
| 806 | + // Wait until we've received the response |
| 807 | + await().atMost(Duration.ofSeconds(1)).until(() -> response.get() != null); |
| 808 | + |
| 809 | + assertThat(response.get().error().code()).isEqualTo(McpSchema.ErrorCodes.METHOD_NOT_FOUND); |
| 810 | + assertThat(response.get().error().message()).isEqualTo("Method not found: foo/bar"); |
| 811 | + } |
| 812 | + finally { |
| 813 | + mcpServer.closeGracefully(); |
| 814 | + } |
| 815 | + } |
| 816 | + |
| 817 | + @Test |
| 818 | + void testInitializedNotificationDoesNotLogWarn() { |
| 819 | + Logger handlerLogger = (Logger) LoggerFactory.getLogger(DefaultMcpStatelessServerHandler.class); |
| 820 | + ListAppender<ILoggingEvent> logAppender = new ListAppender<>(); |
| 821 | + logAppender.start(); |
| 822 | + handlerLogger.addAppender(logAppender); |
| 823 | + |
| 824 | + try { |
| 825 | + var mcpServer = McpServer.sync(mcpStatelessServerTransport) |
| 826 | + .serverInfo("test-server", "1.0.0") |
| 827 | + .capabilities(ServerCapabilities.builder().build()) |
| 828 | + .build(); |
| 829 | + |
| 830 | + try (var mcpClient = clientBuilder.build()) { |
| 831 | + mcpClient.initialize(); // automatically sends notifications/initialized |
| 832 | + } |
| 833 | + finally { |
| 834 | + mcpServer.close(); |
| 835 | + } |
| 836 | + } |
| 837 | + finally { |
| 838 | + handlerLogger.detachAppender(logAppender); |
| 839 | + logAppender.stop(); |
| 840 | + } |
| 841 | + |
| 842 | + assertThat(logAppender.list).noneMatch(event -> event.getLevel() == Level.WARN); |
| 843 | + } |
| 844 | + |
| 845 | + @Test |
| 846 | + void testRootsListChangedNotificationDoesNotLogWarn() { |
| 847 | + Logger handlerLogger = (Logger) LoggerFactory.getLogger(DefaultMcpStatelessServerHandler.class); |
| 848 | + ListAppender<ILoggingEvent> logAppender = new ListAppender<>(); |
| 849 | + logAppender.start(); |
| 850 | + handlerLogger.addAppender(logAppender); |
| 851 | + |
| 852 | + try { |
| 853 | + var mcpServer = McpServer.sync(mcpStatelessServerTransport) |
| 854 | + .serverInfo("test-server", "1.0.0") |
| 855 | + .capabilities(ServerCapabilities.builder().build()) |
| 856 | + .build(); |
| 857 | + |
| 858 | + try (var mcpClient = clientBuilder.build()) { |
| 859 | + mcpClient.initialize(); |
| 860 | + mcpClient.rootsListChangedNotification(); |
| 861 | + } |
| 862 | + finally { |
| 863 | + mcpServer.close(); |
| 864 | + } |
| 865 | + } |
| 866 | + finally { |
| 867 | + handlerLogger.detachAppender(logAppender); |
| 868 | + logAppender.stop(); |
| 869 | + } |
| 870 | + |
| 871 | + assertThat(logAppender.list).noneMatch(event -> event.getLevel() == Level.WARN); |
| 872 | + } |
| 873 | + |
768 | 874 | private double evaluateExpression(String expression) { |
769 | 875 | // Simple expression evaluator for testing |
770 | 876 | return switch (expression) { |
|
0 commit comments