From 68ea4c00ef9e36b305d33df24d03d042c2015de6 Mon Sep 17 00:00:00 2001 From: Christian Tzolov Date: Mon, 6 Oct 2025 14:46:36 +0200 Subject: [PATCH 1/2] refactor: Migrate MCP test to use autoconfiguration and add ChatClient integration Replace manual MCP annotation providers with autoconfiguration beans. Add ChatClient integration test with Anthropic model in sampling handler. Signed-off-by: Christian Tzolov --- .../pom.xml | 21 +++++ .../StreamableMcpAnnotationsManualIT.java | 94 ++++++++----------- 2 files changed, 62 insertions(+), 53 deletions(-) diff --git a/auto-configurations/mcp/spring-ai-autoconfigure-mcp-server-webflux/pom.xml b/auto-configurations/mcp/spring-ai-autoconfigure-mcp-server-webflux/pom.xml index 822927ba052..c5844c26a25 100644 --- a/auto-configurations/mcp/spring-ai-autoconfigure-mcp-server-webflux/pom.xml +++ b/auto-configurations/mcp/spring-ai-autoconfigure-mcp-server-webflux/pom.xml @@ -95,6 +95,27 @@ test + + org.springframework.ai + spring-ai-autoconfigure-model-anthropic + ${project.parent.version} + test + + + + org.springframework.ai + spring-ai-anthropic + ${project.parent.version} + test + + + + org.springframework.ai + spring-ai-autoconfigure-model-chat-client + ${project.parent.version} + test + + diff --git a/auto-configurations/mcp/spring-ai-autoconfigure-mcp-server-webflux/src/test/java/org/springframework/ai/mcp/server/autoconfigure/StreamableMcpAnnotationsManualIT.java b/auto-configurations/mcp/spring-ai-autoconfigure-mcp-server-webflux/src/test/java/org/springframework/ai/mcp/server/autoconfigure/StreamableMcpAnnotationsManualIT.java index bbb4d374a09..37fca4fb3de 100644 --- a/auto-configurations/mcp/spring-ai-autoconfigure-mcp-server-webflux/src/test/java/org/springframework/ai/mcp/server/autoconfigure/StreamableMcpAnnotationsManualIT.java +++ b/auto-configurations/mcp/spring-ai-autoconfigure-mcp-server-webflux/src/test/java/org/springframework/ai/mcp/server/autoconfigure/StreamableMcpAnnotationsManualIT.java @@ -27,7 +27,6 @@ import com.fasterxml.jackson.databind.ObjectMapper; import io.modelcontextprotocol.client.McpSyncClient; -import io.modelcontextprotocol.server.McpServerFeatures; import io.modelcontextprotocol.server.McpSyncServer; import io.modelcontextprotocol.server.McpSyncServerExchange; import io.modelcontextprotocol.server.transport.WebFluxStreamableServerTransportProvider; @@ -54,6 +53,7 @@ import net.javacrumbs.jsonunit.assertj.JsonAssertions; import net.javacrumbs.jsonunit.core.Option; import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.condition.EnabledIfEnvironmentVariable; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.springaicommunity.mcp.annotation.McpArg; @@ -68,29 +68,33 @@ import org.springaicommunity.mcp.annotation.McpSampling; import org.springaicommunity.mcp.annotation.McpTool; import org.springaicommunity.mcp.annotation.McpToolParam; -import org.springaicommunity.mcp.method.elicitation.SyncElicitationSpecification; -import org.springaicommunity.mcp.method.logging.SyncLoggingSpecification; -import org.springaicommunity.mcp.method.progress.SyncProgressSpecification; -import org.springaicommunity.mcp.method.sampling.SyncSamplingSpecification; import reactor.netty.DisposableServer; import reactor.netty.http.server.HttpServer; -import org.springframework.ai.mcp.annotation.spring.SyncMcpAnnotationProviders; +import org.springframework.ai.chat.client.ChatClient; import org.springframework.ai.mcp.client.common.autoconfigure.McpClientAutoConfiguration; import org.springframework.ai.mcp.client.common.autoconfigure.McpToolCallbackAutoConfiguration; +import org.springframework.ai.mcp.client.common.autoconfigure.annotations.McpClientAnnotationScannerAutoConfiguration; +import org.springframework.ai.mcp.client.common.autoconfigure.annotations.McpClientSpecificationFactoryAutoConfiguration; import org.springframework.ai.mcp.client.webflux.autoconfigure.StreamableHttpWebFluxTransportAutoConfiguration; import org.springframework.ai.mcp.server.common.autoconfigure.McpServerAutoConfiguration; import org.springframework.ai.mcp.server.common.autoconfigure.ToolCallbackConverterAutoConfiguration; +import org.springframework.ai.mcp.server.common.autoconfigure.annotations.McpServerAnnotationScannerAutoConfiguration; +import org.springframework.ai.mcp.server.common.autoconfigure.annotations.McpServerSpecificationFactoryAutoConfiguration; import org.springframework.ai.mcp.server.common.autoconfigure.properties.McpServerProperties; import org.springframework.ai.mcp.server.common.autoconfigure.properties.McpServerStreamableHttpProperties; +import org.springframework.ai.model.anthropic.autoconfigure.AnthropicChatAutoConfiguration; +import org.springframework.ai.model.chat.client.autoconfigure.ChatClientAutoConfiguration; import org.springframework.beans.factory.ObjectProvider; import org.springframework.boot.autoconfigure.AutoConfigurations; import org.springframework.boot.test.context.runner.ApplicationContextRunner; import org.springframework.context.ApplicationContext; import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Lazy; import org.springframework.core.ResolvableType; import org.springframework.http.server.reactive.HttpHandler; import org.springframework.http.server.reactive.ReactorHttpHandlerAdapter; +import org.springframework.stereotype.Service; import org.springframework.test.util.TestSocketUtils; import org.springframework.web.reactive.function.server.RouterFunction; import org.springframework.web.reactive.function.server.RouterFunctions; @@ -98,16 +102,22 @@ import static org.assertj.core.api.Assertions.assertThat; import static org.assertj.core.api.InstanceOfAssertFactories.map; +@EnabledIfEnvironmentVariable(named = "ANTHROPIC_API_KEY", matches = ".+") public class StreamableMcpAnnotationsManualIT { private final ApplicationContextRunner serverContextRunner = new ApplicationContextRunner() .withPropertyValues("spring.ai.mcp.server.protocol=STREAMABLE") - .withConfiguration(AutoConfigurations.of(McpServerAutoConfiguration.class, + .withConfiguration(AutoConfigurations.of(McpServerAnnotationScannerAutoConfiguration.class, + McpServerSpecificationFactoryAutoConfiguration.class, McpServerAutoConfiguration.class, ToolCallbackConverterAutoConfiguration.class, McpServerStreamableHttpWebFluxAutoConfiguration.class)); private final ApplicationContextRunner clientApplicationContext = new ApplicationContextRunner() .withConfiguration(AutoConfigurations.of(McpToolCallbackAutoConfiguration.class, - McpClientAutoConfiguration.class, StreamableHttpWebFluxTransportAutoConfiguration.class)); + McpClientAutoConfiguration.class, StreamableHttpWebFluxTransportAutoConfiguration.class, + // MCP Annotations + McpClientAnnotationScannerAutoConfiguration.class, McpClientSpecificationFactoryAutoConfiguration.class, + // Anthropic ChatClient Builder + AnthropicChatAutoConfiguration.class, ChatClientAutoConfiguration.class)); @Test void clientServerCapabilities() { @@ -141,6 +151,7 @@ void clientServerCapabilities() { this.clientApplicationContext.withUserConfiguration(TestMcpClientConfiguration.class) .withPropertyValues(// @formatter:off + "spring.ai.anthropic.api-key=" + System.getenv("ANTHROPIC_API_KEY"), "spring.ai.mcp.client.streamable-http.connections.server1.url=http://localhost:" + serverPort, // "spring.ai.mcp.client.request-timeout=20m", "spring.ai.mcp.client.initialized=false") // @formatter:on @@ -306,28 +317,6 @@ public McpServerHandlers serverSideSpecProviders() { return new McpServerHandlers(); } - @Bean - public List myTools(McpServerHandlers serverSideSpecProviders) { - return SyncMcpAnnotationProviders.toolSpecifications(List.of(serverSideSpecProviders)); - } - - @Bean - public List myResources( - McpServerHandlers serverSideSpecProviders) { - return SyncMcpAnnotationProviders.resourceSpecifications(List.of(serverSideSpecProviders)); - } - - @Bean - public List myPrompts(McpServerHandlers serverSideSpecProviders) { - return SyncMcpAnnotationProviders.promptSpecifications(List.of(serverSideSpecProviders)); - } - - @Bean - public List myCompletions( - McpServerHandlers serverSideSpecProviders) { - return SyncMcpAnnotationProviders.completeSpecifications(List.of(serverSideSpecProviders)); - } - public static class McpServerHandlers { @McpTool(description = "Test tool", name = "tool1") @@ -449,28 +438,9 @@ public TestContext testContext() { } @Bean - public McpClientHandlers mcpClientHandlers(TestContext testContext) { - return new McpClientHandlers(testContext); - } - - @Bean - List loggingSpecs(McpClientHandlers clientMcpHandlers) { - return SyncMcpAnnotationProviders.loggingSpecifications(List.of(clientMcpHandlers)); - } - - @Bean - List samplingSpecs(McpClientHandlers clientMcpHandlers) { - return SyncMcpAnnotationProviders.samplingSpecifications(List.of(clientMcpHandlers)); - } - - @Bean - List elicitationSpecs(McpClientHandlers clientMcpHandlers) { - return SyncMcpAnnotationProviders.elicitationSpecifications(List.of(clientMcpHandlers)); - } - - @Bean - List progressSpecs(McpClientHandlers clientMcpHandlers) { - return SyncMcpAnnotationProviders.progressSpecifications(List.of(clientMcpHandlers)); + public McpClientHandlers mcpClientHandlers(TestContext testContext, + ObjectProvider chatClientBuilderProvider) { + return new McpClientHandlers(testContext, chatClientBuilderProvider); } public static class TestContext { @@ -489,8 +459,21 @@ public static class McpClientHandlers { private TestContext testContext; - public McpClientHandlers(TestContext testContext) { + private final ObjectProvider chatClientBuilderProvider; + + private AtomicReference chatClientRef = new AtomicReference<>(); + + private ChatClient chatClient() { + if (this.chatClientRef.get() == null) { + this.chatClientRef.compareAndSet(null, this.chatClientBuilderProvider.getIfAvailable().build()); + } + return this.chatClientRef.get(); + } + + public McpClientHandlers(TestContext testContext, + ObjectProvider chatClientBuilderProvider) { this.testContext = testContext; + this.chatClientBuilderProvider = chatClientBuilderProvider; } @McpProgress(clients = "server1") @@ -515,6 +498,11 @@ public CreateMessageResult samplingHandler(CreateMessageRequest llmRequest) { String userPrompt = ((McpSchema.TextContent) llmRequest.messages().get(0).content()).text(); String modelHint = llmRequest.modelPreferences().hints().get(0).name(); + // String joke = + // this.chatClientBuilderProvider.getIfAvailable().build().prompt("Tell me + // a joke").call().content(); + String joke = this.chatClient().prompt("Tell me a joke").call().content(); + logger.info("Received joke from chat client: {}", joke); return CreateMessageResult.builder() .content(new McpSchema.TextContent("Response " + userPrompt + " with model hint " + modelHint)) .build(); From 5f699046df4859000a3d9bc3bea1d45ce3d5ba55 Mon Sep 17 00:00:00 2001 From: Christian Tzolov Date: Mon, 6 Oct 2025 15:56:35 +0200 Subject: [PATCH 2/2] Fix checkstyle Signed-off-by: Christian Tzolov --- .../server/autoconfigure/StreamableMcpAnnotationsManualIT.java | 2 -- 1 file changed, 2 deletions(-) diff --git a/auto-configurations/mcp/spring-ai-autoconfigure-mcp-server-webflux/src/test/java/org/springframework/ai/mcp/server/autoconfigure/StreamableMcpAnnotationsManualIT.java b/auto-configurations/mcp/spring-ai-autoconfigure-mcp-server-webflux/src/test/java/org/springframework/ai/mcp/server/autoconfigure/StreamableMcpAnnotationsManualIT.java index 37fca4fb3de..41c9f65586d 100644 --- a/auto-configurations/mcp/spring-ai-autoconfigure-mcp-server-webflux/src/test/java/org/springframework/ai/mcp/server/autoconfigure/StreamableMcpAnnotationsManualIT.java +++ b/auto-configurations/mcp/spring-ai-autoconfigure-mcp-server-webflux/src/test/java/org/springframework/ai/mcp/server/autoconfigure/StreamableMcpAnnotationsManualIT.java @@ -90,11 +90,9 @@ import org.springframework.boot.test.context.runner.ApplicationContextRunner; import org.springframework.context.ApplicationContext; import org.springframework.context.annotation.Bean; -import org.springframework.context.annotation.Lazy; import org.springframework.core.ResolvableType; import org.springframework.http.server.reactive.HttpHandler; import org.springframework.http.server.reactive.ReactorHttpHandlerAdapter; -import org.springframework.stereotype.Service; import org.springframework.test.util.TestSocketUtils; import org.springframework.web.reactive.function.server.RouterFunction; import org.springframework.web.reactive.function.server.RouterFunctions;