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..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 @@ -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,21 +68,23 @@ 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; @@ -98,16 +100,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 +149,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 +315,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 +436,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 +457,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 +496,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();