diff --git a/mcp-spring/mcp-spring-webflux/src/test/java/io/modelcontextprotocol/WebFluxSseIntegrationTests.java b/mcp-spring/mcp-spring-webflux/src/test/java/io/modelcontextprotocol/WebFluxSseIntegrationTests.java index 73aff4629..29a1f76a0 100644 --- a/mcp-spring/mcp-spring-webflux/src/test/java/io/modelcontextprotocol/WebFluxSseIntegrationTests.java +++ b/mcp-spring/mcp-spring-webflux/src/test/java/io/modelcontextprotocol/WebFluxSseIntegrationTests.java @@ -3,6 +3,12 @@ */ package io.modelcontextprotocol; +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatExceptionOfType; +import static org.assertj.core.api.Assertions.assertWith; +import static org.awaitility.Awaitility.await; +import static org.mockito.Mockito.mock; + import java.time.Duration; import java.util.List; import java.util.Map; @@ -15,39 +21,50 @@ import java.util.function.Function; import java.util.stream.Collectors; +import org.junit.jupiter.api.AfterEach; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.ValueSource; +import org.springframework.http.server.reactive.HttpHandler; +import org.springframework.http.server.reactive.ReactorHttpHandlerAdapter; +import org.springframework.web.client.RestClient; +import org.springframework.web.reactive.function.client.WebClient; +import org.springframework.web.reactive.function.server.RouterFunctions; + import com.fasterxml.jackson.databind.ObjectMapper; + import io.modelcontextprotocol.client.McpClient; import io.modelcontextprotocol.client.transport.HttpClientSseClientTransport; import io.modelcontextprotocol.client.transport.WebFluxSseClientTransport; import io.modelcontextprotocol.server.McpServer; import io.modelcontextprotocol.server.McpServerFeatures; -import io.modelcontextprotocol.server.TestUtil; import io.modelcontextprotocol.server.McpSyncServerExchange; +import io.modelcontextprotocol.server.TestUtil; import io.modelcontextprotocol.server.transport.WebFluxSseServerTransportProvider; import io.modelcontextprotocol.spec.McpError; import io.modelcontextprotocol.spec.McpSchema; -import io.modelcontextprotocol.spec.McpSchema.*; -import org.junit.jupiter.api.AfterEach; -import org.junit.jupiter.api.BeforeEach; -import org.junit.jupiter.params.ParameterizedTest; -import org.junit.jupiter.params.provider.ValueSource; +import io.modelcontextprotocol.spec.McpSchema.CallToolResult; +import io.modelcontextprotocol.spec.McpSchema.ClientCapabilities; +import io.modelcontextprotocol.spec.McpSchema.CompleteRequest; +import io.modelcontextprotocol.spec.McpSchema.CompleteResult; +import io.modelcontextprotocol.spec.McpSchema.CreateMessageRequest; +import io.modelcontextprotocol.spec.McpSchema.CreateMessageResult; +import io.modelcontextprotocol.spec.McpSchema.ElicitRequest; +import io.modelcontextprotocol.spec.McpSchema.ElicitResult; +import io.modelcontextprotocol.spec.McpSchema.InitializeResult; +import io.modelcontextprotocol.spec.McpSchema.ModelPreferences; +import io.modelcontextprotocol.spec.McpSchema.Prompt; +import io.modelcontextprotocol.spec.McpSchema.PromptArgument; +import io.modelcontextprotocol.spec.McpSchema.PromptReference; +import io.modelcontextprotocol.spec.McpSchema.Role; +import io.modelcontextprotocol.spec.McpSchema.Root; +import io.modelcontextprotocol.spec.McpSchema.ServerCapabilities; +import io.modelcontextprotocol.spec.McpSchema.Tool; import reactor.core.publisher.Mono; import reactor.netty.DisposableServer; import reactor.netty.http.server.HttpServer; - -import org.springframework.http.server.reactive.HttpHandler; -import org.springframework.http.server.reactive.ReactorHttpHandlerAdapter; -import org.springframework.web.client.RestClient; -import org.springframework.web.reactive.function.client.WebClient; -import org.springframework.web.reactive.function.server.RouterFunctions; import reactor.test.StepVerifier; -import static org.assertj.core.api.Assertions.assertThat; -import static org.assertj.core.api.Assertions.assertThatExceptionOfType; -import static org.assertj.core.api.Assertions.assertWith; -import static org.awaitility.Awaitility.await; -import static org.mockito.Mockito.mock; - class WebFluxSseIntegrationTests { private static final int PORT = TestUtil.findAvailablePort(); @@ -103,10 +120,11 @@ void testCreateMessageWithoutSamplingCapabilities(String clientType) { var clientBuilder = clientBuilders.get(clientType); - McpServerFeatures.AsyncToolSpecification tool = new McpServerFeatures.AsyncToolSpecification( - new McpSchema.Tool("tool1", "tool1 description", emptyJsonSchema), - (exchange, request) -> exchange.createMessage(mock(CreateMessageRequest.class)) - .thenReturn(mock(CallToolResult.class))); + McpServerFeatures.AsyncToolSpecification tool = McpServerFeatures.AsyncToolSpecification.builder() + .tool(new McpSchema.Tool("tool1", "tool1 description", emptyJsonSchema)) + .callTool((exchange, request) -> exchange.createMessage(mock(CreateMessageRequest.class)) + .thenReturn(mock(CallToolResult.class))) + .build(); var server = McpServer.async(mcpServerTransportProvider).serverInfo("test-server", "1.0.0").tools(tool).build(); @@ -145,24 +163,26 @@ void testCreateMessageSuccess(String clientType) { AtomicReference samplingResult = new AtomicReference<>(); - McpServerFeatures.AsyncToolSpecification tool = new McpServerFeatures.AsyncToolSpecification( - new McpSchema.Tool("tool1", "tool1 description", emptyJsonSchema), (exchange, request) -> { - - var createMessageRequest = McpSchema.CreateMessageRequest.builder() - .messages(List.of(new McpSchema.SamplingMessage(McpSchema.Role.USER, - new McpSchema.TextContent("Test message")))) - .modelPreferences(ModelPreferences.builder() - .hints(List.of()) - .costPriority(1.0) - .speedPriority(1.0) - .intelligencePriority(1.0) - .build()) - .build(); - - return exchange.createMessage(createMessageRequest) - .doOnNext(samplingResult::set) - .thenReturn(callResponse); - }); + McpServerFeatures.AsyncToolSpecification tool = McpServerFeatures.AsyncToolSpecification.builder() + .tool(new McpSchema.Tool("tool1", "tool1 description", emptyJsonSchema)) + .callTool((exchange, request) -> { + + var createMessageRequest = McpSchema.CreateMessageRequest.builder() + .messages(List.of(new McpSchema.SamplingMessage(McpSchema.Role.USER, + new McpSchema.TextContent("Test message")))) + .modelPreferences(ModelPreferences.builder() + .hints(List.of()) + .costPriority(1.0) + .speedPriority(1.0) + .intelligencePriority(1.0) + .build()) + .build(); + + return exchange.createMessage(createMessageRequest) + .doOnNext(samplingResult::set) + .thenReturn(callResponse); + }) + .build(); var mcpServer = McpServer.async(mcpServerTransportProvider) .serverInfo("test-server", "1.0.0") @@ -221,24 +241,26 @@ void testCreateMessageWithRequestTimeoutSuccess(String clientType) throws Interr AtomicReference samplingResult = new AtomicReference<>(); - McpServerFeatures.AsyncToolSpecification tool = new McpServerFeatures.AsyncToolSpecification( - new McpSchema.Tool("tool1", "tool1 description", emptyJsonSchema), (exchange, request) -> { - - var craeteMessageRequest = McpSchema.CreateMessageRequest.builder() - .messages(List.of(new McpSchema.SamplingMessage(McpSchema.Role.USER, - new McpSchema.TextContent("Test message")))) - .modelPreferences(ModelPreferences.builder() - .hints(List.of()) - .costPriority(1.0) - .speedPriority(1.0) - .intelligencePriority(1.0) - .build()) - .build(); - - return exchange.createMessage(craeteMessageRequest) - .doOnNext(samplingResult::set) - .thenReturn(callResponse); - }); + McpServerFeatures.AsyncToolSpecification tool = McpServerFeatures.AsyncToolSpecification.builder() + .tool(new McpSchema.Tool("tool1", "tool1 description", emptyJsonSchema)) + .callTool((exchange, request) -> { + + var craeteMessageRequest = McpSchema.CreateMessageRequest.builder() + .messages(List.of(new McpSchema.SamplingMessage(McpSchema.Role.USER, + new McpSchema.TextContent("Test message")))) + .modelPreferences(ModelPreferences.builder() + .hints(List.of()) + .costPriority(1.0) + .speedPriority(1.0) + .intelligencePriority(1.0) + .build()) + .build(); + + return exchange.createMessage(craeteMessageRequest) + .doOnNext(samplingResult::set) + .thenReturn(callResponse); + }) + .build(); var mcpServer = McpServer.async(mcpServerTransportProvider) .requestTimeout(Duration.ofSeconds(4)) @@ -297,16 +319,18 @@ void testCreateMessageWithRequestTimeoutFail(String clientType) throws Interrupt CallToolResult callResponse = new McpSchema.CallToolResult(List.of(new McpSchema.TextContent("CALL RESPONSE")), null); - McpServerFeatures.AsyncToolSpecification tool = new McpServerFeatures.AsyncToolSpecification( - new McpSchema.Tool("tool1", "tool1 description", emptyJsonSchema), (exchange, request) -> { + McpServerFeatures.AsyncToolSpecification tool = McpServerFeatures.AsyncToolSpecification.builder() + .tool(new McpSchema.Tool("tool1", "tool1 description", emptyJsonSchema)) + .callTool((exchange, request) -> { - var craeteMessageRequest = McpSchema.CreateMessageRequest.builder() - .messages(List.of(new McpSchema.SamplingMessage(McpSchema.Role.USER, - new McpSchema.TextContent("Test message")))) - .build(); + var craeteMessageRequest = McpSchema.CreateMessageRequest.builder() + .messages(List.of(new McpSchema.SamplingMessage(McpSchema.Role.USER, + new McpSchema.TextContent("Test message")))) + .build(); - return exchange.createMessage(craeteMessageRequest).thenReturn(callResponse); - }); + return exchange.createMessage(craeteMessageRequest).thenReturn(callResponse); + }) + .build(); var mcpServer = McpServer.async(mcpServerTransportProvider) .requestTimeout(Duration.ofSeconds(1)) @@ -340,13 +364,15 @@ void testCreateElicitationWithoutElicitationCapabilities(String clientType) { var clientBuilder = clientBuilders.get(clientType); - McpServerFeatures.AsyncToolSpecification tool = new McpServerFeatures.AsyncToolSpecification( - new McpSchema.Tool("tool1", "tool1 description", emptyJsonSchema), (exchange, request) -> { + McpServerFeatures.AsyncToolSpecification tool = McpServerFeatures.AsyncToolSpecification.builder() + .tool(new McpSchema.Tool("tool1", "tool1 description", emptyJsonSchema)) + .callTool((exchange, request) -> { - exchange.createElicitation(mock(ElicitRequest.class)).block(); + exchange.createElicitation(mock(ElicitRequest.class)).block(); - return Mono.just(mock(CallToolResult.class)); - }); + return Mono.just(mock(CallToolResult.class)); + }) + .build(); var server = McpServer.async(mcpServerTransportProvider).serverInfo("test-server", "1.0.0").tools(tool).build(); @@ -383,23 +409,25 @@ void testCreateElicitationSuccess(String clientType) { CallToolResult callResponse = new McpSchema.CallToolResult(List.of(new McpSchema.TextContent("CALL RESPONSE")), null); - McpServerFeatures.AsyncToolSpecification tool = new McpServerFeatures.AsyncToolSpecification( - new McpSchema.Tool("tool1", "tool1 description", emptyJsonSchema), (exchange, request) -> { + McpServerFeatures.AsyncToolSpecification tool = McpServerFeatures.AsyncToolSpecification.builder() + .tool(new McpSchema.Tool("tool1", "tool1 description", emptyJsonSchema)) + .callTool((exchange, request) -> { - var elicitationRequest = ElicitRequest.builder() - .message("Test message") - .requestedSchema( - Map.of("type", "object", "properties", Map.of("message", Map.of("type", "string")))) - .build(); + var elicitationRequest = ElicitRequest.builder() + .message("Test message") + .requestedSchema( + Map.of("type", "object", "properties", Map.of("message", Map.of("type", "string")))) + .build(); - StepVerifier.create(exchange.createElicitation(elicitationRequest)).consumeNextWith(result -> { - assertThat(result).isNotNull(); - assertThat(result.action()).isEqualTo(ElicitResult.Action.ACCEPT); - assertThat(result.content().get("message")).isEqualTo("Test message"); - }).verifyComplete(); + StepVerifier.create(exchange.createElicitation(elicitationRequest)).consumeNextWith(result -> { + assertThat(result).isNotNull(); + assertThat(result.action()).isEqualTo(ElicitResult.Action.ACCEPT); + assertThat(result.content().get("message")).isEqualTo("Test message"); + }).verifyComplete(); - return Mono.just(callResponse); - }); + return Mono.just(callResponse); + }) + .build(); var mcpServer = McpServer.async(mcpServerTransportProvider) .serverInfo("test-server", "1.0.0") @@ -445,23 +473,25 @@ void testCreateElicitationWithRequestTimeoutSuccess(String clientType) { CallToolResult callResponse = new McpSchema.CallToolResult(List.of(new McpSchema.TextContent("CALL RESPONSE")), null); - McpServerFeatures.AsyncToolSpecification tool = new McpServerFeatures.AsyncToolSpecification( - new McpSchema.Tool("tool1", "tool1 description", emptyJsonSchema), (exchange, request) -> { + McpServerFeatures.AsyncToolSpecification tool = McpServerFeatures.AsyncToolSpecification.builder() + .tool(new McpSchema.Tool("tool1", "tool1 description", emptyJsonSchema)) + .callTool((exchange, request) -> { - var elicitationRequest = ElicitRequest.builder() - .message("Test message") - .requestedSchema( - Map.of("type", "object", "properties", Map.of("message", Map.of("type", "string")))) - .build(); + var elicitationRequest = ElicitRequest.builder() + .message("Test message") + .requestedSchema( + Map.of("type", "object", "properties", Map.of("message", Map.of("type", "string")))) + .build(); - StepVerifier.create(exchange.createElicitation(elicitationRequest)).consumeNextWith(result -> { - assertThat(result).isNotNull(); - assertThat(result.action()).isEqualTo(ElicitResult.Action.ACCEPT); - assertThat(result.content().get("message")).isEqualTo("Test message"); - }).verifyComplete(); + StepVerifier.create(exchange.createElicitation(elicitationRequest)).consumeNextWith(result -> { + assertThat(result).isNotNull(); + assertThat(result.action()).isEqualTo(ElicitResult.Action.ACCEPT); + assertThat(result.content().get("message")).isEqualTo("Test message"); + }).verifyComplete(); - return Mono.just(callResponse); - }); + return Mono.just(callResponse); + }) + .build(); var mcpServer = McpServer.async(mcpServerTransportProvider) .serverInfo("test-server", "1.0.0") @@ -514,23 +544,25 @@ void testCreateElicitationWithRequestTimeoutFail(String clientType) { CallToolResult callResponse = new McpSchema.CallToolResult(List.of(new McpSchema.TextContent("CALL RESPONSE")), null); - McpServerFeatures.AsyncToolSpecification tool = new McpServerFeatures.AsyncToolSpecification( - new McpSchema.Tool("tool1", "tool1 description", emptyJsonSchema), (exchange, request) -> { + McpServerFeatures.AsyncToolSpecification tool = McpServerFeatures.AsyncToolSpecification.builder() + .tool(new McpSchema.Tool("tool1", "tool1 description", emptyJsonSchema)) + .callTool((exchange, request) -> { - var elicitationRequest = ElicitRequest.builder() - .message("Test message") - .requestedSchema( - Map.of("type", "object", "properties", Map.of("message", Map.of("type", "string")))) - .build(); + var elicitationRequest = ElicitRequest.builder() + .message("Test message") + .requestedSchema( + Map.of("type", "object", "properties", Map.of("message", Map.of("type", "string")))) + .build(); - StepVerifier.create(exchange.createElicitation(elicitationRequest)).consumeNextWith(result -> { - assertThat(result).isNotNull(); - assertThat(result.action()).isEqualTo(ElicitResult.Action.ACCEPT); - assertThat(result.content().get("message")).isEqualTo("Test message"); - }).verifyComplete(); + StepVerifier.create(exchange.createElicitation(elicitationRequest)).consumeNextWith(result -> { + assertThat(result).isNotNull(); + assertThat(result.action()).isEqualTo(ElicitResult.Action.ACCEPT); + assertThat(result.content().get("message")).isEqualTo("Test message"); + }).verifyComplete(); - return Mono.just(callResponse); - }); + return Mono.just(callResponse); + }) + .build(); var mcpServer = McpServer.async(mcpServerTransportProvider) .serverInfo("test-server", "1.0.0") @@ -605,13 +637,15 @@ void testRootsWithoutCapability(String clientType) { var clientBuilder = clientBuilders.get(clientType); - McpServerFeatures.SyncToolSpecification tool = new McpServerFeatures.SyncToolSpecification( - new McpSchema.Tool("tool1", "tool1 description", emptyJsonSchema), (exchange, request) -> { + McpServerFeatures.SyncToolSpecification tool = McpServerFeatures.SyncToolSpecification.builder() + .tool(new McpSchema.Tool("tool1", "tool1 description", emptyJsonSchema)) + .callTool((exchange, request) -> { - exchange.listRoots(); // try to list roots + exchange.listRoots(); // try to list roots - return mock(CallToolResult.class); - }); + return mock(CallToolResult.class); + }) + .build(); var mcpServer = McpServer.sync(mcpServerTransportProvider).rootsChangeHandler((exchange, rootsUpdate) -> { }).tools(tool).build(); @@ -744,17 +778,19 @@ void testToolCallSuccess(String clientType) { var clientBuilder = clientBuilders.get(clientType); var callResponse = new McpSchema.CallToolResult(List.of(new McpSchema.TextContent("CALL RESPONSE")), null); - McpServerFeatures.SyncToolSpecification tool1 = new McpServerFeatures.SyncToolSpecification( - new McpSchema.Tool("tool1", "tool1 description", emptyJsonSchema), (exchange, request) -> { - // perform a blocking call to a remote service - String response = RestClient.create() - .get() - .uri("https://raw.githubusercontent.com/modelcontextprotocol/java-sdk/refs/heads/main/README.md") - .retrieve() - .body(String.class); - assertThat(response).isNotBlank(); - return callResponse; - }); + McpServerFeatures.SyncToolSpecification tool1 = McpServerFeatures.SyncToolSpecification.builder() + .tool(new McpSchema.Tool("tool1", "tool1 description", emptyJsonSchema)) + .callTool((exchange, request) -> { + // perform a blocking call to a remote service + String response = RestClient.create() + .get() + .uri("https://raw.githubusercontent.com/modelcontextprotocol/java-sdk/refs/heads/main/README.md") + .retrieve() + .body(String.class); + assertThat(response).isNotBlank(); + return callResponse; + }) + .build(); var mcpServer = McpServer.sync(mcpServerTransportProvider) .capabilities(ServerCapabilities.builder().tools(true).build()) @@ -784,17 +820,19 @@ void testToolListChangeHandlingSuccess(String clientType) { var clientBuilder = clientBuilders.get(clientType); var callResponse = new McpSchema.CallToolResult(List.of(new McpSchema.TextContent("CALL RESPONSE")), null); - McpServerFeatures.SyncToolSpecification tool1 = new McpServerFeatures.SyncToolSpecification( - new McpSchema.Tool("tool1", "tool1 description", emptyJsonSchema), (exchange, request) -> { - // perform a blocking call to a remote service - String response = RestClient.create() - .get() - .uri("https://raw.githubusercontent.com/modelcontextprotocol/java-sdk/refs/heads/main/README.md") - .retrieve() - .body(String.class); - assertThat(response).isNotBlank(); - return callResponse; - }); + McpServerFeatures.SyncToolSpecification tool1 = McpServerFeatures.SyncToolSpecification.builder() + .tool(new McpSchema.Tool("tool1", "tool1 description", emptyJsonSchema)) + .callTool((exchange, request) -> { + // perform a blocking call to a remote service + String response = RestClient.create() + .get() + .uri("https://raw.githubusercontent.com/modelcontextprotocol/java-sdk/refs/heads/main/README.md") + .retrieve() + .body(String.class); + assertThat(response).isNotBlank(); + return callResponse; + }) + .build(); AtomicReference> rootsRef = new AtomicReference<>(); @@ -835,9 +873,10 @@ void testToolListChangeHandlingSuccess(String clientType) { }); // Add a new tool - McpServerFeatures.SyncToolSpecification tool2 = new McpServerFeatures.SyncToolSpecification( - new McpSchema.Tool("tool2", "tool2 description", emptyJsonSchema), - (exchange, request) -> callResponse); + McpServerFeatures.SyncToolSpecification tool2 = McpServerFeatures.SyncToolSpecification.builder() + .tool(new McpSchema.Tool("tool2", "tool2 description", emptyJsonSchema)) + .callTool((exchange, request) -> callResponse) + .build(); mcpServer.addTool(tool2); @@ -880,13 +919,13 @@ void testLoggingNotification(String clientType) throws InterruptedException { var clientBuilder = clientBuilders.get(clientType); // Create server with a tool that sends logging notifications - McpServerFeatures.AsyncToolSpecification tool = new McpServerFeatures.AsyncToolSpecification( - new McpSchema.Tool("logging-test", "Test logging notifications", emptyJsonSchema), - (exchange, request) -> { + McpServerFeatures.AsyncToolSpecification tool = McpServerFeatures.AsyncToolSpecification.builder() + .tool(new McpSchema.Tool("logging-test", "Test logging notifications", emptyJsonSchema)) + .callTool((exchange, request) -> { - // Create and send notifications with different levels + // Create and send notifications with different levels - //@formatter:off + //@formatter:off return exchange // This should be filtered out (DEBUG < NOTICE) .loggingNotification(McpSchema.LoggingMessageNotification.builder() .level(McpSchema.LoggingLevel.DEBUG) @@ -919,7 +958,8 @@ void testLoggingNotification(String clientType) throws InterruptedException { .build())) .thenReturn(new CallToolResult("Logging test completed", false)); //@formatter:on - }); + }) + .build(); var mcpServer = McpServer.async(mcpServerTransportProvider) .serverInfo("test-server", "1.0.0") @@ -1034,26 +1074,27 @@ void testPingSuccess(String clientType) { // Create server with a tool that uses ping functionality AtomicReference executionOrder = new AtomicReference<>(""); - McpServerFeatures.AsyncToolSpecification tool = new McpServerFeatures.AsyncToolSpecification( - new McpSchema.Tool("ping-async-test", "Test ping async behavior", emptyJsonSchema), - (exchange, request) -> { + McpServerFeatures.AsyncToolSpecification tool = McpServerFeatures.AsyncToolSpecification.builder() + .tool(new McpSchema.Tool("ping-async-test", "Test ping async behavior", emptyJsonSchema)) + .callTool((exchange, request) -> { - executionOrder.set(executionOrder.get() + "1"); + executionOrder.set(executionOrder.get() + "1"); - // Test async ping behavior - return exchange.ping().doOnNext(result -> { + // Test async ping behavior + return exchange.ping().doOnNext(result -> { - assertThat(result).isNotNull(); - // Ping should return an empty object or map - assertThat(result).isInstanceOf(Map.class); + assertThat(result).isNotNull(); + // Ping should return an empty object or map + assertThat(result).isInstanceOf(Map.class); - executionOrder.set(executionOrder.get() + "2"); - assertThat(result).isNotNull(); - }).then(Mono.fromCallable(() -> { - executionOrder.set(executionOrder.get() + "3"); - return new CallToolResult("Async ping test completed", false); - })); - }); + executionOrder.set(executionOrder.get() + "2"); + assertThat(result).isNotNull(); + }).then(Mono.fromCallable(() -> { + executionOrder.set(executionOrder.get() + "3"); + return new CallToolResult("Async ping test completed", false); + })); + }) + .build(); var mcpServer = McpServer.async(mcpServerTransportProvider) .serverInfo("test-server", "1.0.0") diff --git a/mcp-spring/mcp-spring-webmvc/src/test/java/io/modelcontextprotocol/server/WebMvcSseIntegrationTests.java b/mcp-spring/mcp-spring-webmvc/src/test/java/io/modelcontextprotocol/server/WebMvcSseIntegrationTests.java index 43d6f40fe..b92ff7a5b 100644 --- a/mcp-spring/mcp-spring-webmvc/src/test/java/io/modelcontextprotocol/server/WebMvcSseIntegrationTests.java +++ b/mcp-spring/mcp-spring-webmvc/src/test/java/io/modelcontextprotocol/server/WebMvcSseIntegrationTests.java @@ -3,6 +3,11 @@ */ package io.modelcontextprotocol.server; +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatExceptionOfType; +import static org.awaitility.Awaitility.await; +import static org.mockito.Mockito.mock; + import java.time.Duration; import java.util.List; import java.util.Map; @@ -10,7 +15,20 @@ import java.util.concurrent.atomic.AtomicReference; import java.util.function.Function; +import org.apache.catalina.LifecycleException; +import org.apache.catalina.LifecycleState; +import org.junit.jupiter.api.AfterEach; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.web.client.RestClient; +import org.springframework.web.servlet.config.annotation.EnableWebMvc; +import org.springframework.web.servlet.function.RouterFunction; +import org.springframework.web.servlet.function.ServerResponse; + import com.fasterxml.jackson.databind.ObjectMapper; + import io.modelcontextprotocol.client.McpClient; import io.modelcontextprotocol.client.transport.HttpClientSseClientTransport; import io.modelcontextprotocol.server.transport.WebMvcSseServerTransportProvider; @@ -26,26 +44,9 @@ import io.modelcontextprotocol.spec.McpSchema.Root; import io.modelcontextprotocol.spec.McpSchema.ServerCapabilities; import io.modelcontextprotocol.spec.McpSchema.Tool; -import org.apache.catalina.LifecycleException; -import org.apache.catalina.LifecycleState; -import org.junit.jupiter.api.AfterEach; -import org.junit.jupiter.api.BeforeEach; -import org.junit.jupiter.api.Test; import reactor.core.publisher.Mono; import reactor.test.StepVerifier; -import org.springframework.context.annotation.Bean; -import org.springframework.context.annotation.Configuration; -import org.springframework.web.client.RestClient; -import org.springframework.web.servlet.config.annotation.EnableWebMvc; -import org.springframework.web.servlet.function.RouterFunction; -import org.springframework.web.servlet.function.ServerResponse; - -import static org.assertj.core.api.Assertions.assertThat; -import static org.assertj.core.api.Assertions.assertThatExceptionOfType; -import static org.awaitility.Awaitility.await; -import static org.mockito.Mockito.mock; - class WebMvcSseIntegrationTests { private static final int PORT = TestUtil.findAvailablePort(); @@ -119,13 +120,13 @@ public void after() { @Test void testCreateMessageWithoutSamplingCapabilities() { - McpServerFeatures.AsyncToolSpecification tool = new McpServerFeatures.AsyncToolSpecification( - new McpSchema.Tool("tool1", "tool1 description", emptyJsonSchema), (exchange, request) -> { - - exchange.createMessage(mock(McpSchema.CreateMessageRequest.class)).block(); - - return Mono.just(mock(CallToolResult.class)); - }); + McpServerFeatures.AsyncToolSpecification tool = McpServerFeatures.AsyncToolSpecification.builder() + .tool(new McpSchema.Tool("tool1", "tool1 description", emptyJsonSchema)) + .callTool((exchange, request) -> { + exchange.createMessage(mock(McpSchema.CreateMessageRequest.class)).block(); + return Mono.just(mock(CallToolResult.class)); + }) + .build(); //@formatter:off var server = McpServer.async(mcpServerTransportProvider) @@ -166,31 +167,33 @@ void testCreateMessageSuccess() { CallToolResult callResponse = new McpSchema.CallToolResult(List.of(new McpSchema.TextContent("CALL RESPONSE")), null); - McpServerFeatures.AsyncToolSpecification tool = new McpServerFeatures.AsyncToolSpecification( - new McpSchema.Tool("tool1", "tool1 description", emptyJsonSchema), (exchange, request) -> { - - var createMessageRequest = McpSchema.CreateMessageRequest.builder() - .messages(List.of(new McpSchema.SamplingMessage(McpSchema.Role.USER, - new McpSchema.TextContent("Test message")))) - .modelPreferences(ModelPreferences.builder() - .hints(List.of()) - .costPriority(1.0) - .speedPriority(1.0) - .intelligencePriority(1.0) - .build()) - .build(); - - StepVerifier.create(exchange.createMessage(createMessageRequest)).consumeNextWith(result -> { - assertThat(result).isNotNull(); - assertThat(result.role()).isEqualTo(Role.USER); - assertThat(result.content()).isInstanceOf(McpSchema.TextContent.class); - assertThat(((McpSchema.TextContent) result.content()).text()).isEqualTo("Test message"); - assertThat(result.model()).isEqualTo("MockModelName"); - assertThat(result.stopReason()).isEqualTo(CreateMessageResult.StopReason.STOP_SEQUENCE); - }).verifyComplete(); - - return Mono.just(callResponse); - }); + McpServerFeatures.AsyncToolSpecification tool = McpServerFeatures.AsyncToolSpecification.builder() + .tool(new McpSchema.Tool("tool1", "tool1 description", emptyJsonSchema)) + .callTool((exchange, request) -> { + + var createMessageRequest = McpSchema.CreateMessageRequest.builder() + .messages(List.of(new McpSchema.SamplingMessage(McpSchema.Role.USER, + new McpSchema.TextContent("Test message")))) + .modelPreferences(ModelPreferences.builder() + .hints(List.of()) + .costPriority(1.0) + .speedPriority(1.0) + .intelligencePriority(1.0) + .build()) + .build(); + + StepVerifier.create(exchange.createMessage(createMessageRequest)).consumeNextWith(result -> { + assertThat(result).isNotNull(); + assertThat(result.role()).isEqualTo(Role.USER); + assertThat(result.content()).isInstanceOf(McpSchema.TextContent.class); + assertThat(((McpSchema.TextContent) result.content()).text()).isEqualTo("Test message"); + assertThat(result.model()).isEqualTo("MockModelName"); + assertThat(result.stopReason()).isEqualTo(CreateMessageResult.StopReason.STOP_SEQUENCE); + }).verifyComplete(); + + return Mono.just(callResponse); + }) + .build(); //@formatter:off var mcpServer = McpServer.async(mcpServerTransportProvider) @@ -242,31 +245,33 @@ void testCreateMessageWithRequestTimeoutSuccess() throws InterruptedException { CallToolResult callResponse = new McpSchema.CallToolResult(List.of(new McpSchema.TextContent("CALL RESPONSE")), null); - McpServerFeatures.AsyncToolSpecification tool = new McpServerFeatures.AsyncToolSpecification( - new McpSchema.Tool("tool1", "tool1 description", emptyJsonSchema), (exchange, request) -> { - - var craeteMessageRequest = McpSchema.CreateMessageRequest.builder() - .messages(List.of(new McpSchema.SamplingMessage(McpSchema.Role.USER, - new McpSchema.TextContent("Test message")))) - .modelPreferences(ModelPreferences.builder() - .hints(List.of()) - .costPriority(1.0) - .speedPriority(1.0) - .intelligencePriority(1.0) - .build()) - .build(); - - StepVerifier.create(exchange.createMessage(craeteMessageRequest)).consumeNextWith(result -> { - assertThat(result).isNotNull(); - assertThat(result.role()).isEqualTo(Role.USER); - assertThat(result.content()).isInstanceOf(McpSchema.TextContent.class); - assertThat(((McpSchema.TextContent) result.content()).text()).isEqualTo("Test message"); - assertThat(result.model()).isEqualTo("MockModelName"); - assertThat(result.stopReason()).isEqualTo(CreateMessageResult.StopReason.STOP_SEQUENCE); - }).verifyComplete(); - - return Mono.just(callResponse); - }); + McpServerFeatures.AsyncToolSpecification tool = McpServerFeatures.AsyncToolSpecification.builder() + .tool(new McpSchema.Tool("tool1", "tool1 description", emptyJsonSchema)) + .callTool((exchange, request) -> { + + var craeteMessageRequest = McpSchema.CreateMessageRequest.builder() + .messages(List.of(new McpSchema.SamplingMessage(McpSchema.Role.USER, + new McpSchema.TextContent("Test message")))) + .modelPreferences(ModelPreferences.builder() + .hints(List.of()) + .costPriority(1.0) + .speedPriority(1.0) + .intelligencePriority(1.0) + .build()) + .build(); + + StepVerifier.create(exchange.createMessage(craeteMessageRequest)).consumeNextWith(result -> { + assertThat(result).isNotNull(); + assertThat(result.role()).isEqualTo(Role.USER); + assertThat(result.content()).isInstanceOf(McpSchema.TextContent.class); + assertThat(((McpSchema.TextContent) result.content()).text()).isEqualTo("Test message"); + assertThat(result.model()).isEqualTo("MockModelName"); + assertThat(result.stopReason()).isEqualTo(CreateMessageResult.StopReason.STOP_SEQUENCE); + }).verifyComplete(); + + return Mono.just(callResponse); + }) + .build(); var mcpServer = McpServer.async(mcpServerTransportProvider) .serverInfo("test-server", "1.0.0") @@ -314,31 +319,33 @@ void testCreateMessageWithRequestTimeoutFail() throws InterruptedException { CallToolResult callResponse = new McpSchema.CallToolResult(List.of(new McpSchema.TextContent("CALL RESPONSE")), null); - McpServerFeatures.AsyncToolSpecification tool = new McpServerFeatures.AsyncToolSpecification( - new McpSchema.Tool("tool1", "tool1 description", emptyJsonSchema), (exchange, request) -> { - - var craeteMessageRequest = McpSchema.CreateMessageRequest.builder() - .messages(List.of(new McpSchema.SamplingMessage(McpSchema.Role.USER, - new McpSchema.TextContent("Test message")))) - .modelPreferences(ModelPreferences.builder() - .hints(List.of()) - .costPriority(1.0) - .speedPriority(1.0) - .intelligencePriority(1.0) - .build()) - .build(); - - StepVerifier.create(exchange.createMessage(craeteMessageRequest)).consumeNextWith(result -> { - assertThat(result).isNotNull(); - assertThat(result.role()).isEqualTo(Role.USER); - assertThat(result.content()).isInstanceOf(McpSchema.TextContent.class); - assertThat(((McpSchema.TextContent) result.content()).text()).isEqualTo("Test message"); - assertThat(result.model()).isEqualTo("MockModelName"); - assertThat(result.stopReason()).isEqualTo(CreateMessageResult.StopReason.STOP_SEQUENCE); - }).verifyComplete(); - - return Mono.just(callResponse); - }); + McpServerFeatures.AsyncToolSpecification tool = McpServerFeatures.AsyncToolSpecification.builder() + .tool(new McpSchema.Tool("tool1", "tool1 description", emptyJsonSchema)) + .callTool((exchange, request) -> { + + var craeteMessageRequest = McpSchema.CreateMessageRequest.builder() + .messages(List.of(new McpSchema.SamplingMessage(McpSchema.Role.USER, + new McpSchema.TextContent("Test message")))) + .modelPreferences(ModelPreferences.builder() + .hints(List.of()) + .costPriority(1.0) + .speedPriority(1.0) + .intelligencePriority(1.0) + .build()) + .build(); + + StepVerifier.create(exchange.createMessage(craeteMessageRequest)).consumeNextWith(result -> { + assertThat(result).isNotNull(); + assertThat(result.role()).isEqualTo(Role.USER); + assertThat(result.content()).isInstanceOf(McpSchema.TextContent.class); + assertThat(((McpSchema.TextContent) result.content()).text()).isEqualTo("Test message"); + assertThat(result.model()).isEqualTo("MockModelName"); + assertThat(result.stopReason()).isEqualTo(CreateMessageResult.StopReason.STOP_SEQUENCE); + }).verifyComplete(); + + return Mono.just(callResponse); + }) + .build(); var mcpServer = McpServer.async(mcpServerTransportProvider) .serverInfo("test-server", "1.0.0") @@ -363,13 +370,15 @@ void testCreateMessageWithRequestTimeoutFail() throws InterruptedException { @Test void testCreateElicitationWithoutElicitationCapabilities() { - McpServerFeatures.AsyncToolSpecification tool = new McpServerFeatures.AsyncToolSpecification( - new McpSchema.Tool("tool1", "tool1 description", emptyJsonSchema), (exchange, request) -> { + McpServerFeatures.AsyncToolSpecification tool = McpServerFeatures.AsyncToolSpecification.builder() + .tool(new McpSchema.Tool("tool1", "tool1 description", emptyJsonSchema)) + .callTool((exchange, request) -> { - exchange.createElicitation(mock(McpSchema.ElicitRequest.class)).block(); + exchange.createElicitation(mock(McpSchema.ElicitRequest.class)).block(); - return Mono.just(mock(CallToolResult.class)); - }); + return Mono.just(mock(CallToolResult.class)); + }) + .build(); var server = McpServer.async(mcpServerTransportProvider).serverInfo("test-server", "1.0.0").tools(tool).build(); @@ -404,23 +413,25 @@ void testCreateElicitationSuccess() { CallToolResult callResponse = new McpSchema.CallToolResult(List.of(new McpSchema.TextContent("CALL RESPONSE")), null); - McpServerFeatures.AsyncToolSpecification tool = new McpServerFeatures.AsyncToolSpecification( - new McpSchema.Tool("tool1", "tool1 description", emptyJsonSchema), (exchange, request) -> { + McpServerFeatures.AsyncToolSpecification tool = McpServerFeatures.AsyncToolSpecification.builder() + .tool(new McpSchema.Tool("tool1", "tool1 description", emptyJsonSchema)) + .callTool((exchange, request) -> { - var elicitationRequest = McpSchema.ElicitRequest.builder() - .message("Test message") - .requestedSchema( - Map.of("type", "object", "properties", Map.of("message", Map.of("type", "string")))) - .build(); + var elicitationRequest = McpSchema.ElicitRequest.builder() + .message("Test message") + .requestedSchema( + Map.of("type", "object", "properties", Map.of("message", Map.of("type", "string")))) + .build(); - StepVerifier.create(exchange.createElicitation(elicitationRequest)).consumeNextWith(result -> { - assertThat(result).isNotNull(); - assertThat(result.action()).isEqualTo(McpSchema.ElicitResult.Action.ACCEPT); - assertThat(result.content().get("message")).isEqualTo("Test message"); - }).verifyComplete(); + StepVerifier.create(exchange.createElicitation(elicitationRequest)).consumeNextWith(result -> { + assertThat(result).isNotNull(); + assertThat(result.action()).isEqualTo(McpSchema.ElicitResult.Action.ACCEPT); + assertThat(result.content().get("message")).isEqualTo("Test message"); + }).verifyComplete(); - return Mono.just(callResponse); - }); + return Mono.just(callResponse); + }) + .build(); var mcpServer = McpServer.async(mcpServerTransportProvider) .serverInfo("test-server", "1.0.0") @@ -471,23 +482,25 @@ void testCreateElicitationWithRequestTimeoutSuccess() { CallToolResult callResponse = new McpSchema.CallToolResult(List.of(new McpSchema.TextContent("CALL RESPONSE")), null); - McpServerFeatures.AsyncToolSpecification tool = new McpServerFeatures.AsyncToolSpecification( - new McpSchema.Tool("tool1", "tool1 description", emptyJsonSchema), (exchange, request) -> { + McpServerFeatures.AsyncToolSpecification tool = McpServerFeatures.AsyncToolSpecification.builder() + .tool(new McpSchema.Tool("tool1", "tool1 description", emptyJsonSchema)) + .callTool((exchange, request) -> { - var elicitationRequest = McpSchema.ElicitRequest.builder() - .message("Test message") - .requestedSchema( - Map.of("type", "object", "properties", Map.of("message", Map.of("type", "string")))) - .build(); + var elicitationRequest = McpSchema.ElicitRequest.builder() + .message("Test message") + .requestedSchema( + Map.of("type", "object", "properties", Map.of("message", Map.of("type", "string")))) + .build(); - StepVerifier.create(exchange.createElicitation(elicitationRequest)).consumeNextWith(result -> { - assertThat(result).isNotNull(); - assertThat(result.action()).isEqualTo(McpSchema.ElicitResult.Action.ACCEPT); - assertThat(result.content().get("message")).isEqualTo("Test message"); - }).verifyComplete(); + StepVerifier.create(exchange.createElicitation(elicitationRequest)).consumeNextWith(result -> { + assertThat(result).isNotNull(); + assertThat(result.action()).isEqualTo(McpSchema.ElicitResult.Action.ACCEPT); + assertThat(result.content().get("message")).isEqualTo("Test message"); + }).verifyComplete(); - return Mono.just(callResponse); - }); + return Mono.just(callResponse); + }) + .build(); var mcpServer = McpServer.async(mcpServerTransportProvider) .serverInfo("test-server", "1.0.0") @@ -535,23 +548,25 @@ void testCreateElicitationWithRequestTimeoutFail() { CallToolResult callResponse = new McpSchema.CallToolResult(List.of(new McpSchema.TextContent("CALL RESPONSE")), null); - McpServerFeatures.AsyncToolSpecification tool = new McpServerFeatures.AsyncToolSpecification( - new McpSchema.Tool("tool1", "tool1 description", emptyJsonSchema), (exchange, request) -> { + McpServerFeatures.AsyncToolSpecification tool = McpServerFeatures.AsyncToolSpecification.builder() + .tool(new McpSchema.Tool("tool1", "tool1 description", emptyJsonSchema)) + .callTool((exchange, request) -> { - var elicitationRequest = McpSchema.ElicitRequest.builder() - .message("Test message") - .requestedSchema( - Map.of("type", "object", "properties", Map.of("message", Map.of("type", "string")))) - .build(); + var elicitationRequest = McpSchema.ElicitRequest.builder() + .message("Test message") + .requestedSchema( + Map.of("type", "object", "properties", Map.of("message", Map.of("type", "string")))) + .build(); - StepVerifier.create(exchange.createElicitation(elicitationRequest)).consumeNextWith(result -> { - assertThat(result).isNotNull(); - assertThat(result.action()).isEqualTo(McpSchema.ElicitResult.Action.ACCEPT); - assertThat(result.content().get("message")).isEqualTo("Test message"); - }).verifyComplete(); + StepVerifier.create(exchange.createElicitation(elicitationRequest)).consumeNextWith(result -> { + assertThat(result).isNotNull(); + assertThat(result.action()).isEqualTo(McpSchema.ElicitResult.Action.ACCEPT); + assertThat(result.content().get("message")).isEqualTo("Test message"); + }).verifyComplete(); - return Mono.just(callResponse); - }); + return Mono.just(callResponse); + }) + .build(); var mcpServer = McpServer.async(mcpServerTransportProvider) .serverInfo("test-server", "1.0.0") @@ -620,13 +635,15 @@ void testRootsSuccess() { @Test void testRootsWithoutCapability() { - McpServerFeatures.SyncToolSpecification tool = new McpServerFeatures.SyncToolSpecification( - new McpSchema.Tool("tool1", "tool1 description", emptyJsonSchema), (exchange, request) -> { + McpServerFeatures.SyncToolSpecification tool = McpServerFeatures.SyncToolSpecification.builder() + .tool(new McpSchema.Tool("tool1", "tool1 description", emptyJsonSchema)) + .callTool((exchange, request) -> { - exchange.listRoots(); // try to list roots + exchange.listRoots(); // try to list roots - return mock(CallToolResult.class); - }); + return mock(CallToolResult.class); + }) + .build(); var mcpServer = McpServer.sync(mcpServerTransportProvider).rootsChangeHandler((exchange, rootsUpdate) -> { }).tools(tool).build(); @@ -747,17 +764,19 @@ void testRootsServerCloseWithActiveSubscription() { void testToolCallSuccess() { var callResponse = new McpSchema.CallToolResult(List.of(new McpSchema.TextContent("CALL RESPONSE")), null); - McpServerFeatures.SyncToolSpecification tool1 = new McpServerFeatures.SyncToolSpecification( - new McpSchema.Tool("tool1", "tool1 description", emptyJsonSchema), (exchange, request) -> { - // perform a blocking call to a remote service - String response = RestClient.create() - .get() - .uri("https://raw.githubusercontent.com/modelcontextprotocol/java-sdk/refs/heads/main/README.md") - .retrieve() - .body(String.class); - assertThat(response).isNotBlank(); - return callResponse; - }); + McpServerFeatures.SyncToolSpecification tool1 = McpServerFeatures.SyncToolSpecification.builder() + .tool(new McpSchema.Tool("tool1", "tool1 description", emptyJsonSchema)) + .callTool((exchange, request) -> { + // perform a blocking call to a remote service + String response = RestClient.create() + .get() + .uri("https://raw.githubusercontent.com/modelcontextprotocol/java-sdk/refs/heads/main/README.md") + .retrieve() + .body(String.class); + assertThat(response).isNotBlank(); + return callResponse; + }) + .build(); var mcpServer = McpServer.sync(mcpServerTransportProvider) .capabilities(ServerCapabilities.builder().tools(true).build()) @@ -783,17 +802,19 @@ void testToolCallSuccess() { void testToolListChangeHandlingSuccess() { var callResponse = new McpSchema.CallToolResult(List.of(new McpSchema.TextContent("CALL RESPONSE")), null); - McpServerFeatures.SyncToolSpecification tool1 = new McpServerFeatures.SyncToolSpecification( - new McpSchema.Tool("tool1", "tool1 description", emptyJsonSchema), (exchange, request) -> { - // perform a blocking call to a remote service - String response = RestClient.create() - .get() - .uri("https://raw.githubusercontent.com/modelcontextprotocol/java-sdk/refs/heads/main/README.md") - .retrieve() - .body(String.class); - assertThat(response).isNotBlank(); - return callResponse; - }); + McpServerFeatures.SyncToolSpecification tool1 = McpServerFeatures.SyncToolSpecification.builder() + .tool(new McpSchema.Tool("tool1", "tool1 description", emptyJsonSchema)) + .callTool((exchange, request) -> { + // perform a blocking call to a remote service + String response = RestClient.create() + .get() + .uri("https://raw.githubusercontent.com/modelcontextprotocol/java-sdk/refs/heads/main/README.md") + .retrieve() + .body(String.class); + assertThat(response).isNotBlank(); + return callResponse; + }) + .build(); AtomicReference> rootsRef = new AtomicReference<>(); @@ -834,9 +855,10 @@ void testToolListChangeHandlingSuccess() { }); // Add a new tool - McpServerFeatures.SyncToolSpecification tool2 = new McpServerFeatures.SyncToolSpecification( - new McpSchema.Tool("tool2", "tool2 description", emptyJsonSchema), - (exchange, request) -> callResponse); + McpServerFeatures.SyncToolSpecification tool2 = McpServerFeatures.SyncToolSpecification.builder() + .tool(new McpSchema.Tool("tool2", "tool2 description", emptyJsonSchema)) + .callTool((exchange, request) -> callResponse) + .build(); mcpServer.addTool(tool2); diff --git a/mcp-test/src/main/java/io/modelcontextprotocol/server/AbstractMcpAsyncServerTests.java b/mcp-test/src/main/java/io/modelcontextprotocol/server/AbstractMcpAsyncServerTests.java index 12827f469..905c473f3 100644 --- a/mcp-test/src/main/java/io/modelcontextprotocol/server/AbstractMcpAsyncServerTests.java +++ b/mcp-test/src/main/java/io/modelcontextprotocol/server/AbstractMcpAsyncServerTests.java @@ -34,7 +34,6 @@ * * @author Christian Tzolov */ -// KEEP IN SYNC with the class in mcp-test module public abstract class AbstractMcpAsyncServerTests { private static final String TEST_TOOL_NAME = "test-tool"; @@ -102,6 +101,7 @@ void testImmediateClose() { """; @Test + @Deprecated void testAddTool() { Tool newTool = new McpSchema.Tool("new-tool", "New test tool", emptyJsonSchema); var mcpAsyncServer = McpServer.async(createMcpTransportProvider()) @@ -117,6 +117,23 @@ void testAddTool() { } @Test + void testAddToolCall() { + Tool newTool = new McpSchema.Tool("new-tool", "New test tool", emptyJsonSchema); + var mcpAsyncServer = McpServer.async(createMcpTransportProvider()) + .serverInfo("test-server", "1.0.0") + .capabilities(ServerCapabilities.builder().tools(true).build()) + .build(); + + StepVerifier.create(mcpAsyncServer.addTool(McpServerFeatures.AsyncToolSpecification.builder() + .tool(newTool) + .callTool((exchange, request) -> Mono.just(new CallToolResult(List.of(), false))) + .build())).verifyComplete(); + + assertThatCode(() -> mcpAsyncServer.closeGracefully().block(Duration.ofSeconds(10))).doesNotThrowAnyException(); + } + + @Test + @Deprecated void testAddDuplicateTool() { Tool duplicateTool = new McpSchema.Tool(TEST_TOOL_NAME, "Duplicate tool", emptyJsonSchema); @@ -137,6 +154,83 @@ void testAddDuplicateTool() { assertThatCode(() -> mcpAsyncServer.closeGracefully().block(Duration.ofSeconds(10))).doesNotThrowAnyException(); } + @Test + void testAddDuplicateToolCall() { + Tool duplicateTool = new McpSchema.Tool(TEST_TOOL_NAME, "Duplicate tool", emptyJsonSchema); + + var mcpAsyncServer = McpServer.async(createMcpTransportProvider()) + .serverInfo("test-server", "1.0.0") + .capabilities(ServerCapabilities.builder().tools(true).build()) + .toolCall(duplicateTool, (exchange, request) -> Mono.just(new CallToolResult(List.of(), false))) + .build(); + + StepVerifier.create(mcpAsyncServer.addTool(McpServerFeatures.AsyncToolSpecification.builder() + .tool(duplicateTool) + .callTool((exchange, request) -> Mono.just(new CallToolResult(List.of(), false))) + .build())).verifyErrorSatisfies(error -> { + assertThat(error).isInstanceOf(McpError.class) + .hasMessage("Tool with name '" + TEST_TOOL_NAME + "' already exists"); + }); + + assertThatCode(() -> mcpAsyncServer.closeGracefully().block(Duration.ofSeconds(10))).doesNotThrowAnyException(); + } + + @Test + void testDuplicateToolCallDuringBuilding() { + Tool duplicateTool = new Tool("duplicate-build-toolcall", "Duplicate toolcall during building", + emptyJsonSchema); + + assertThatThrownBy(() -> McpServer.async(createMcpTransportProvider()) + .serverInfo("test-server", "1.0.0") + .capabilities(ServerCapabilities.builder().tools(true).build()) + .toolCall(duplicateTool, (exchange, request) -> Mono.just(new CallToolResult(List.of(), false))) + .toolCall(duplicateTool, (exchange, request) -> Mono.just(new CallToolResult(List.of(), false))) // Duplicate! + .build()).isInstanceOf(IllegalArgumentException.class) + .hasMessage("Tool with name 'duplicate-build-toolcall' is already registered."); + } + + @Test + void testDuplicateToolsInBatchListRegistration() { + Tool duplicateTool = new Tool("batch-list-tool", "Duplicate tool in batch list", emptyJsonSchema); + List specs = List.of( + McpServerFeatures.AsyncToolSpecification.builder() + .tool(duplicateTool) + .callTool((exchange, request) -> Mono.just(new CallToolResult(List.of(), false))) + .build(), + McpServerFeatures.AsyncToolSpecification.builder() + .tool(duplicateTool) + .callTool((exchange, request) -> Mono.just(new CallToolResult(List.of(), false))) + .build() // Duplicate! + ); + + assertThatThrownBy(() -> McpServer.async(createMcpTransportProvider()) + .serverInfo("test-server", "1.0.0") + .capabilities(ServerCapabilities.builder().tools(true).build()) + .tools(specs) + .build()).isInstanceOf(IllegalArgumentException.class) + .hasMessage("Tool with name 'batch-list-tool' is already registered."); + } + + @Test + void testDuplicateToolsInBatchVarargsRegistration() { + Tool duplicateTool = new Tool("batch-varargs-tool", "Duplicate tool in batch varargs", emptyJsonSchema); + + assertThatThrownBy(() -> McpServer.async(createMcpTransportProvider()) + .serverInfo("test-server", "1.0.0") + .capabilities(ServerCapabilities.builder().tools(true).build()) + .tools(McpServerFeatures.AsyncToolSpecification.builder() + .tool(duplicateTool) + .callTool((exchange, request) -> Mono.just(new CallToolResult(List.of(), false))) + .build(), + McpServerFeatures.AsyncToolSpecification.builder() + .tool(duplicateTool) + .callTool((exchange, request) -> Mono.just(new CallToolResult(List.of(), false))) + .build() // Duplicate! + ) + .build()).isInstanceOf(IllegalArgumentException.class) + .hasMessage("Tool with name 'batch-varargs-tool' is already registered."); + } + @Test void testRemoveTool() { Tool too = new McpSchema.Tool(TEST_TOOL_NAME, "Duplicate tool", emptyJsonSchema); @@ -144,7 +238,7 @@ void testRemoveTool() { var mcpAsyncServer = McpServer.async(createMcpTransportProvider()) .serverInfo("test-server", "1.0.0") .capabilities(ServerCapabilities.builder().tools(true).build()) - .tool(too, (exchange, args) -> Mono.just(new CallToolResult(List.of(), false))) + .toolCall(too, (exchange, args) -> Mono.just(new CallToolResult(List.of(), false))) .build(); StepVerifier.create(mcpAsyncServer.removeTool(TEST_TOOL_NAME)).verifyComplete(); @@ -173,7 +267,7 @@ void testNotifyToolsListChanged() { var mcpAsyncServer = McpServer.async(createMcpTransportProvider()) .serverInfo("test-server", "1.0.0") .capabilities(ServerCapabilities.builder().tools(true).build()) - .tool(too, (exchange, args) -> Mono.just(new CallToolResult(List.of(), false))) + .toolCall(too, (exchange, args) -> Mono.just(new CallToolResult(List.of(), false))) .build(); StepVerifier.create(mcpAsyncServer.notifyToolsListChanged()).verifyComplete(); 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 eefcdf9a3..a6b4d2f5c 100644 --- a/mcp-test/src/main/java/io/modelcontextprotocol/server/AbstractMcpSyncServerTests.java +++ b/mcp-test/src/main/java/io/modelcontextprotocol/server/AbstractMcpSyncServerTests.java @@ -4,8 +4,16 @@ package io.modelcontextprotocol.server; +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatCode; +import static org.assertj.core.api.Assertions.assertThatThrownBy; + import java.util.List; +import org.junit.jupiter.api.AfterEach; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; + import io.modelcontextprotocol.spec.McpError; import io.modelcontextprotocol.spec.McpSchema; import io.modelcontextprotocol.spec.McpSchema.CallToolResult; @@ -17,21 +25,13 @@ import io.modelcontextprotocol.spec.McpSchema.ServerCapabilities; import io.modelcontextprotocol.spec.McpSchema.Tool; import io.modelcontextprotocol.spec.McpServerTransportProvider; -import org.junit.jupiter.api.AfterEach; -import org.junit.jupiter.api.BeforeEach; -import org.junit.jupiter.api.Test; - -import static org.assertj.core.api.Assertions.assertThat; -import static org.assertj.core.api.Assertions.assertThatCode; -import static org.assertj.core.api.Assertions.assertThatThrownBy; /** * Test suite for the {@link McpSyncServer} that can be used with different - * {@link io.modelcontextprotocol.spec.McpServerTransportProvider} implementations. + * {@link McpTransportProvider} implementations. * * @author Christian Tzolov */ -// KEEP IN SYNC with the class in mcp-test module public abstract class AbstractMcpSyncServerTests { private static final String TEST_TOOL_NAME = "test-tool"; @@ -109,6 +109,7 @@ void testGetAsyncServer() { """; @Test + @Deprecated void testAddTool() { var mcpSyncServer = McpServer.sync(createMcpTransportProvider()) .serverInfo("test-server", "1.0.0") @@ -124,6 +125,23 @@ void testAddTool() { } @Test + void testAddToolCall() { + var mcpSyncServer = McpServer.sync(createMcpTransportProvider()) + .serverInfo("test-server", "1.0.0") + .capabilities(ServerCapabilities.builder().tools(true).build()) + .build(); + + Tool newTool = new McpSchema.Tool("new-tool", "New test tool", emptyJsonSchema); + assertThatCode(() -> mcpSyncServer.addTool(McpServerFeatures.SyncToolSpecification.builder() + .tool(newTool) + .callTool((exchange, request) -> new CallToolResult(List.of(), false)) + .build())).doesNotThrowAnyException(); + + assertThatCode(() -> mcpSyncServer.closeGracefully()).doesNotThrowAnyException(); + } + + @Test + @Deprecated void testAddDuplicateTool() { Tool duplicateTool = new McpSchema.Tool(TEST_TOOL_NAME, "Duplicate tool", emptyJsonSchema); @@ -141,6 +159,81 @@ void testAddDuplicateTool() { assertThatCode(() -> mcpSyncServer.closeGracefully()).doesNotThrowAnyException(); } + @Test + void testAddDuplicateToolCall() { + Tool duplicateTool = new McpSchema.Tool(TEST_TOOL_NAME, "Duplicate tool", emptyJsonSchema); + + var mcpSyncServer = McpServer.sync(createMcpTransportProvider()) + .serverInfo("test-server", "1.0.0") + .capabilities(ServerCapabilities.builder().tools(true).build()) + .toolCall(duplicateTool, (exchange, request) -> new CallToolResult(List.of(), false)) + .build(); + + assertThatThrownBy(() -> mcpSyncServer.addTool(McpServerFeatures.SyncToolSpecification.builder() + .tool(duplicateTool) + .callTool((exchange, request) -> new CallToolResult(List.of(), false)) + .build())).isInstanceOf(McpError.class) + .hasMessage("Tool with name '" + TEST_TOOL_NAME + "' already exists"); + + assertThatCode(() -> mcpSyncServer.closeGracefully()).doesNotThrowAnyException(); + } + + @Test + void testDuplicateToolCallDuringBuilding() { + Tool duplicateTool = new Tool("duplicate-build-toolcall", "Duplicate toolcall during building", + emptyJsonSchema); + + assertThatThrownBy(() -> McpServer.sync(createMcpTransportProvider()) + .serverInfo("test-server", "1.0.0") + .capabilities(ServerCapabilities.builder().tools(true).build()) + .toolCall(duplicateTool, (exchange, request) -> new CallToolResult(List.of(), false)) + .toolCall(duplicateTool, (exchange, request) -> new CallToolResult(List.of(), false)) // Duplicate! + .build()).isInstanceOf(IllegalArgumentException.class) + .hasMessage("Tool with name 'duplicate-build-toolcall' is already registered."); + } + + @Test + void testDuplicateToolsInBatchListRegistration() { + Tool duplicateTool = new Tool("batch-list-tool", "Duplicate tool in batch list", emptyJsonSchema); + List specs = List.of( + McpServerFeatures.SyncToolSpecification.builder() + .tool(duplicateTool) + .callTool((exchange, request) -> new CallToolResult(List.of(), false)) + .build(), + McpServerFeatures.SyncToolSpecification.builder() + .tool(duplicateTool) + .callTool((exchange, request) -> new CallToolResult(List.of(), false)) + .build() // Duplicate! + ); + + assertThatThrownBy(() -> McpServer.sync(createMcpTransportProvider()) + .serverInfo("test-server", "1.0.0") + .capabilities(ServerCapabilities.builder().tools(true).build()) + .tools(specs) + .build()).isInstanceOf(IllegalArgumentException.class) + .hasMessage("Tool with name 'batch-list-tool' is already registered."); + } + + @Test + void testDuplicateToolsInBatchVarargsRegistration() { + Tool duplicateTool = new Tool("batch-varargs-tool", "Duplicate tool in batch varargs", emptyJsonSchema); + + assertThatThrownBy(() -> McpServer.sync(createMcpTransportProvider()) + .serverInfo("test-server", "1.0.0") + .capabilities(ServerCapabilities.builder().tools(true).build()) + .tools(McpServerFeatures.SyncToolSpecification.builder() + .tool(duplicateTool) + .callTool((exchange, request) -> new CallToolResult(List.of(), false)) + .build(), + McpServerFeatures.SyncToolSpecification.builder() + .tool(duplicateTool) + .callTool((exchange, request) -> new CallToolResult(List.of(), false)) + .build() // Duplicate! + ) + .build()).isInstanceOf(IllegalArgumentException.class) + .hasMessage("Tool with name 'batch-varargs-tool' is already registered."); + } + @Test void testRemoveTool() { Tool tool = new McpSchema.Tool(TEST_TOOL_NAME, "Test tool", emptyJsonSchema); @@ -148,7 +241,7 @@ void testRemoveTool() { var mcpSyncServer = McpServer.sync(createMcpTransportProvider()) .serverInfo("test-server", "1.0.0") .capabilities(ServerCapabilities.builder().tools(true).build()) - .tool(tool, (exchange, args) -> new CallToolResult(List.of(), false)) + .toolCall(tool, (exchange, args) -> new CallToolResult(List.of(), false)) .build(); assertThatCode(() -> mcpSyncServer.removeTool(TEST_TOOL_NAME)).doesNotThrowAnyException(); diff --git a/mcp/src/main/java/io/modelcontextprotocol/server/McpAsyncServer.java b/mcp/src/main/java/io/modelcontextprotocol/server/McpAsyncServer.java index 02ad955b9..38ed6525b 100644 --- a/mcp/src/main/java/io/modelcontextprotocol/server/McpAsyncServer.java +++ b/mcp/src/main/java/io/modelcontextprotocol/server/McpAsyncServer.java @@ -268,18 +268,18 @@ private McpServerSession.NotificationHandler asyncRootsListChangedNotificationHa // --------------------------------------- /** - * Add a new tool specification at runtime. - * @param toolSpecification The tool specification to add + * Add a new tool call specification at runtime. + * @param toolCallSpecification The tool specification to add * @return Mono that completes when clients have been notified of the change */ - public Mono addTool(McpServerFeatures.AsyncToolSpecification toolSpecification) { - if (toolSpecification == null) { + public Mono addTool(McpServerFeatures.AsyncToolSpecification toolCallSpecification) { + if (toolCallSpecification == null) { return Mono.error(new McpError("Tool specification must not be null")); } - if (toolSpecification.tool() == null) { + if (toolCallSpecification.tool() == null) { return Mono.error(new McpError("Tool must not be null")); } - if (toolSpecification.call() == null) { + if (toolCallSpecification.call() == null && toolCallSpecification.callTool() == null) { return Mono.error(new McpError("Tool call handler must not be null")); } if (this.serverCapabilities.tools() == null) { @@ -288,13 +288,13 @@ public Mono addTool(McpServerFeatures.AsyncToolSpecification toolSpecifica return Mono.defer(() -> { // Check for duplicate tool names - if (this.tools.stream().anyMatch(th -> th.tool().name().equals(toolSpecification.tool().name()))) { + if (this.tools.stream().anyMatch(th -> th.tool().name().equals(toolCallSpecification.tool().name()))) { return Mono - .error(new McpError("Tool with name '" + toolSpecification.tool().name() + "' already exists")); + .error(new McpError("Tool with name '" + toolCallSpecification.tool().name() + "' already exists")); } - this.tools.add(toolSpecification); - logger.debug("Added tool handler: {}", toolSpecification.tool().name()); + this.tools.add(toolCallSpecification); + logger.debug("Added tool handler: {}", toolCallSpecification.tool().name()); if (this.serverCapabilities.tools().listChanged()) { return notifyToolsListChanged(); @@ -360,7 +360,7 @@ private McpServerSession.RequestHandler toolsCallRequestHandler( return Mono.error(new McpError("Tool not found: " + callToolRequest.name())); } - return toolSpecification.map(tool -> tool.call().apply(exchange, callToolRequest.arguments())) + return toolSpecification.map(tool -> tool.callTool().apply(exchange, callToolRequest)) .orElse(Mono.error(new McpError("Tool not found: " + callToolRequest.name()))); }; } diff --git a/mcp/src/main/java/io/modelcontextprotocol/server/McpServer.java b/mcp/src/main/java/io/modelcontextprotocol/server/McpServer.java index d6ec2cc30..7c88e12d1 100644 --- a/mcp/src/main/java/io/modelcontextprotocol/server/McpServer.java +++ b/mcp/src/main/java/io/modelcontextprotocol/server/McpServer.java @@ -82,12 +82,16 @@ * .capabilities(new ServerCapabilities(...)) * // Register tools * .tools( - * new McpServerFeatures.AsyncToolSpecification(calculatorTool, - * (exchange, args) -> Mono.fromSupplier(() -> calculate(args)) - * .map(result -> new CallToolResult("Result: " + result))), - * new McpServerFeatures.AsyncToolSpecification(weatherTool, - * (exchange, args) -> Mono.fromSupplier(() -> getWeather(args)) - * .map(result -> new CallToolResult("Weather: " + result))) + * McpServerFeatures.AsyncToolSpecification.builder() + * .tool(calculatorTool) + * .callTool((exchange, args) -> Mono.fromSupplier(() -> calculate(args.arguments())) + * .map(result -> new CallToolResult("Result: " + result)))) + *. .build(), + * McpServerFeatures.AsyncToolSpecification.builder() + * .tool((weatherTool) + * .callTool((exchange, args) -> Mono.fromSupplier(() -> getWeather(args.arguments())) + * .map(result -> new CallToolResult("Weather: " + result)))) + * .build() * ) * // Register resources * .resources( @@ -321,17 +325,46 @@ public AsyncSpecification capabilities(McpSchema.ServerCapabilities serverCapabi * map of arguments passed to the tool. * @return This builder instance for method chaining * @throws IllegalArgumentException if tool or handler is null + * @deprecated Use {@link #toolCall(McpSchema.Tool, BiFunction)} instead for tool + * calls that require a request object. */ + @Deprecated public AsyncSpecification tool(McpSchema.Tool tool, BiFunction, Mono> handler) { Assert.notNull(tool, "Tool must not be null"); Assert.notNull(handler, "Handler must not be null"); + assertNoDuplicateTool(tool.name()); this.tools.add(new McpServerFeatures.AsyncToolSpecification(tool, handler)); return this; } + /** + * Adds a single tool with its implementation handler to the server. This is a + * convenience method for registering individual tools without creating a + * {@link McpServerFeatures.AsyncToolCallSpecification} explicitly. + * @param tool The tool definition including name, description, and schema. Must + * not be null. + * @param handler The function that implements the tool's logic. Must not be null. + * The function's first argument is an {@link McpAsyncServerExchange} upon which + * the server can interact with the connected client. The second argument is the + * {@link McpSchema.CallToolRequest} object containing the tool call + * @return This builder instance for method chaining + * @throws IllegalArgumentException if tool or handler is null + */ + public AsyncSpecification toolCall(McpSchema.Tool tool, + BiFunction> handler) { + + Assert.notNull(tool, "Tool must not be null"); + Assert.notNull(handler, "Handler must not be null"); + assertNoDuplicateTool(tool.name()); + + this.tools.add(McpServerFeatures.AsyncToolSpecification.builder().tool(tool).callTool(handler).build()); + + return this; + } + /** * Adds multiple tools with their handlers to the server using a List. This method * is useful when tools are dynamically generated or loaded from a configuration @@ -344,7 +377,12 @@ public AsyncSpecification tool(McpSchema.Tool tool, */ public AsyncSpecification tools(List toolSpecifications) { Assert.notNull(toolSpecifications, "Tool handlers list must not be null"); - this.tools.addAll(toolSpecifications); + + for (var tool : toolSpecifications) { + assertNoDuplicateTool(tool.tool().name()); + this.tools.add(tool); + } + return this; } @@ -355,24 +393,31 @@ public AsyncSpecification tools(List t *

* Example usage:

{@code
 		 * .tools(
-		 *     new McpServerFeatures.AsyncToolSpecification(calculatorTool, calculatorHandler),
-		 *     new McpServerFeatures.AsyncToolSpecification(weatherTool, weatherHandler),
-		 *     new McpServerFeatures.AsyncToolSpecification(fileManagerTool, fileManagerHandler)
+		 *     McpServerFeatures.AsyncToolSpecification.builder().tool(calculatorTool).callTool(calculatorHandler).build(),
+		 *     McpServerFeatures.AsyncToolSpecification.builder().tool(weatherTool).callTool(weatherHandler).build(),
+		 *     McpServerFeatures.AsyncToolSpecification.builder().tool(fileManagerTool).callTool(fileManagerHandler).build()
 		 * )
 		 * }
* @param toolSpecifications The tool specifications to add. Must not be null. * @return This builder instance for method chaining * @throws IllegalArgumentException if toolSpecifications is null - * @see #tools(List) */ public AsyncSpecification tools(McpServerFeatures.AsyncToolSpecification... toolSpecifications) { Assert.notNull(toolSpecifications, "Tool handlers list must not be null"); + for (McpServerFeatures.AsyncToolSpecification tool : toolSpecifications) { + assertNoDuplicateTool(tool.tool().name()); this.tools.add(tool); } return this; } + private void assertNoDuplicateTool(String toolName) { + if (this.tools.stream().anyMatch(toolSpec -> toolSpec.tool().name().equals(toolName))) { + throw new IllegalArgumentException("Tool with name '" + toolName + "' is already registered."); + } + } + /** * Registers multiple resources with their handlers using a Map. This method is * useful when resources are dynamically generated or loaded from a configuration @@ -812,17 +857,45 @@ public SyncSpecification capabilities(McpSchema.ServerCapabilities serverCapabil * list of arguments passed to the tool. * @return This builder instance for method chaining * @throws IllegalArgumentException if tool or handler is null + * @deprecated Use {@link #toolCall(McpSchema.Tool, BiFunction)} instead for tool + * calls that require a request object. */ + @Deprecated public SyncSpecification tool(McpSchema.Tool tool, BiFunction, McpSchema.CallToolResult> handler) { Assert.notNull(tool, "Tool must not be null"); Assert.notNull(handler, "Handler must not be null"); + assertNoDuplicateTool(tool.name()); this.tools.add(new McpServerFeatures.SyncToolSpecification(tool, handler)); return this; } + /** + * Adds a single tool with its implementation handler to the server. This is a + * convenience method for registering individual tools without creating a + * {@link McpServerFeatures.SyncToolSpecification} explicitly. + * @param tool The tool definition including name, description, and schema. Must + * not be null. + * @param handler The function that implements the tool's logic. Must not be null. + * The function's first argument is an {@link McpSyncServerExchange} upon which + * the server can interact with the connected client. The second argument is the + * list of arguments passed to the tool. + * @return This builder instance for method chaining + * @throws IllegalArgumentException if tool or handler is null + */ + public SyncSpecification toolCall(McpSchema.Tool tool, + BiFunction handler) { + Assert.notNull(tool, "Tool must not be null"); + Assert.notNull(handler, "Handler must not be null"); + assertNoDuplicateTool(tool.name()); + + this.tools.add(new McpServerFeatures.SyncToolSpecification(tool, null, handler)); + + return this; + } + /** * Adds multiple tools with their handlers to the server using a List. This method * is useful when tools are dynamically generated or loaded from a configuration @@ -835,7 +908,13 @@ public SyncSpecification tool(McpSchema.Tool tool, */ public SyncSpecification tools(List toolSpecifications) { Assert.notNull(toolSpecifications, "Tool handlers list must not be null"); - this.tools.addAll(toolSpecifications); + + for (var tool : toolSpecifications) { + String toolName = tool.tool().name(); + assertNoDuplicateTool(toolName); // Check against existing tools + this.tools.add(tool); + } + return this; } @@ -858,12 +937,20 @@ public SyncSpecification tools(List too */ public SyncSpecification tools(McpServerFeatures.SyncToolSpecification... toolSpecifications) { Assert.notNull(toolSpecifications, "Tool handlers list must not be null"); + for (McpServerFeatures.SyncToolSpecification tool : toolSpecifications) { + assertNoDuplicateTool(tool.tool().name()); this.tools.add(tool); } return this; } + private void assertNoDuplicateTool(String toolName) { + if (this.tools.stream().anyMatch(toolSpec -> toolSpec.tool().name().equals(toolName))) { + throw new IllegalArgumentException("Tool with name '" + toolName + "' is already registered."); + } + } + /** * Registers multiple resources with their handlers using a Map. This method is * useful when resources are dynamically generated or loaded from a configuration diff --git a/mcp/src/main/java/io/modelcontextprotocol/server/McpServerFeatures.java b/mcp/src/main/java/io/modelcontextprotocol/server/McpServerFeatures.java index 8311f5d41..b818f0b7e 100644 --- a/mcp/src/main/java/io/modelcontextprotocol/server/McpServerFeatures.java +++ b/mcp/src/main/java/io/modelcontextprotocol/server/McpServerFeatures.java @@ -12,6 +12,7 @@ import java.util.function.BiFunction; import io.modelcontextprotocol.spec.McpSchema; +import io.modelcontextprotocol.spec.McpSchema.CallToolRequest; import io.modelcontextprotocol.util.Assert; import io.modelcontextprotocol.util.Utils; import reactor.core.publisher.Mono; @@ -203,52 +204,99 @@ record Sync(McpSchema.Implementation serverInfo, McpSchema.ServerCapabilities se /** * Specification of a tool with its asynchronous handler function. Tools are the * primary way for MCP servers to expose functionality to AI models. Each tool - * represents a specific capability, such as: - *
    - *
  • Performing calculations - *
  • Accessing external APIs - *
  • Querying databases - *
  • Manipulating files - *
  • Executing system commands - *
- * - *

- * Example tool specification:

{@code
-	 * new McpServerFeatures.AsyncToolSpecification(
-	 *     new Tool(
-	 *         "calculator",
-	 *         "Performs mathematical calculations",
-	 *         new JsonSchemaObject()
-	 *             .required("expression")
-	 *             .property("expression", JsonSchemaType.STRING)
-	 *     ),
-	 *     (exchange, args) -> {
-	 *         String expr = (String) args.get("expression");
-	 *         return Mono.fromSupplier(() -> evaluate(expr))
-	 *             .map(result -> new CallToolResult("Result: " + result));
-	 *     }
-	 * )
-	 * }
+ * represents a specific capability. * * @param tool The tool definition including name, description, and parameter schema - * @param call The function that implements the tool's logic, receiving arguments and - * returning results. The function's first argument is an - * {@link McpAsyncServerExchange} upon which the server can interact with the - * connected client. The second arguments is a map of tool arguments. + * @param call Deprecated. Uset he {@link AsyncToolCallSpecification#callTool} + * instead. + * @param callTool The function that implements the tool's logic, receiving a + * {@link McpAsyncServerExchange} and a + * {@link io.modelcontextprotocol.spec.McpSchema.CallToolRequest} and returning + * results. The function's first argument is an {@link McpAsyncServerExchange} upon + * which the server can interact with the connected client. The second arguments is a + * map of tool arguments. */ public record AsyncToolSpecification(McpSchema.Tool tool, - BiFunction, Mono> call) { + @Deprecated BiFunction, Mono> call, + BiFunction> callTool) { - static AsyncToolSpecification fromSync(SyncToolSpecification tool) { + /** + * @deprecated Use {@link #AsyncToolSpecification(McpSchema.Tool, null, + * BiFunction)} instead. + **/ + @Deprecated + public AsyncToolSpecification(McpSchema.Tool tool, + BiFunction, Mono> call) { + this(tool, call, (exchange, toolReq) -> call.apply(exchange, toolReq.arguments())); + } + + static AsyncToolSpecification fromSync(SyncToolSpecification syncToolSpec) { // FIXME: This is temporary, proper validation should be implemented - if (tool == null) { + if (syncToolSpec == null) { return null; } - return new AsyncToolSpecification(tool.tool(), - (exchange, map) -> Mono - .fromCallable(() -> tool.call().apply(new McpSyncServerExchange(exchange), map)) + + return new AsyncToolSpecification(syncToolSpec.tool(), + (syncToolSpec.call() != null) ? (exchange, map) -> Mono + .fromCallable(() -> syncToolSpec.call().apply(new McpSyncServerExchange(exchange), map)) + .subscribeOn(Schedulers.boundedElastic()) : null, + (exchange, req) -> Mono + .fromCallable(() -> syncToolSpec.callTool().apply(new McpSyncServerExchange(exchange), req)) .subscribeOn(Schedulers.boundedElastic())); } + + /** + * Builder for creating AsyncToolSpecification instances. + */ + public static class Builder { + + private McpSchema.Tool tool; + + private BiFunction> callTool; + + /** + * Sets the tool definition. + * @param tool The tool definition including name, description, and parameter + * schema + * @return this builder instance + */ + public Builder tool(McpSchema.Tool tool) { + this.tool = tool; + return this; + } + + /** + * Sets the call tool handler function. + * @param callTool The function that implements the tool's logic + * @return this builder instance + */ + public Builder callTool( + BiFunction> callTool) { + this.callTool = callTool; + return this; + } + + /** + * Builds the AsyncToolSpecification instance. + * @return a new AsyncToolSpecification instance + * @throws IllegalArgumentException if required fields are not set + */ + public AsyncToolSpecification build() { + Assert.notNull(tool, "Tool must not be null"); + Assert.notNull(callTool, "CallTool function must not be null"); + + return new AsyncToolSpecification(tool, null, callTool); + } + + } + + /** + * Creates a new builder instance. + * @return a new Builder instance + */ + public static Builder builder() { + return new Builder(); + } } /** @@ -263,13 +311,13 @@ static AsyncToolSpecification fromSync(SyncToolSpecification tool) { * * *

- * Example resource specification:

{@code
+	 * Example resource specification:
+	 *
+	 * 
{@code
 	 * new McpServerFeatures.AsyncResourceSpecification(
-	 *     new Resource("docs", "Documentation files", "text/markdown"),
-	 *     (exchange, request) ->
-	 *         Mono.fromSupplier(() -> readFile(request.getPath()))
-	 *             .map(ReadResourceResult::new)
-	 * )
+	 * 		new Resource("docs", "Documentation files", "text/markdown"),
+	 * 		(exchange, request) -> Mono.fromSupplier(() -> readFile(request.getPath()))
+	 * 				.map(ReadResourceResult::new))
 	 * }
* * @param resource The resource definition including name, description, and MIME type @@ -305,16 +353,16 @@ static AsyncResourceSpecification fromSync(SyncResourceSpecification resource) { * * *

- * Example prompt specification:

{@code
+	 * Example prompt specification:
+	 *
+	 * 
{@code
 	 * new McpServerFeatures.AsyncPromptSpecification(
-	 *     new Prompt("analyze", "Code analysis template"),
-	 *     (exchange, request) -> {
-	 *         String code = request.getArguments().get("code");
-	 *         return Mono.just(new GetPromptResult(
-	 *             "Analyze this code:\n\n" + code + "\n\nProvide feedback on:"
-	 *         ));
-	 *     }
-	 * )
+	 * 		new Prompt("analyze", "Code analysis template"),
+	 * 		(exchange, request) -> {
+	 * 			String code = request.getArguments().get("code");
+	 * 			return Mono.just(new GetPromptResult(
+	 * 					"Analyze this code:\n\n" + code + "\n\nProvide feedback on:"));
+	 * 		})
 	 * }
* * @param prompt The prompt definition including name and description @@ -379,41 +427,99 @@ static AsyncCompletionSpecification fromSync(SyncCompletionSpecification complet /** * Specification of a tool with its synchronous handler function. Tools are the - * primary way for MCP servers to expose functionality to AI models. Each tool - * represents a specific capability, such as: - *
    - *
  • Performing calculations - *
  • Accessing external APIs - *
  • Querying databases - *
  • Manipulating files - *
  • Executing system commands - *
+ * primary way for MCP servers to expose functionality to AI models. * *

- * Example tool specification:

{@code
-	 * new McpServerFeatures.SyncToolSpecification(
-	 *     new Tool(
-	 *         "calculator",
-	 *         "Performs mathematical calculations",
-	 *         new JsonSchemaObject()
-	 *             .required("expression")
-	 *             .property("expression", JsonSchemaType.STRING)
-	 *     ),
-	 *     (exchange, args) -> {
-	 *         String expr = (String) args.get("expression");
-	 *         return new CallToolResult("Result: " + evaluate(expr));
-	 *     }
-	 * )
+	 * Example tool specification:
+	 *
+	 * 
{@code
+	 * McpServerFeatures.SyncToolSpecification.builder()
+	 * 		.tool(new Tool(
+	 * 				"calculator",
+	 * 				"Performs mathematical calculations",
+	 * 				new JsonSchemaObject()
+	 * 						.required("expression")
+	 * 						.property("expression", JsonSchemaType.STRING)))
+	 * 		.toolCall((exchange, req) -> {
+	 * 			String expr = (String) req.arguments().get("expression");
+	 * 			return new CallToolResult("Result: " + evaluate(expr));
+	 * 		}))
+	 *      .build();
 	 * }
* * @param tool The tool definition including name, description, and parameter schema - * @param call The function that implements the tool's logic, receiving arguments and - * returning results. The function's first argument is an + * @param (deprecated) call The function that implements the tool's logic, receiving + * arguments and returning results. The function's first argument is an * {@link McpSyncServerExchange} upon which the server can interact with the connected - * client. The second arguments is a map of arguments passed to the tool. + * @param callTool The function that implements the tool's logic, receiving a + * {@link McpSyncServerExchange} and a + * {@link io.modelcontextprotocol.spec.McpSchema.CallToolRequest} and returning + * results. The function's first argument is an {@link McpSyncServerExchange} upon + * which the server can interact with the client. The second arguments is a map of + * arguments passed to the tool. */ public record SyncToolSpecification(McpSchema.Tool tool, - BiFunction, McpSchema.CallToolResult> call) { + @Deprecated BiFunction, McpSchema.CallToolResult> call, + BiFunction callTool) { + + @Deprecated + public SyncToolSpecification(McpSchema.Tool tool, + BiFunction, McpSchema.CallToolResult> call) { + this(tool, call, (exchange, toolReq) -> call.apply(exchange, toolReq.arguments())); + } + + /** + * Builder for creating SyncToolSpecification instances. + */ + public static class Builder { + + private McpSchema.Tool tool; + + private BiFunction callTool; + + /** + * Sets the tool definition. + * @param tool The tool definition including name, description, and parameter + * schema + * @return this builder instance + */ + public Builder tool(McpSchema.Tool tool) { + this.tool = tool; + return this; + } + + /** + * Sets the call tool handler function. + * @param callTool The function that implements the tool's logic + * @return this builder instance + */ + public Builder callTool( + BiFunction callTool) { + this.callTool = callTool; + return this; + } + + /** + * Builds the SyncToolSpecification instance. + * @return a new SyncToolSpecification instance + * @throws IllegalArgumentException if required fields are not set + */ + public SyncToolSpecification build() { + Assert.notNull(tool, "Tool must not be null"); + Assert.notNull(callTool, "CallTool function must not be null"); + + return new SyncToolSpecification(tool, null, callTool); + } + + } + + /** + * Creates a new builder instance. + * @return a new Builder instance + */ + public static Builder builder() { + return new Builder(); + } } /** @@ -428,14 +534,15 @@ public record SyncToolSpecification(McpSchema.Tool tool, * * *

- * Example resource specification:

{@code
+	 * Example resource specification:
+	 *
+	 * 
{@code
 	 * new McpServerFeatures.SyncResourceSpecification(
-	 *     new Resource("docs", "Documentation files", "text/markdown"),
-	 *     (exchange, request) -> {
-	 *         String content = readFile(request.getPath());
-	 *         return new ReadResourceResult(content);
-	 *     }
-	 * )
+	 * 		new Resource("docs", "Documentation files", "text/markdown"),
+	 * 		(exchange, request) -> {
+	 * 			String content = readFile(request.getPath());
+	 * 			return new ReadResourceResult(content);
+	 * 		})
 	 * }
* * @param resource The resource definition including name, description, and MIME type @@ -460,16 +567,16 @@ public record SyncResourceSpecification(McpSchema.Resource resource, * * *

- * Example prompt specification:

{@code
+	 * Example prompt specification:
+	 *
+	 * 
{@code
 	 * new McpServerFeatures.SyncPromptSpecification(
-	 *     new Prompt("analyze", "Code analysis template"),
-	 *     (exchange, request) -> {
-	 *         String code = request.getArguments().get("code");
-	 *         return new GetPromptResult(
-	 *             "Analyze this code:\n\n" + code + "\n\nProvide feedback on:"
-	 *         );
-	 *     }
-	 * )
+	 * 		new Prompt("analyze", "Code analysis template"),
+	 * 		(exchange, request) -> {
+	 * 			String code = request.getArguments().get("code");
+	 * 			return new GetPromptResult(
+	 * 					"Analyze this code:\n\n" + code + "\n\nProvide feedback on:");
+	 * 		})
 	 * }
* * @param prompt The prompt definition including name and description diff --git a/mcp/src/main/java/io/modelcontextprotocol/server/McpSyncServer.java b/mcp/src/main/java/io/modelcontextprotocol/server/McpSyncServer.java index 91f8d9e4c..92bc03f8c 100644 --- a/mcp/src/main/java/io/modelcontextprotocol/server/McpSyncServer.java +++ b/mcp/src/main/java/io/modelcontextprotocol/server/McpSyncServer.java @@ -4,6 +4,7 @@ package io.modelcontextprotocol.server; +import io.modelcontextprotocol.server.McpServerFeatures.AsyncToolSpecification; import io.modelcontextprotocol.spec.McpSchema; import io.modelcontextprotocol.spec.McpSchema.LoggingMessageNotification; import io.modelcontextprotocol.util.Assert; @@ -68,7 +69,7 @@ public McpSyncServer(McpAsyncServer asyncServer) { * @param toolHandler The tool handler to add */ public void addTool(McpServerFeatures.SyncToolSpecification toolHandler) { - this.asyncServer.addTool(McpServerFeatures.AsyncToolSpecification.fromSync(toolHandler)).block(); + this.asyncServer.addTool(AsyncToolSpecification.fromSync(toolHandler)).block(); } /** diff --git a/mcp/src/test/java/io/modelcontextprotocol/server/AbstractMcpAsyncServerTests.java b/mcp/src/test/java/io/modelcontextprotocol/server/AbstractMcpAsyncServerTests.java index dd9f65895..c523d1179 100644 --- a/mcp/src/test/java/io/modelcontextprotocol/server/AbstractMcpAsyncServerTests.java +++ b/mcp/src/test/java/io/modelcontextprotocol/server/AbstractMcpAsyncServerTests.java @@ -30,10 +30,11 @@ /** * Test suite for the {@link McpAsyncServer} that can be used with different - * {@link McpTransportProvider} implementations. + * {@link io.modelcontextprotocol.spec.McpServerTransportProvider} implementations. * * @author Christian Tzolov */ +// KEEP IN SYNC with the class in mcp-test module public abstract class AbstractMcpAsyncServerTests { private static final String TEST_TOOL_NAME = "test-tool"; @@ -101,6 +102,7 @@ void testImmediateClose() { """; @Test + @Deprecated void testAddTool() { Tool newTool = new McpSchema.Tool("new-tool", "New test tool", emptyJsonSchema); var mcpAsyncServer = McpServer.async(createMcpTransportProvider()) @@ -116,6 +118,23 @@ void testAddTool() { } @Test + void testAddToolCall() { + Tool newTool = new McpSchema.Tool("new-tool", "New test tool", emptyJsonSchema); + var mcpAsyncServer = McpServer.async(createMcpTransportProvider()) + .serverInfo("test-server", "1.0.0") + .capabilities(ServerCapabilities.builder().tools(true).build()) + .build(); + + StepVerifier.create(mcpAsyncServer.addTool(McpServerFeatures.AsyncToolSpecification.builder() + .tool(newTool) + .callTool((exchange, request) -> Mono.just(new CallToolResult(List.of(), false))) + .build())).verifyComplete(); + + assertThatCode(() -> mcpAsyncServer.closeGracefully().block(Duration.ofSeconds(10))).doesNotThrowAnyException(); + } + + @Test + @Deprecated void testAddDuplicateTool() { Tool duplicateTool = new McpSchema.Tool(TEST_TOOL_NAME, "Duplicate tool", emptyJsonSchema); @@ -136,6 +155,83 @@ void testAddDuplicateTool() { assertThatCode(() -> mcpAsyncServer.closeGracefully().block(Duration.ofSeconds(10))).doesNotThrowAnyException(); } + @Test + void testAddDuplicateToolCall() { + Tool duplicateTool = new McpSchema.Tool(TEST_TOOL_NAME, "Duplicate tool", emptyJsonSchema); + + var mcpAsyncServer = McpServer.async(createMcpTransportProvider()) + .serverInfo("test-server", "1.0.0") + .capabilities(ServerCapabilities.builder().tools(true).build()) + .toolCall(duplicateTool, (exchange, request) -> Mono.just(new CallToolResult(List.of(), false))) + .build(); + + StepVerifier.create(mcpAsyncServer.addTool(McpServerFeatures.AsyncToolSpecification.builder() + .tool(duplicateTool) + .callTool((exchange, request) -> Mono.just(new CallToolResult(List.of(), false))) + .build())).verifyErrorSatisfies(error -> { + assertThat(error).isInstanceOf(McpError.class) + .hasMessage("Tool with name '" + TEST_TOOL_NAME + "' already exists"); + }); + + assertThatCode(() -> mcpAsyncServer.closeGracefully().block(Duration.ofSeconds(10))).doesNotThrowAnyException(); + } + + @Test + void testDuplicateToolCallDuringBuilding() { + Tool duplicateTool = new Tool("duplicate-build-toolcall", "Duplicate toolcall during building", + emptyJsonSchema); + + assertThatThrownBy(() -> McpServer.async(createMcpTransportProvider()) + .serverInfo("test-server", "1.0.0") + .capabilities(ServerCapabilities.builder().tools(true).build()) + .toolCall(duplicateTool, (exchange, request) -> Mono.just(new CallToolResult(List.of(), false))) + .toolCall(duplicateTool, (exchange, request) -> Mono.just(new CallToolResult(List.of(), false))) // Duplicate! + .build()).isInstanceOf(IllegalArgumentException.class) + .hasMessage("Tool with name 'duplicate-build-toolcall' is already registered."); + } + + @Test + void testDuplicateToolsInBatchListRegistration() { + Tool duplicateTool = new Tool("batch-list-tool", "Duplicate tool in batch list", emptyJsonSchema); + List specs = List.of( + McpServerFeatures.AsyncToolSpecification.builder() + .tool(duplicateTool) + .callTool((exchange, request) -> Mono.just(new CallToolResult(List.of(), false))) + .build(), + McpServerFeatures.AsyncToolSpecification.builder() + .tool(duplicateTool) + .callTool((exchange, request) -> Mono.just(new CallToolResult(List.of(), false))) + .build() // Duplicate! + ); + + assertThatThrownBy(() -> McpServer.async(createMcpTransportProvider()) + .serverInfo("test-server", "1.0.0") + .capabilities(ServerCapabilities.builder().tools(true).build()) + .tools(specs) + .build()).isInstanceOf(IllegalArgumentException.class) + .hasMessage("Tool with name 'batch-list-tool' is already registered."); + } + + @Test + void testDuplicateToolsInBatchVarargsRegistration() { + Tool duplicateTool = new Tool("batch-varargs-tool", "Duplicate tool in batch varargs", emptyJsonSchema); + + assertThatThrownBy(() -> McpServer.async(createMcpTransportProvider()) + .serverInfo("test-server", "1.0.0") + .capabilities(ServerCapabilities.builder().tools(true).build()) + .tools(McpServerFeatures.AsyncToolSpecification.builder() + .tool(duplicateTool) + .callTool((exchange, request) -> Mono.just(new CallToolResult(List.of(), false))) + .build(), + McpServerFeatures.AsyncToolSpecification.builder() + .tool(duplicateTool) + .callTool((exchange, request) -> Mono.just(new CallToolResult(List.of(), false))) + .build() // Duplicate! + ) + .build()).isInstanceOf(IllegalArgumentException.class) + .hasMessage("Tool with name 'batch-varargs-tool' is already registered."); + } + @Test void testRemoveTool() { Tool too = new McpSchema.Tool(TEST_TOOL_NAME, "Duplicate tool", emptyJsonSchema); @@ -143,7 +239,7 @@ void testRemoveTool() { var mcpAsyncServer = McpServer.async(createMcpTransportProvider()) .serverInfo("test-server", "1.0.0") .capabilities(ServerCapabilities.builder().tools(true).build()) - .tool(too, (exchange, args) -> Mono.just(new CallToolResult(List.of(), false))) + .toolCall(too, (exchange, args) -> Mono.just(new CallToolResult(List.of(), false))) .build(); StepVerifier.create(mcpAsyncServer.removeTool(TEST_TOOL_NAME)).verifyComplete(); @@ -172,7 +268,7 @@ void testNotifyToolsListChanged() { var mcpAsyncServer = McpServer.async(createMcpTransportProvider()) .serverInfo("test-server", "1.0.0") .capabilities(ServerCapabilities.builder().tools(true).build()) - .tool(too, (exchange, args) -> Mono.just(new CallToolResult(List.of(), false))) + .toolCall(too, (exchange, args) -> Mono.just(new CallToolResult(List.of(), false))) .build(); StepVerifier.create(mcpAsyncServer.notifyToolsListChanged()).verifyComplete(); diff --git a/mcp/src/test/java/io/modelcontextprotocol/server/AbstractMcpSyncServerTests.java b/mcp/src/test/java/io/modelcontextprotocol/server/AbstractMcpSyncServerTests.java index 6cbb8632c..018b547cb 100644 --- a/mcp/src/test/java/io/modelcontextprotocol/server/AbstractMcpSyncServerTests.java +++ b/mcp/src/test/java/io/modelcontextprotocol/server/AbstractMcpSyncServerTests.java @@ -4,8 +4,16 @@ package io.modelcontextprotocol.server; +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatCode; +import static org.assertj.core.api.Assertions.assertThatThrownBy; + import java.util.List; +import org.junit.jupiter.api.AfterEach; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; + import io.modelcontextprotocol.spec.McpError; import io.modelcontextprotocol.spec.McpSchema; import io.modelcontextprotocol.spec.McpSchema.CallToolResult; @@ -17,13 +25,6 @@ import io.modelcontextprotocol.spec.McpSchema.ServerCapabilities; import io.modelcontextprotocol.spec.McpSchema.Tool; import io.modelcontextprotocol.spec.McpServerTransportProvider; -import org.junit.jupiter.api.AfterEach; -import org.junit.jupiter.api.BeforeEach; -import org.junit.jupiter.api.Test; - -import static org.assertj.core.api.Assertions.assertThat; -import static org.assertj.core.api.Assertions.assertThatCode; -import static org.assertj.core.api.Assertions.assertThatThrownBy; /** * Test suite for the {@link McpSyncServer} that can be used with different @@ -31,6 +32,7 @@ * * @author Christian Tzolov */ +// KEEP IN SYNC with the class in mcp-test module public abstract class AbstractMcpSyncServerTests { private static final String TEST_TOOL_NAME = "test-tool"; @@ -108,6 +110,7 @@ void testGetAsyncServer() { """; @Test + @Deprecated void testAddTool() { var mcpSyncServer = McpServer.sync(createMcpTransportProvider()) .serverInfo("test-server", "1.0.0") @@ -123,6 +126,23 @@ void testAddTool() { } @Test + void testAddToolCall() { + var mcpSyncServer = McpServer.sync(createMcpTransportProvider()) + .serverInfo("test-server", "1.0.0") + .capabilities(ServerCapabilities.builder().tools(true).build()) + .build(); + + Tool newTool = new McpSchema.Tool("new-tool", "New test tool", emptyJsonSchema); + assertThatCode(() -> mcpSyncServer.addTool(McpServerFeatures.SyncToolSpecification.builder() + .tool(newTool) + .callTool((exchange, request) -> new CallToolResult(List.of(), false)) + .build())).doesNotThrowAnyException(); + + assertThatCode(() -> mcpSyncServer.closeGracefully()).doesNotThrowAnyException(); + } + + @Test + @Deprecated void testAddDuplicateTool() { Tool duplicateTool = new McpSchema.Tool(TEST_TOOL_NAME, "Duplicate tool", emptyJsonSchema); @@ -140,6 +160,81 @@ void testAddDuplicateTool() { assertThatCode(() -> mcpSyncServer.closeGracefully()).doesNotThrowAnyException(); } + @Test + void testAddDuplicateToolCall() { + Tool duplicateTool = new McpSchema.Tool(TEST_TOOL_NAME, "Duplicate tool", emptyJsonSchema); + + var mcpSyncServer = McpServer.sync(createMcpTransportProvider()) + .serverInfo("test-server", "1.0.0") + .capabilities(ServerCapabilities.builder().tools(true).build()) + .toolCall(duplicateTool, (exchange, request) -> new CallToolResult(List.of(), false)) + .build(); + + assertThatThrownBy(() -> mcpSyncServer.addTool(McpServerFeatures.SyncToolSpecification.builder() + .tool(duplicateTool) + .callTool((exchange, request) -> new CallToolResult(List.of(), false)) + .build())).isInstanceOf(McpError.class) + .hasMessage("Tool with name '" + TEST_TOOL_NAME + "' already exists"); + + assertThatCode(() -> mcpSyncServer.closeGracefully()).doesNotThrowAnyException(); + } + + @Test + void testDuplicateToolCallDuringBuilding() { + Tool duplicateTool = new Tool("duplicate-build-toolcall", "Duplicate toolcall during building", + emptyJsonSchema); + + assertThatThrownBy(() -> McpServer.sync(createMcpTransportProvider()) + .serverInfo("test-server", "1.0.0") + .capabilities(ServerCapabilities.builder().tools(true).build()) + .toolCall(duplicateTool, (exchange, request) -> new CallToolResult(List.of(), false)) + .toolCall(duplicateTool, (exchange, request) -> new CallToolResult(List.of(), false)) // Duplicate! + .build()).isInstanceOf(IllegalArgumentException.class) + .hasMessage("Tool with name 'duplicate-build-toolcall' is already registered."); + } + + @Test + void testDuplicateToolsInBatchListRegistration() { + Tool duplicateTool = new Tool("batch-list-tool", "Duplicate tool in batch list", emptyJsonSchema); + List specs = List.of( + McpServerFeatures.SyncToolSpecification.builder() + .tool(duplicateTool) + .callTool((exchange, request) -> new CallToolResult(List.of(), false)) + .build(), + McpServerFeatures.SyncToolSpecification.builder() + .tool(duplicateTool) + .callTool((exchange, request) -> new CallToolResult(List.of(), false)) + .build() // Duplicate! + ); + + assertThatThrownBy(() -> McpServer.sync(createMcpTransportProvider()) + .serverInfo("test-server", "1.0.0") + .capabilities(ServerCapabilities.builder().tools(true).build()) + .tools(specs) + .build()).isInstanceOf(IllegalArgumentException.class) + .hasMessage("Tool with name 'batch-list-tool' is already registered."); + } + + @Test + void testDuplicateToolsInBatchVarargsRegistration() { + Tool duplicateTool = new Tool("batch-varargs-tool", "Duplicate tool in batch varargs", emptyJsonSchema); + + assertThatThrownBy(() -> McpServer.sync(createMcpTransportProvider()) + .serverInfo("test-server", "1.0.0") + .capabilities(ServerCapabilities.builder().tools(true).build()) + .tools(McpServerFeatures.SyncToolSpecification.builder() + .tool(duplicateTool) + .callTool((exchange, request) -> new CallToolResult(List.of(), false)) + .build(), + McpServerFeatures.SyncToolSpecification.builder() + .tool(duplicateTool) + .callTool((exchange, request) -> new CallToolResult(List.of(), false)) + .build() // Duplicate! + ) + .build()).isInstanceOf(IllegalArgumentException.class) + .hasMessage("Tool with name 'batch-varargs-tool' is already registered."); + } + @Test void testRemoveTool() { Tool tool = new McpSchema.Tool(TEST_TOOL_NAME, "Test tool", emptyJsonSchema); @@ -147,7 +242,7 @@ void testRemoveTool() { var mcpSyncServer = McpServer.sync(createMcpTransportProvider()) .serverInfo("test-server", "1.0.0") .capabilities(ServerCapabilities.builder().tools(true).build()) - .tool(tool, (exchange, args) -> new CallToolResult(List.of(), false)) + .toolCall(tool, (exchange, args) -> new CallToolResult(List.of(), false)) .build(); assertThatCode(() -> mcpSyncServer.removeTool(TEST_TOOL_NAME)).doesNotThrowAnyException(); diff --git a/mcp/src/test/java/io/modelcontextprotocol/server/AsyncToolSpecificationBuilderTest.java b/mcp/src/test/java/io/modelcontextprotocol/server/AsyncToolSpecificationBuilderTest.java new file mode 100644 index 000000000..8997c8f42 --- /dev/null +++ b/mcp/src/test/java/io/modelcontextprotocol/server/AsyncToolSpecificationBuilderTest.java @@ -0,0 +1,229 @@ +/* + * Copyright 2024-2024 the original author or authors. + */ + +package io.modelcontextprotocol.server; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatThrownBy; + +import java.util.List; +import java.util.Map; + +import org.junit.jupiter.api.Test; + +import io.modelcontextprotocol.spec.McpSchema.CallToolRequest; +import io.modelcontextprotocol.spec.McpSchema.CallToolResult; +import io.modelcontextprotocol.spec.McpSchema.TextContent; +import io.modelcontextprotocol.spec.McpSchema.Tool; +import reactor.core.publisher.Mono; +import reactor.test.StepVerifier; + +/** + * Tests for {@link McpServerFeatures.AsyncToolSpecification.Builder}. + * + * @author Christian Tzolov + */ +class AsyncToolSpecificationBuilderTest { + + String emptyJsonSchema = """ + { + "type": "object" + } + """; + + @Test + void builderShouldCreateValidAsyncToolSpecification() { + + Tool tool = new Tool("test-tool", "A test tool", emptyJsonSchema); + + McpServerFeatures.AsyncToolSpecification specification = McpServerFeatures.AsyncToolSpecification.builder() + .tool(tool) + .callTool((exchange, request) -> Mono + .just(new CallToolResult(List.of(new TextContent("Test result")), false))) + .build(); + + assertThat(specification).isNotNull(); + assertThat(specification.tool()).isEqualTo(tool); + assertThat(specification.callTool()).isNotNull(); + assertThat(specification.call()).isNull(); // deprecated field should be null + } + + @Test + void builderShouldThrowExceptionWhenToolIsNull() { + assertThatThrownBy(() -> McpServerFeatures.AsyncToolSpecification.builder() + .callTool((exchange, request) -> Mono.just(new CallToolResult(List.of(), false))) + .build()).isInstanceOf(IllegalArgumentException.class).hasMessage("Tool must not be null"); + } + + @Test + void builderShouldThrowExceptionWhenCallToolIsNull() { + Tool tool = new Tool("test-tool", "A test tool", emptyJsonSchema); + + assertThatThrownBy(() -> McpServerFeatures.AsyncToolSpecification.builder().tool(tool).build()) + .isInstanceOf(IllegalArgumentException.class) + .hasMessage("CallTool function must not be null"); + } + + @Test + void builderShouldAllowMethodChaining() { + Tool tool = new Tool("test-tool", "A test tool", emptyJsonSchema); + McpServerFeatures.AsyncToolSpecification.Builder builder = McpServerFeatures.AsyncToolSpecification.builder(); + + // Then - verify method chaining returns the same builder instance + assertThat(builder.tool(tool)).isSameAs(builder); + assertThat(builder.callTool((exchange, request) -> Mono.just(new CallToolResult(List.of(), false)))) + .isSameAs(builder); + } + + @Test + void builtSpecificationShouldExecuteCallToolCorrectly() { + Tool tool = new Tool("calculator", "Simple calculator", emptyJsonSchema); + String expectedResult = "42"; + + McpServerFeatures.AsyncToolSpecification specification = McpServerFeatures.AsyncToolSpecification.builder() + .tool(tool) + .callTool((exchange, request) -> { + return Mono.just(new CallToolResult(List.of(new TextContent(expectedResult)), false)); + }) + .build(); + + CallToolRequest request = new CallToolRequest("calculator", Map.of()); + Mono resultMono = specification.callTool().apply(null, request); + + StepVerifier.create(resultMono).assertNext(result -> { + assertThat(result).isNotNull(); + assertThat(result.content()).hasSize(1); + assertThat(result.content().get(0)).isInstanceOf(TextContent.class); + assertThat(((TextContent) result.content().get(0)).text()).isEqualTo(expectedResult); + assertThat(result.isError()).isFalse(); + }).verifyComplete(); + } + + @Test + @SuppressWarnings("deprecation") + void deprecatedConstructorShouldWorkCorrectly() { + Tool tool = new Tool("deprecated-tool", "A deprecated tool", emptyJsonSchema); + String expectedResult = "deprecated result"; + + // Test the deprecated constructor that takes a 'call' function + McpServerFeatures.AsyncToolSpecification specification = new McpServerFeatures.AsyncToolSpecification(tool, + (exchange, arguments) -> Mono + .just(new CallToolResult(List.of(new TextContent(expectedResult)), false))); + + assertThat(specification).isNotNull(); + assertThat(specification.tool()).isEqualTo(tool); + assertThat(specification.call()).isNotNull(); // deprecated field should be set + assertThat(specification.callTool()).isNotNull(); // should be automatically + // created + + // Test that the callTool function works (it should delegate to the call function) + CallToolRequest request = new CallToolRequest("deprecated-tool", Map.of("arg1", "value1")); + Mono resultMono = specification.callTool().apply(null, request); + + StepVerifier.create(resultMono).assertNext(result -> { + assertThat(result).isNotNull(); + assertThat(result.content()).hasSize(1); + assertThat(result.content().get(0)).isInstanceOf(TextContent.class); + assertThat(((TextContent) result.content().get(0)).text()).isEqualTo(expectedResult); + assertThat(result.isError()).isFalse(); + }).verifyComplete(); + + // Test that the deprecated call function also works directly + Mono callResultMono = specification.call().apply(null, request.arguments()); + + StepVerifier.create(callResultMono).assertNext(result -> { + assertThat(result).isNotNull(); + assertThat(result.content()).hasSize(1); + assertThat(result.content().get(0)).isInstanceOf(TextContent.class); + assertThat(((TextContent) result.content().get(0)).text()).isEqualTo(expectedResult); + assertThat(result.isError()).isFalse(); + }).verifyComplete(); + } + + @Test + void fromSyncShouldConvertSyncToolSpecificationCorrectly() { + Tool tool = new Tool("sync-tool", "A sync tool", emptyJsonSchema); + String expectedResult = "sync result"; + + // Create a sync tool specification + McpServerFeatures.SyncToolSpecification syncSpec = McpServerFeatures.SyncToolSpecification.builder() + .tool(tool) + .callTool((exchange, request) -> new CallToolResult(List.of(new TextContent(expectedResult)), false)) + .build(); + + // Convert to async using fromSync + McpServerFeatures.AsyncToolSpecification asyncSpec = McpServerFeatures.AsyncToolSpecification + .fromSync(syncSpec); + + assertThat(asyncSpec).isNotNull(); + assertThat(asyncSpec.tool()).isEqualTo(tool); + assertThat(asyncSpec.callTool()).isNotNull(); + assertThat(asyncSpec.call()).isNull(); // should be null since sync spec doesn't + // have deprecated call + + // Test that the converted async specification works correctly + CallToolRequest request = new CallToolRequest("sync-tool", Map.of("param", "value")); + Mono resultMono = asyncSpec.callTool().apply(null, request); + + StepVerifier.create(resultMono).assertNext(result -> { + assertThat(result).isNotNull(); + assertThat(result.content()).hasSize(1); + assertThat(result.content().get(0)).isInstanceOf(TextContent.class); + assertThat(((TextContent) result.content().get(0)).text()).isEqualTo(expectedResult); + assertThat(result.isError()).isFalse(); + }).verifyComplete(); + } + + @Test + @SuppressWarnings("deprecation") + void fromSyncShouldConvertSyncToolSpecificationWithDeprecatedCallCorrectly() { + Tool tool = new Tool("sync-deprecated-tool", "A sync tool with deprecated call", emptyJsonSchema); + String expectedResult = "sync deprecated result"; + McpAsyncServerExchange nullExchange = null; // Mock or create a suitable exchange + // if needed + + // Create a sync tool specification using the deprecated constructor + McpServerFeatures.SyncToolSpecification syncSpec = new McpServerFeatures.SyncToolSpecification(tool, + (exchange, arguments) -> new CallToolResult(List.of(new TextContent(expectedResult)), false)); + + // Convert to async using fromSync + McpServerFeatures.AsyncToolSpecification asyncSpec = McpServerFeatures.AsyncToolSpecification + .fromSync(syncSpec); + + assertThat(asyncSpec).isNotNull(); + assertThat(asyncSpec.tool()).isEqualTo(tool); + assertThat(asyncSpec.callTool()).isNotNull(); + assertThat(asyncSpec.call()).isNotNull(); // should be set since sync spec has + // deprecated call + + // Test that the converted async specification works correctly via callTool + CallToolRequest request = new CallToolRequest("sync-deprecated-tool", Map.of("param", "value")); + Mono resultMono = asyncSpec.callTool().apply(nullExchange, request); + + StepVerifier.create(resultMono).assertNext(result -> { + assertThat(result).isNotNull(); + assertThat(result.content()).hasSize(1); + assertThat(result.content().get(0)).isInstanceOf(TextContent.class); + assertThat(((TextContent) result.content().get(0)).text()).isEqualTo(expectedResult); + assertThat(result.isError()).isFalse(); + }).verifyComplete(); + + // Test that the deprecated call function also works + Mono callResultMono = asyncSpec.call().apply(nullExchange, request.arguments()); + + StepVerifier.create(callResultMono).assertNext(result -> { + assertThat(result).isNotNull(); + assertThat(result.content()).hasSize(1); + assertThat(result.content().get(0)).isInstanceOf(TextContent.class); + assertThat(((TextContent) result.content().get(0)).text()).isEqualTo(expectedResult); + assertThat(result.isError()).isFalse(); + }).verifyComplete(); + } + + @Test + void fromSyncShouldReturnNullWhenSyncSpecIsNull() { + assertThat(McpServerFeatures.AsyncToolSpecification.fromSync(null)).isNull(); + } + +} diff --git a/mcp/src/test/java/io/modelcontextprotocol/server/BaseMcpAsyncServerTests.java b/mcp/src/test/java/io/modelcontextprotocol/server/BaseMcpAsyncServerTests.java deleted file mode 100644 index 208bcb71b..000000000 --- a/mcp/src/test/java/io/modelcontextprotocol/server/BaseMcpAsyncServerTests.java +++ /dev/null @@ -1,5 +0,0 @@ -package io.modelcontextprotocol.server; - -public abstract class BaseMcpAsyncServerTests { - -} diff --git a/mcp/src/test/java/io/modelcontextprotocol/server/SyncToolSpecificationBuilderTest.java b/mcp/src/test/java/io/modelcontextprotocol/server/SyncToolSpecificationBuilderTest.java new file mode 100644 index 000000000..e493de29f --- /dev/null +++ b/mcp/src/test/java/io/modelcontextprotocol/server/SyncToolSpecificationBuilderTest.java @@ -0,0 +1,98 @@ +/* + * Copyright 2024-2024 the original author or authors. + */ + +package io.modelcontextprotocol.server; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatThrownBy; + +import java.util.List; +import java.util.Map; + +import org.junit.jupiter.api.Test; + +import io.modelcontextprotocol.spec.McpSchema.CallToolRequest; +import io.modelcontextprotocol.spec.McpSchema.CallToolResult; +import io.modelcontextprotocol.spec.McpSchema.TextContent; +import io.modelcontextprotocol.spec.McpSchema.Tool; + +/** + * Tests for {@link McpServerFeatures.SyncToolSpecification.Builder}. + * + * @author Christian Tzolov + */ +class SyncToolSpecificationBuilderTest { + + String emptyJsonSchema = """ + { + "type": "object" + } + """; + + @Test + void builderShouldCreateValidSyncToolSpecification() { + + Tool tool = new Tool("test-tool", "A test tool", emptyJsonSchema); + + McpServerFeatures.SyncToolSpecification specification = McpServerFeatures.SyncToolSpecification.builder() + .tool(tool) + .callTool((exchange, request) -> new CallToolResult(List.of(new TextContent("Test result")), false)) + .build(); + + assertThat(specification).isNotNull(); + assertThat(specification.tool()).isEqualTo(tool); + assertThat(specification.callTool()).isNotNull(); + assertThat(specification.call()).isNull(); // deprecated field should be null + } + + @Test + void builderShouldThrowExceptionWhenToolIsNull() { + assertThatThrownBy(() -> McpServerFeatures.SyncToolSpecification.builder() + .callTool((exchange, request) -> new CallToolResult(List.of(), false)) + .build()).isInstanceOf(IllegalArgumentException.class).hasMessage("Tool must not be null"); + } + + @Test + void builderShouldThrowExceptionWhenCallToolIsNull() { + Tool tool = new Tool("test-tool", "A test tool", emptyJsonSchema); + + assertThatThrownBy(() -> McpServerFeatures.SyncToolSpecification.builder().tool(tool).build()) + .isInstanceOf(IllegalArgumentException.class) + .hasMessage("CallTool function must not be null"); + } + + @Test + void builderShouldAllowMethodChaining() { + Tool tool = new Tool("test-tool", "A test tool", emptyJsonSchema); + McpServerFeatures.SyncToolSpecification.Builder builder = McpServerFeatures.SyncToolSpecification.builder(); + + // Then - verify method chaining returns the same builder instance + assertThat(builder.tool(tool)).isSameAs(builder); + assertThat(builder.callTool((exchange, request) -> new CallToolResult(List.of(), false))).isSameAs(builder); + } + + @Test + void builtSpecificationShouldExecuteCallToolCorrectly() { + Tool tool = new Tool("calculator", "Simple calculator", emptyJsonSchema); + String expectedResult = "42"; + + McpServerFeatures.SyncToolSpecification specification = McpServerFeatures.SyncToolSpecification.builder() + .tool(tool) + .callTool((exchange, request) -> { + // Simple test implementation + return new CallToolResult(List.of(new TextContent(expectedResult)), false); + }) + .build(); + + CallToolRequest request = new CallToolRequest("calculator", Map.of()); + CallToolResult result = specification.callTool().apply(null, request); + + assertThat(result).isNotNull(); + assertThat(result.content()).hasSize(1); + assertThat(result.content().get(0)).isInstanceOf(TextContent.class); + assertThat(((TextContent) result.content().get(0)).text()).isEqualTo(expectedResult); + assertThat(result.isError()).isFalse(); + } + +} diff --git a/mcp/src/test/java/io/modelcontextprotocol/server/transport/HttpServletSseServerTransportProviderIntegrationTests.java b/mcp/src/test/java/io/modelcontextprotocol/server/transport/HttpServletSseServerTransportProviderIntegrationTests.java index 4bd98b406..d378afc78 100644 --- a/mcp/src/test/java/io/modelcontextprotocol/server/transport/HttpServletSseServerTransportProviderIntegrationTests.java +++ b/mcp/src/test/java/io/modelcontextprotocol/server/transport/HttpServletSseServerTransportProviderIntegrationTests.java @@ -109,13 +109,15 @@ public void after() { // @Disabled void testCreateMessageWithoutSamplingCapabilities() { - McpServerFeatures.AsyncToolSpecification tool = new McpServerFeatures.AsyncToolSpecification( - new McpSchema.Tool("tool1", "tool1 description", emptyJsonSchema), (exchange, request) -> { + McpServerFeatures.AsyncToolSpecification tool = McpServerFeatures.AsyncToolSpecification.builder() + .tool(new McpSchema.Tool("tool1", "tool1 description", emptyJsonSchema)) + .callTool((exchange, request) -> { - exchange.createMessage(mock(McpSchema.CreateMessageRequest.class)).block(); + exchange.createMessage(mock(McpSchema.CreateMessageRequest.class)).block(); - return Mono.just(mock(CallToolResult.class)); - }); + return Mono.just(mock(CallToolResult.class)); + }) + .build(); var server = McpServer.async(mcpServerTransportProvider).serverInfo("test-server", "1.0.0").tools(tool).build(); @@ -151,31 +153,33 @@ void testCreateMessageSuccess() { CallToolResult callResponse = new McpSchema.CallToolResult(List.of(new McpSchema.TextContent("CALL RESPONSE")), null); - McpServerFeatures.AsyncToolSpecification tool = new McpServerFeatures.AsyncToolSpecification( - new McpSchema.Tool("tool1", "tool1 description", emptyJsonSchema), (exchange, request) -> { - - var createMessageRequest = McpSchema.CreateMessageRequest.builder() - .messages(List.of(new McpSchema.SamplingMessage(McpSchema.Role.USER, - new McpSchema.TextContent("Test message")))) - .modelPreferences(ModelPreferences.builder() - .hints(List.of()) - .costPriority(1.0) - .speedPriority(1.0) - .intelligencePriority(1.0) - .build()) - .build(); - - StepVerifier.create(exchange.createMessage(createMessageRequest)).consumeNextWith(result -> { - assertThat(result).isNotNull(); - assertThat(result.role()).isEqualTo(Role.USER); - assertThat(result.content()).isInstanceOf(McpSchema.TextContent.class); - assertThat(((McpSchema.TextContent) result.content()).text()).isEqualTo("Test message"); - assertThat(result.model()).isEqualTo("MockModelName"); - assertThat(result.stopReason()).isEqualTo(CreateMessageResult.StopReason.STOP_SEQUENCE); - }).verifyComplete(); - - return Mono.just(callResponse); - }); + McpServerFeatures.AsyncToolSpecification tool = McpServerFeatures.AsyncToolSpecification.builder() + .tool(new McpSchema.Tool("tool1", "tool1 description", emptyJsonSchema)) + .callTool((exchange, request) -> { + + var createMessageRequest = McpSchema.CreateMessageRequest.builder() + .messages(List.of(new McpSchema.SamplingMessage(McpSchema.Role.USER, + new McpSchema.TextContent("Test message")))) + .modelPreferences(ModelPreferences.builder() + .hints(List.of()) + .costPriority(1.0) + .speedPriority(1.0) + .intelligencePriority(1.0) + .build()) + .build(); + + StepVerifier.create(exchange.createMessage(createMessageRequest)).consumeNextWith(result -> { + assertThat(result).isNotNull(); + assertThat(result.role()).isEqualTo(Role.USER); + assertThat(result.content()).isInstanceOf(McpSchema.TextContent.class); + assertThat(((McpSchema.TextContent) result.content()).text()).isEqualTo("Test message"); + assertThat(result.model()).isEqualTo("MockModelName"); + assertThat(result.stopReason()).isEqualTo(CreateMessageResult.StopReason.STOP_SEQUENCE); + }).verifyComplete(); + + return Mono.just(callResponse); + }) + .build(); var mcpServer = McpServer.async(mcpServerTransportProvider) .serverInfo("test-server", "1.0.0") @@ -226,31 +230,33 @@ void testCreateMessageWithRequestTimeoutSuccess() throws InterruptedException { CallToolResult callResponse = new McpSchema.CallToolResult(List.of(new McpSchema.TextContent("CALL RESPONSE")), null); - McpServerFeatures.AsyncToolSpecification tool = new McpServerFeatures.AsyncToolSpecification( - new McpSchema.Tool("tool1", "tool1 description", emptyJsonSchema), (exchange, request) -> { - - var craeteMessageRequest = McpSchema.CreateMessageRequest.builder() - .messages(List.of(new McpSchema.SamplingMessage(McpSchema.Role.USER, - new McpSchema.TextContent("Test message")))) - .modelPreferences(ModelPreferences.builder() - .hints(List.of()) - .costPriority(1.0) - .speedPriority(1.0) - .intelligencePriority(1.0) - .build()) - .build(); - - StepVerifier.create(exchange.createMessage(craeteMessageRequest)).consumeNextWith(result -> { - assertThat(result).isNotNull(); - assertThat(result.role()).isEqualTo(Role.USER); - assertThat(result.content()).isInstanceOf(McpSchema.TextContent.class); - assertThat(((McpSchema.TextContent) result.content()).text()).isEqualTo("Test message"); - assertThat(result.model()).isEqualTo("MockModelName"); - assertThat(result.stopReason()).isEqualTo(CreateMessageResult.StopReason.STOP_SEQUENCE); - }).verifyComplete(); - - return Mono.just(callResponse); - }); + McpServerFeatures.AsyncToolSpecification tool = McpServerFeatures.AsyncToolSpecification.builder() + .tool(new McpSchema.Tool("tool1", "tool1 description", emptyJsonSchema)) + .callTool((exchange, request) -> { + + var craeteMessageRequest = McpSchema.CreateMessageRequest.builder() + .messages(List.of(new McpSchema.SamplingMessage(McpSchema.Role.USER, + new McpSchema.TextContent("Test message")))) + .modelPreferences(ModelPreferences.builder() + .hints(List.of()) + .costPriority(1.0) + .speedPriority(1.0) + .intelligencePriority(1.0) + .build()) + .build(); + + StepVerifier.create(exchange.createMessage(craeteMessageRequest)).consumeNextWith(result -> { + assertThat(result).isNotNull(); + assertThat(result.role()).isEqualTo(Role.USER); + assertThat(result.content()).isInstanceOf(McpSchema.TextContent.class); + assertThat(((McpSchema.TextContent) result.content()).text()).isEqualTo("Test message"); + assertThat(result.model()).isEqualTo("MockModelName"); + assertThat(result.stopReason()).isEqualTo(CreateMessageResult.StopReason.STOP_SEQUENCE); + }).verifyComplete(); + + return Mono.just(callResponse); + }) + .build(); var mcpServer = McpServer.async(mcpServerTransportProvider) .serverInfo("test-server", "1.0.0") @@ -298,31 +304,33 @@ void testCreateMessageWithRequestTimeoutFail() throws InterruptedException { CallToolResult callResponse = new McpSchema.CallToolResult(List.of(new McpSchema.TextContent("CALL RESPONSE")), null); - McpServerFeatures.AsyncToolSpecification tool = new McpServerFeatures.AsyncToolSpecification( - new McpSchema.Tool("tool1", "tool1 description", emptyJsonSchema), (exchange, request) -> { - - var craeteMessageRequest = McpSchema.CreateMessageRequest.builder() - .messages(List.of(new McpSchema.SamplingMessage(McpSchema.Role.USER, - new McpSchema.TextContent("Test message")))) - .modelPreferences(ModelPreferences.builder() - .hints(List.of()) - .costPriority(1.0) - .speedPriority(1.0) - .intelligencePriority(1.0) - .build()) - .build(); - - StepVerifier.create(exchange.createMessage(craeteMessageRequest)).consumeNextWith(result -> { - assertThat(result).isNotNull(); - assertThat(result.role()).isEqualTo(Role.USER); - assertThat(result.content()).isInstanceOf(McpSchema.TextContent.class); - assertThat(((McpSchema.TextContent) result.content()).text()).isEqualTo("Test message"); - assertThat(result.model()).isEqualTo("MockModelName"); - assertThat(result.stopReason()).isEqualTo(CreateMessageResult.StopReason.STOP_SEQUENCE); - }).verifyComplete(); - - return Mono.just(callResponse); - }); + McpServerFeatures.AsyncToolSpecification tool = McpServerFeatures.AsyncToolSpecification.builder() + .tool(new McpSchema.Tool("tool1", "tool1 description", emptyJsonSchema)) + .callTool((exchange, request) -> { + + var craeteMessageRequest = McpSchema.CreateMessageRequest.builder() + .messages(List.of(new McpSchema.SamplingMessage(McpSchema.Role.USER, + new McpSchema.TextContent("Test message")))) + .modelPreferences(ModelPreferences.builder() + .hints(List.of()) + .costPriority(1.0) + .speedPriority(1.0) + .intelligencePriority(1.0) + .build()) + .build(); + + StepVerifier.create(exchange.createMessage(craeteMessageRequest)).consumeNextWith(result -> { + assertThat(result).isNotNull(); + assertThat(result.role()).isEqualTo(Role.USER); + assertThat(result.content()).isInstanceOf(McpSchema.TextContent.class); + assertThat(((McpSchema.TextContent) result.content()).text()).isEqualTo("Test message"); + assertThat(result.model()).isEqualTo("MockModelName"); + assertThat(result.stopReason()).isEqualTo(CreateMessageResult.StopReason.STOP_SEQUENCE); + }).verifyComplete(); + + return Mono.just(callResponse); + }) + .build(); var mcpServer = McpServer.async(mcpServerTransportProvider) .serverInfo("test-server", "1.0.0") @@ -348,13 +356,15 @@ void testCreateMessageWithRequestTimeoutFail() throws InterruptedException { // @Disabled void testCreateElicitationWithoutElicitationCapabilities() { - McpServerFeatures.AsyncToolSpecification tool = new McpServerFeatures.AsyncToolSpecification( - new McpSchema.Tool("tool1", "tool1 description", emptyJsonSchema), (exchange, request) -> { + McpServerFeatures.AsyncToolSpecification tool = McpServerFeatures.AsyncToolSpecification.builder() + .tool(new McpSchema.Tool("tool1", "tool1 description", emptyJsonSchema)) + .callTool((exchange, request) -> { - exchange.createElicitation(mock(ElicitRequest.class)).block(); + exchange.createElicitation(mock(ElicitRequest.class)).block(); - return Mono.just(mock(CallToolResult.class)); - }); + return Mono.just(mock(CallToolResult.class)); + }) + .build(); var server = McpServer.async(mcpServerTransportProvider).serverInfo("test-server", "1.0.0").tools(tool).build(); @@ -388,23 +398,25 @@ void testCreateElicitationSuccess() { CallToolResult callResponse = new McpSchema.CallToolResult(List.of(new McpSchema.TextContent("CALL RESPONSE")), null); - McpServerFeatures.AsyncToolSpecification tool = new McpServerFeatures.AsyncToolSpecification( - new McpSchema.Tool("tool1", "tool1 description", emptyJsonSchema), (exchange, request) -> { + McpServerFeatures.AsyncToolSpecification tool = McpServerFeatures.AsyncToolSpecification.builder() + .tool(new McpSchema.Tool("tool1", "tool1 description", emptyJsonSchema)) + .callTool((exchange, request) -> { - var elicitationRequest = ElicitRequest.builder() - .message("Test message") - .requestedSchema( - Map.of("type", "object", "properties", Map.of("message", Map.of("type", "string")))) - .build(); + var elicitationRequest = ElicitRequest.builder() + .message("Test message") + .requestedSchema( + Map.of("type", "object", "properties", Map.of("message", Map.of("type", "string")))) + .build(); - StepVerifier.create(exchange.createElicitation(elicitationRequest)).consumeNextWith(result -> { - assertThat(result).isNotNull(); - assertThat(result.action()).isEqualTo(ElicitResult.Action.ACCEPT); - assertThat(result.content().get("message")).isEqualTo("Test message"); - }).verifyComplete(); + StepVerifier.create(exchange.createElicitation(elicitationRequest)).consumeNextWith(result -> { + assertThat(result).isNotNull(); + assertThat(result.action()).isEqualTo(ElicitResult.Action.ACCEPT); + assertThat(result.content().get("message")).isEqualTo("Test message"); + }).verifyComplete(); - return Mono.just(callResponse); - }); + return Mono.just(callResponse); + }) + .build(); var mcpServer = McpServer.async(mcpServerTransportProvider) .serverInfo("test-server", "1.0.0") @@ -454,23 +466,25 @@ void testCreateElicitationWithRequestTimeoutSuccess() { CallToolResult callResponse = new McpSchema.CallToolResult(List.of(new McpSchema.TextContent("CALL RESPONSE")), null); - McpServerFeatures.AsyncToolSpecification tool = new McpServerFeatures.AsyncToolSpecification( - new McpSchema.Tool("tool1", "tool1 description", emptyJsonSchema), (exchange, request) -> { + McpServerFeatures.AsyncToolSpecification tool = McpServerFeatures.AsyncToolSpecification.builder() + .tool(new McpSchema.Tool("tool1", "tool1 description", emptyJsonSchema)) + .callTool((exchange, request) -> { - var elicitationRequest = ElicitRequest.builder() - .message("Test message") - .requestedSchema( - Map.of("type", "object", "properties", Map.of("message", Map.of("type", "string")))) - .build(); + var elicitationRequest = ElicitRequest.builder() + .message("Test message") + .requestedSchema( + Map.of("type", "object", "properties", Map.of("message", Map.of("type", "string")))) + .build(); - StepVerifier.create(exchange.createElicitation(elicitationRequest)).consumeNextWith(result -> { - assertThat(result).isNotNull(); - assertThat(result.action()).isEqualTo(ElicitResult.Action.ACCEPT); - assertThat(result.content().get("message")).isEqualTo("Test message"); - }).verifyComplete(); + StepVerifier.create(exchange.createElicitation(elicitationRequest)).consumeNextWith(result -> { + assertThat(result).isNotNull(); + assertThat(result.action()).isEqualTo(ElicitResult.Action.ACCEPT); + assertThat(result.content().get("message")).isEqualTo("Test message"); + }).verifyComplete(); - return Mono.just(callResponse); - }); + return Mono.just(callResponse); + }) + .build(); var mcpServer = McpServer.async(mcpServerTransportProvider) .serverInfo("test-server", "1.0.0") @@ -517,23 +531,25 @@ void testCreateElicitationWithRequestTimeoutFail() { CallToolResult callResponse = new McpSchema.CallToolResult(List.of(new McpSchema.TextContent("CALL RESPONSE")), null); - McpServerFeatures.AsyncToolSpecification tool = new McpServerFeatures.AsyncToolSpecification( - new McpSchema.Tool("tool1", "tool1 description", emptyJsonSchema), (exchange, request) -> { + McpServerFeatures.AsyncToolSpecification tool = McpServerFeatures.AsyncToolSpecification.builder() + .tool(new McpSchema.Tool("tool1", "tool1 description", emptyJsonSchema)) + .callTool((exchange, request) -> { - var elicitationRequest = ElicitRequest.builder() - .message("Test message") - .requestedSchema( - Map.of("type", "object", "properties", Map.of("message", Map.of("type", "string")))) - .build(); + var elicitationRequest = ElicitRequest.builder() + .message("Test message") + .requestedSchema( + Map.of("type", "object", "properties", Map.of("message", Map.of("type", "string")))) + .build(); - StepVerifier.create(exchange.createElicitation(elicitationRequest)).consumeNextWith(result -> { - assertThat(result).isNotNull(); - assertThat(result.action()).isEqualTo(ElicitResult.Action.ACCEPT); - assertThat(result.content().get("message")).isEqualTo("Test message"); - }).verifyComplete(); + StepVerifier.create(exchange.createElicitation(elicitationRequest)).consumeNextWith(result -> { + assertThat(result).isNotNull(); + assertThat(result.action()).isEqualTo(ElicitResult.Action.ACCEPT); + assertThat(result.content().get("message")).isEqualTo("Test message"); + }).verifyComplete(); - return Mono.just(callResponse); - }); + return Mono.just(callResponse); + }) + .build(); var mcpServer = McpServer.async(mcpServerTransportProvider) .serverInfo("test-server", "1.0.0") @@ -602,13 +618,15 @@ void testRootsSuccess() { @Test void testRootsWithoutCapability() { - McpServerFeatures.SyncToolSpecification tool = new McpServerFeatures.SyncToolSpecification( - new McpSchema.Tool("tool1", "tool1 description", emptyJsonSchema), (exchange, request) -> { + McpServerFeatures.SyncToolSpecification tool = McpServerFeatures.SyncToolSpecification.builder() + .tool(new McpSchema.Tool("tool1", "tool1 description", emptyJsonSchema)) + .callTool((exchange, request) -> { - exchange.listRoots(); // try to list roots + exchange.listRoots(); // try to list roots - return mock(CallToolResult.class); - }); + return mock(CallToolResult.class); + }) + .build(); var mcpServer = McpServer.sync(mcpServerTransportProvider).rootsChangeHandler((exchange, rootsUpdate) -> { }).tools(tool).build(); @@ -726,17 +744,19 @@ void testRootsServerCloseWithActiveSubscription() { void testToolCallSuccess() { var callResponse = new McpSchema.CallToolResult(List.of(new McpSchema.TextContent("CALL RESPONSE")), null); - McpServerFeatures.SyncToolSpecification tool1 = new McpServerFeatures.SyncToolSpecification( - new McpSchema.Tool("tool1", "tool1 description", emptyJsonSchema), (exchange, request) -> { - // perform a blocking call to a remote service - String response = RestClient.create() - .get() - .uri("https://raw.githubusercontent.com/modelcontextprotocol/java-sdk/refs/heads/main/README.md") - .retrieve() - .body(String.class); - assertThat(response).isNotBlank(); - return callResponse; - }); + McpServerFeatures.SyncToolSpecification tool1 = McpServerFeatures.SyncToolSpecification.builder() + .tool(new McpSchema.Tool("tool1", "tool1 description", emptyJsonSchema)) + .callTool((exchange, request) -> { + // perform a blocking call to a remote service + String response = RestClient.create() + .get() + .uri("https://raw.githubusercontent.com/modelcontextprotocol/java-sdk/refs/heads/main/README.md") + .retrieve() + .body(String.class); + assertThat(response).isNotBlank(); + return callResponse; + }) + .build(); var mcpServer = McpServer.sync(mcpServerTransportProvider) .capabilities(ServerCapabilities.builder().tools(true).build()) @@ -762,17 +782,19 @@ void testToolCallSuccess() { void testToolListChangeHandlingSuccess() { var callResponse = new McpSchema.CallToolResult(List.of(new McpSchema.TextContent("CALL RESPONSE")), null); - McpServerFeatures.SyncToolSpecification tool1 = new McpServerFeatures.SyncToolSpecification( - new McpSchema.Tool("tool1", "tool1 description", emptyJsonSchema), (exchange, request) -> { - // perform a blocking call to a remote service - String response = RestClient.create() - .get() - .uri("https://raw.githubusercontent.com/modelcontextprotocol/java-sdk/refs/heads/main/README.md") - .retrieve() - .body(String.class); - assertThat(response).isNotBlank(); - return callResponse; - }); + McpServerFeatures.SyncToolSpecification tool1 = McpServerFeatures.SyncToolSpecification.builder() + .tool(new McpSchema.Tool("tool1", "tool1 description", emptyJsonSchema)) + .callTool((exchange, request) -> { + // perform a blocking call to a remote service + String response = RestClient.create() + .get() + .uri("https://raw.githubusercontent.com/modelcontextprotocol/java-sdk/refs/heads/main/README.md") + .retrieve() + .body(String.class); + assertThat(response).isNotBlank(); + return callResponse; + }) + .build(); AtomicReference> rootsRef = new AtomicReference<>(); @@ -813,9 +835,10 @@ void testToolListChangeHandlingSuccess() { }); // Add a new tool - McpServerFeatures.SyncToolSpecification tool2 = new McpServerFeatures.SyncToolSpecification( - new McpSchema.Tool("tool2", "tool2 description", emptyJsonSchema), - (exchange, request) -> callResponse); + McpServerFeatures.SyncToolSpecification tool2 = McpServerFeatures.SyncToolSpecification.builder() + .tool(new McpSchema.Tool("tool2", "tool2 description", emptyJsonSchema)) + .callTool((exchange, request) -> callResponse) + .build(); mcpServer.addTool(tool2); @@ -849,59 +872,60 @@ void testLoggingNotification() { List receivedNotifications = new ArrayList<>(); // Create server with a tool that sends logging notifications - McpServerFeatures.AsyncToolSpecification tool = new McpServerFeatures.AsyncToolSpecification( - new McpSchema.Tool("logging-test", "Test logging notifications", emptyJsonSchema), - (exchange, request) -> { - - // Create and send notifications with different levels - - // This should be filtered out (DEBUG < NOTICE) - exchange - .loggingNotification(McpSchema.LoggingMessageNotification.builder() - .level(McpSchema.LoggingLevel.DEBUG) - .logger("test-logger") - .data("Debug message") - .build()) - .block(); - - // This should be sent (NOTICE >= NOTICE) - exchange - .loggingNotification(McpSchema.LoggingMessageNotification.builder() - .level(McpSchema.LoggingLevel.NOTICE) - .logger("test-logger") - .data("Notice message") - .build()) - .block(); - - // This should be sent (ERROR > NOTICE) - exchange - .loggingNotification(McpSchema.LoggingMessageNotification.builder() - .level(McpSchema.LoggingLevel.ERROR) - .logger("test-logger") - .data("Error message") - .build()) - .block(); - - // This should be filtered out (INFO < NOTICE) - exchange - .loggingNotification(McpSchema.LoggingMessageNotification.builder() - .level(McpSchema.LoggingLevel.INFO) - .logger("test-logger") - .data("Another info message") - .build()) - .block(); - - // This should be sent (ERROR >= NOTICE) - exchange - .loggingNotification(McpSchema.LoggingMessageNotification.builder() - .level(McpSchema.LoggingLevel.ERROR) - .logger("test-logger") - .data("Another error message") - .build()) - .block(); - - return Mono.just(new CallToolResult("Logging test completed", false)); - }); + McpServerFeatures.AsyncToolSpecification tool = McpServerFeatures.AsyncToolSpecification.builder() + .tool(new McpSchema.Tool("logging-test", "Test logging notifications", emptyJsonSchema)) + .callTool((exchange, request) -> { + + // Create and send notifications with different levels + + // This should be filtered out (DEBUG < NOTICE) + exchange + .loggingNotification(McpSchema.LoggingMessageNotification.builder() + .level(McpSchema.LoggingLevel.DEBUG) + .logger("test-logger") + .data("Debug message") + .build()) + .block(); + + // This should be sent (NOTICE >= NOTICE) + exchange + .loggingNotification(McpSchema.LoggingMessageNotification.builder() + .level(McpSchema.LoggingLevel.NOTICE) + .logger("test-logger") + .data("Notice message") + .build()) + .block(); + + // This should be sent (ERROR > NOTICE) + exchange + .loggingNotification(McpSchema.LoggingMessageNotification.builder() + .level(McpSchema.LoggingLevel.ERROR) + .logger("test-logger") + .data("Error message") + .build()) + .block(); + + // This should be filtered out (INFO < NOTICE) + exchange + .loggingNotification(McpSchema.LoggingMessageNotification.builder() + .level(McpSchema.LoggingLevel.INFO) + .logger("test-logger") + .data("Another info message") + .build()) + .block(); + + // This should be sent (ERROR >= NOTICE) + exchange + .loggingNotification(McpSchema.LoggingMessageNotification.builder() + .level(McpSchema.LoggingLevel.ERROR) + .logger("test-logger") + .data("Another error message") + .build()) + .block(); + + return Mono.just(new CallToolResult("Logging test completed", false)); + }) + .build(); var mcpServer = McpServer.async(mcpServerTransportProvider) .serverInfo("test-server", "1.0.0")