From f5dde485789044dc9bf01b3b85a6871897d128cf Mon Sep 17 00:00:00 2001 From: guanxu <1510424541@qq.com> Date: Wed, 29 Oct 2025 22:17:01 +0800 Subject: [PATCH] Add tests for Stdio Transport MCP and fix in StdioTransportAutoConfiguration - Add McpStdioClientPropertiesTests unit test class. - Add StdioTransportAutoConfigurationTests unit test class. - Add StdioTransportAutoConfiguration integration test class. - Fix in StdioTransportAutoConfiguration to use the auto-configured ObjectMapper instead of creating a new instance. Signed-off-by: guanxu <1510424541@qq.com> --- .../StdioTransportAutoConfiguration.java | 8 +- .../StdioTransportAutoConfigurationIT.java | 73 ++++++ .../StdioTransportAutoConfigurationTests.java | 160 ++++++++++++ .../McpStdioClientPropertiesTests.java | 232 ++++++++++++++++++ .../src/test/resources/test-mcp-servers.json | 21 ++ 5 files changed, 492 insertions(+), 2 deletions(-) create mode 100644 auto-configurations/mcp/spring-ai-autoconfigure-mcp-client-common/src/test/java/org/springframework/ai/mcp/client/common/autoconfigure/StdioTransportAutoConfigurationIT.java create mode 100644 auto-configurations/mcp/spring-ai-autoconfigure-mcp-client-common/src/test/java/org/springframework/ai/mcp/client/common/autoconfigure/StdioTransportAutoConfigurationTests.java create mode 100644 auto-configurations/mcp/spring-ai-autoconfigure-mcp-client-common/src/test/java/org/springframework/ai/mcp/client/common/autoconfigure/properties/McpStdioClientPropertiesTests.java create mode 100644 auto-configurations/mcp/spring-ai-autoconfigure-mcp-client-common/src/test/resources/test-mcp-servers.json diff --git a/auto-configurations/mcp/spring-ai-autoconfigure-mcp-client-common/src/main/java/org/springframework/ai/mcp/client/common/autoconfigure/StdioTransportAutoConfiguration.java b/auto-configurations/mcp/spring-ai-autoconfigure-mcp-client-common/src/main/java/org/springframework/ai/mcp/client/common/autoconfigure/StdioTransportAutoConfiguration.java index f3c5c1b4186..f84353eaa85 100644 --- a/auto-configurations/mcp/spring-ai-autoconfigure-mcp-client-common/src/main/java/org/springframework/ai/mcp/client/common/autoconfigure/StdioTransportAutoConfiguration.java +++ b/auto-configurations/mcp/spring-ai-autoconfigure-mcp-client-common/src/main/java/org/springframework/ai/mcp/client/common/autoconfigure/StdioTransportAutoConfiguration.java @@ -28,6 +28,7 @@ import org.springframework.ai.mcp.client.common.autoconfigure.properties.McpClientCommonProperties; import org.springframework.ai.mcp.client.common.autoconfigure.properties.McpStdioClientProperties; +import org.springframework.beans.factory.ObjectProvider; import org.springframework.boot.autoconfigure.AutoConfiguration; import org.springframework.boot.autoconfigure.condition.ConditionalOnClass; import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty; @@ -73,13 +74,16 @@ public class StdioTransportAutoConfiguration { * @return list of named MCP transports */ @Bean - public List stdioTransports(McpStdioClientProperties stdioProperties) { + public List stdioTransports(McpStdioClientProperties stdioProperties, + ObjectProvider objectMapperProvider) { + + ObjectMapper objectMapper = objectMapperProvider.getIfAvailable(ObjectMapper::new); List stdioTransports = new ArrayList<>(); for (Map.Entry serverParameters : stdioProperties.toServerParameters().entrySet()) { var transport = new StdioClientTransport(serverParameters.getValue(), - new JacksonMcpJsonMapper(new ObjectMapper())); + new JacksonMcpJsonMapper(objectMapper)); stdioTransports.add(new NamedClientMcpTransport(serverParameters.getKey(), transport)); } diff --git a/auto-configurations/mcp/spring-ai-autoconfigure-mcp-client-common/src/test/java/org/springframework/ai/mcp/client/common/autoconfigure/StdioTransportAutoConfigurationIT.java b/auto-configurations/mcp/spring-ai-autoconfigure-mcp-client-common/src/test/java/org/springframework/ai/mcp/client/common/autoconfigure/StdioTransportAutoConfigurationIT.java new file mode 100644 index 00000000000..c8d5ec4db19 --- /dev/null +++ b/auto-configurations/mcp/spring-ai-autoconfigure-mcp-client-common/src/test/java/org/springframework/ai/mcp/client/common/autoconfigure/StdioTransportAutoConfigurationIT.java @@ -0,0 +1,73 @@ +/* + * Copyright 2025-2025 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.ai.mcp.client.common.autoconfigure; + +import java.util.List; +import java.util.Map; + +import io.modelcontextprotocol.client.McpSyncClient; +import io.modelcontextprotocol.spec.McpSchema; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.Timeout; + +import org.springframework.boot.autoconfigure.AutoConfigurations; +import org.springframework.boot.test.context.runner.ApplicationContextRunner; + +import static org.assertj.core.api.Assertions.assertThat; + +/** + * Integration tests for {@link StdioTransportAutoConfiguration} with H2. + * + * @author guan xu + */ +@Timeout(15) +@SuppressWarnings("unchecked") +public class StdioTransportAutoConfigurationIT { + + private final ApplicationContextRunner contextRunner = new ApplicationContextRunner().withConfiguration( + AutoConfigurations.of(McpClientAutoConfiguration.class, StdioTransportAutoConfiguration.class)); + + @Test + void connectionsStdioTest() { + this.contextRunner + .withPropertyValues("spring.ai.mcp.client.stdio.connections.server1.command=npx", + "spring.ai.mcp.client.stdio.connections.server1.args[0]=-y", + "spring.ai.mcp.client.stdio.connections.server1.args[1]=@modelcontextprotocol/server-everything") + .run(context -> { + List mcpClients = (List) context.getBean("mcpSyncClients"); + assertThat(mcpClients).isNotNull(); + assertThat(mcpClients).hasSize(1); + + McpSyncClient mcpClient = mcpClients.get(0); + mcpClient.ping(); + + McpSchema.ListToolsResult toolsResult = mcpClient.listTools(); + assertThat(toolsResult).isNotNull(); + assertThat(toolsResult.tools()).isNotEmpty(); + + McpSchema.CallToolResult result = mcpClient.callTool(McpSchema.CallToolRequest.builder() + .name("add") + .arguments(Map.of("operation", "add", "a", 1, "b", 2)) + .build()); + assertThat(result).isNotNull(); + assertThat(result.content()).isNotEmpty(); + + mcpClient.closeGracefully(); + }); + } + +} diff --git a/auto-configurations/mcp/spring-ai-autoconfigure-mcp-client-common/src/test/java/org/springframework/ai/mcp/client/common/autoconfigure/StdioTransportAutoConfigurationTests.java b/auto-configurations/mcp/spring-ai-autoconfigure-mcp-client-common/src/test/java/org/springframework/ai/mcp/client/common/autoconfigure/StdioTransportAutoConfigurationTests.java new file mode 100644 index 00000000000..863e63a4e1e --- /dev/null +++ b/auto-configurations/mcp/spring-ai-autoconfigure-mcp-client-common/src/test/java/org/springframework/ai/mcp/client/common/autoconfigure/StdioTransportAutoConfigurationTests.java @@ -0,0 +1,160 @@ +/* + * Copyright 2025-2025 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.ai.mcp.client.common.autoconfigure; + +import java.lang.reflect.Field; +import java.util.List; + +import com.fasterxml.jackson.databind.ObjectMapper; +import io.modelcontextprotocol.client.transport.StdioClientTransport; +import org.junit.jupiter.api.Test; + +import org.springframework.beans.factory.NoSuchBeanDefinitionException; +import org.springframework.boot.autoconfigure.AutoConfigurations; +import org.springframework.boot.test.context.runner.ApplicationContextRunner; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.util.ReflectionUtils; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatThrownBy; + +/** + * Tests for {@link StdioTransportAutoConfiguration}. + * + * @author guan xu + */ +@SuppressWarnings("unchecked") +public class StdioTransportAutoConfigurationTests { + + private final ApplicationContextRunner applicationContext = new ApplicationContextRunner() + .withConfiguration(AutoConfigurations.of(StdioTransportAutoConfiguration.class)); + + @Test + void stdioTransportsNotPresentIfStdioDisabled() { + this.applicationContext.withPropertyValues("spring.ai.mcp.client.enabled", "false") + .run(context -> assertThat(context.containsBean("stdioTransports")).isFalse()); + } + + @Test + void noTransportsCreatedWithEmptyConnections() { + this.applicationContext.run(context -> { + List transports = context.getBean("stdioTransports", List.class); + assertThat(transports).isEmpty(); + }); + } + + @Test + void singleConnectionCreateOneTransport() { + this.applicationContext + .withPropertyValues("spring.ai.mcp.client.stdio.connections.server1.command=java", + "spring.ai.mcp.client.stdio.connections.server1.args[0]=--server.port=8080", + "spring.ai.mcp.client.stdio.connections.server1.args[1]=-jar", + "spring.ai.mcp.client.stdio.connections.server1.args[2]=server1.jar", + "spring.ai.mcp.client.stdio.connections.server1.env.API_KEY=sk-abc123") + .run(context -> { + List transports = context.getBean("stdioTransports", List.class); + assertThat(transports).hasSize(1); + assertThat(transports.get(0).name()).isEqualTo("server1"); + assertThat(transports.get(0).transport()).isInstanceOf(StdioClientTransport.class); + }); + } + + @Test + void multipleConnectionsCreateMultipleTransports() { + this.applicationContext + .withPropertyValues("spring.ai.mcp.client.stdio.connections.server1.command=java", + "spring.ai.mcp.client.stdio.connections.server1.args[0]=--server.port=8080", + "spring.ai.mcp.client.stdio.connections.server1.args[1]=-jar", + "spring.ai.mcp.client.stdio.connections.server1.args[2]=server1.jar", + "spring.ai.mcp.client.stdio.connections.server2.command=python", + "spring.ai.mcp.client.stdio.connections.server2.args[0]=server2.py") + .run(context -> { + List transports = context.getBean("stdioTransports", List.class); + assertThat(transports).hasSize(2); + assertThat(transports).extracting("name").containsExactlyInAnyOrder("server1", "server2"); + assertThat(transports).extracting("transport") + .allMatch(transport -> transport instanceof StdioClientTransport); + }); + } + + @Test + void serversConfigurationCreateMultipleTransports() { + this.applicationContext + .withPropertyValues("spring.ai.mcp.client.stdio.serversConfiguration=classpath:test-mcp-servers.json") + .run(context -> { + List transports = context.getBean("stdioTransports", List.class); + assertThat(transports).hasSize(2); + assertThat(transports).extracting("name").containsExactlyInAnyOrder("server1", "server2"); + assertThat(transports).extracting("transport") + .allMatch(transport -> transport instanceof StdioClientTransport); + }); + } + + @Test + void customObjectMapperIsUsed() { + this.applicationContext + .withPropertyValues("spring.ai.mcp.client.stdio.connections.server1.command=java", + "spring.ai.mcp.client.stdio.connections.server1.args[0]=--server.port=8080", + "spring.ai.mcp.client.stdio.connections.server1.args[1]=-jar", + "spring.ai.mcp.client.stdio.connections.server1.args[2]=server1.jar", + "spring.ai.mcp.client.stdio.connections.server1.env.API_KEY=sk-abc123") + .withUserConfiguration(CustomObjectMapperConfiguration.class) + .run(context -> { + assertThat(context.getBean(ObjectMapper.class)).isNotNull(); + List transports = context.getBean("stdioTransports", List.class); + assertThat(transports).hasSize(1); + assertThat(transports.get(0).name()).isEqualTo("server1"); + assertThat(transports.get(0).transport()).isInstanceOf(StdioClientTransport.class); + Field privateField = ReflectionUtils.findField(StdioClientTransport.class, "jsonMapper"); + ReflectionUtils.makeAccessible(privateField); + assertThat(privateField.get(transports.get(0).transport())).isNotNull(); + }); + } + + @Test + void newObjectMapperIsUsed() { + this.applicationContext + .withPropertyValues("spring.ai.mcp.client.stdio.connections.server1.command=java", + "spring.ai.mcp.client.stdio.connections.server1.args[0]=--server.port=8080", + "spring.ai.mcp.client.stdio.connections.server1.args[1]=-jar", + "spring.ai.mcp.client.stdio.connections.server1.args[2]=server1.jar", + "spring.ai.mcp.client.stdio.connections.server1.env.API_KEY=sk-abc123") + .run(context -> { + assertThatThrownBy(() -> context.getBean(ObjectMapper.class)) + .isInstanceOf(NoSuchBeanDefinitionException.class); + List transports = context.getBean("stdioTransports", List.class); + assertThat(transports).hasSize(1); + assertThat(transports.get(0).name()).isEqualTo("server1"); + assertThat(transports.get(0).transport()).isInstanceOf(StdioClientTransport.class); + Field privateField = ReflectionUtils.findField(StdioClientTransport.class, "jsonMapper"); + ReflectionUtils.makeAccessible(privateField); + assertThat(privateField.get(transports.get(0).transport())).isNotNull(); + }); + } + + @Configuration + static class CustomObjectMapperConfiguration { + + @Bean + ObjectMapper objectMapper() { + return new ObjectMapper(); + } + + } + +} diff --git a/auto-configurations/mcp/spring-ai-autoconfigure-mcp-client-common/src/test/java/org/springframework/ai/mcp/client/common/autoconfigure/properties/McpStdioClientPropertiesTests.java b/auto-configurations/mcp/spring-ai-autoconfigure-mcp-client-common/src/test/java/org/springframework/ai/mcp/client/common/autoconfigure/properties/McpStdioClientPropertiesTests.java new file mode 100644 index 00000000000..1b173641466 --- /dev/null +++ b/auto-configurations/mcp/spring-ai-autoconfigure-mcp-client-common/src/test/java/org/springframework/ai/mcp/client/common/autoconfigure/properties/McpStdioClientPropertiesTests.java @@ -0,0 +1,232 @@ +/* + * Copyright 2025-2025 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.ai.mcp.client.common.autoconfigure.properties; + +import java.util.List; +import java.util.Map; + +import io.modelcontextprotocol.client.transport.ServerParameters; +import org.assertj.core.api.InstanceOfAssertFactories; +import org.junit.jupiter.api.Test; + +import org.springframework.boot.context.properties.EnableConfigurationProperties; +import org.springframework.boot.test.context.runner.ApplicationContextRunner; +import org.springframework.context.annotation.Configuration; + +import static org.assertj.core.api.Assertions.assertThat; + +/** + * Tests for {@link McpStdioClientProperties}. + * + * @author guan xu + */ +class McpStdioClientPropertiesTests { + + private final ApplicationContextRunner contextRunner = new ApplicationContextRunner() + .withUserConfiguration(TestConfiguration.class); + + @Test + void configPrefixConstant() { + assertThat(McpStdioClientProperties.CONFIG_PREFIX).isEqualTo("spring.ai.mcp.client.stdio"); + } + + @Test + void defaultValues() { + this.contextRunner.run(context -> { + McpStdioClientProperties properties = context.getBean(McpStdioClientProperties.class); + assertThat(properties.getServersConfiguration()).isNull(); + assertThat(properties.getConnections()).isNotNull(); + assertThat(properties.getConnections()).isEmpty(); + }); + } + + @Test + void singleConnection() { + this.contextRunner + .withPropertyValues("spring.ai.mcp.client.stdio.connections.server1.command=java", + "spring.ai.mcp.client.stdio.connections.server1.args[0]=--server.port=8080", + "spring.ai.mcp.client.stdio.connections.server1.args[1]=-jar", + "spring.ai.mcp.client.stdio.connections.server1.args[2]=server1.jar", + "spring.ai.mcp.client.stdio.connections.server1.env.API_KEY=sk-abc123") + .run(context -> { + McpStdioClientProperties properties = context.getBean(McpStdioClientProperties.class); + assertThat(properties.getConnections()).hasSize(1); + assertThat(properties.getConnections()).containsKey("server1"); + assertThat(properties.getConnections().get("server1")) + .isInstanceOf(McpStdioClientProperties.Parameters.class); + assertThat(properties.getConnections().get("server1").command()).isEqualTo("java"); + assertThat(properties.getConnections().get("server1").args()).isInstanceOf(List.class) + .asInstanceOf(InstanceOfAssertFactories.LIST) + .contains("--server.port=8080", "-jar", "server1.jar"); + assertThat(properties.getConnections().get("server1").env()).isInstanceOf(Map.class) + .asInstanceOf(InstanceOfAssertFactories.MAP) + .containsEntry("API_KEY", "sk-abc123"); + }); + } + + @Test + void multipleConnections() { + this.contextRunner + .withPropertyValues("spring.ai.mcp.client.stdio.connections.server1.command=java", + "spring.ai.mcp.client.stdio.connections.server1.args[0]=--server.port=8080", + "spring.ai.mcp.client.stdio.connections.server1.args[1]=-jar", + "spring.ai.mcp.client.stdio.connections.server1.args[2]=server1.jar", + "spring.ai.mcp.client.stdio.connections.server1.env.API_KEY=sk-abc123", + "spring.ai.mcp.client.stdio.connections.server2.command=python", + "spring.ai.mcp.client.stdio.connections.server2.args[0]=server2.py") + .run(context -> { + McpStdioClientProperties properties = context.getBean(McpStdioClientProperties.class); + assertThat(properties.getConnections()).hasSize(2); + assertThat(properties.getConnections()).containsKeys("server1", "server2"); + assertThat(properties.getConnections().get("server1")) + .isInstanceOf(McpStdioClientProperties.Parameters.class); + assertThat(properties.getConnections().get("server1").command()).isEqualTo("java"); + assertThat(properties.getConnections().get("server1").args()).isInstanceOf(List.class) + .asInstanceOf(InstanceOfAssertFactories.LIST) + .contains("--server.port=8080", "-jar", "server1.jar"); + assertThat(properties.getConnections().get("server1").env()).isInstanceOf(Map.class) + .containsEntry("API_KEY", "sk-abc123"); + assertThat(properties.getConnections().get("server2")) + .isInstanceOf(McpStdioClientProperties.Parameters.class); + assertThat(properties.getConnections().get("server2").command()).isEqualTo("python"); + assertThat(properties.getConnections().get("server2").args()).isInstanceOf(List.class) + .asInstanceOf(InstanceOfAssertFactories.LIST) + .contains("server2.py"); + assertThat(properties.getConnections().get("server2").env()).isNull(); + }); + } + + @Test + void serversConfigurationToServerParameters() { + this.contextRunner + .withPropertyValues("spring.ai.mcp.client.stdio.serversConfiguration=classpath:test-mcp-servers.json") + .run(context -> { + McpStdioClientProperties properties = context.getBean(McpStdioClientProperties.class); + assertThat(properties.toServerParameters()).hasSize(2); + assertThat(properties.toServerParameters()).containsKeys("server1", "server2"); + assertThat(properties.toServerParameters().get("server1")).isInstanceOf(ServerParameters.class); + assertThat(properties.toServerParameters().get("server1").getCommand()).isEqualTo("java"); + assertThat(properties.toServerParameters().get("server1").getArgs()).isInstanceOf(List.class) + .asInstanceOf(InstanceOfAssertFactories.LIST) + .contains("--server.port=8080", "-jar", "server1.jar"); + assertThat(properties.toServerParameters().get("server1").getEnv()).isInstanceOf(Map.class) + .asInstanceOf(InstanceOfAssertFactories.MAP) + .containsEntry("API_KEY", "sk-abc123"); + assertThat(properties.toServerParameters().get("server2")).isInstanceOf(ServerParameters.class); + assertThat(properties.toServerParameters().get("server2").getCommand()).isEqualTo("python"); + assertThat(properties.toServerParameters().get("server2").getArgs()).isInstanceOf(List.class) + .asInstanceOf(InstanceOfAssertFactories.LIST) + .contains("server2.py"); + assertThat(properties.toServerParameters().get("server2").getEnv()).isInstanceOf(Map.class) + .asInstanceOf(InstanceOfAssertFactories.MAP) + .doesNotContainEntry("API_KEY", "sk-abc123"); + }); + } + + @Test + void serversConfigurationAndConnectionsToServerParameters() { + this.contextRunner + .withPropertyValues("spring.ai.mcp.client.stdio.serversConfiguration=classpath:test-mcp-servers.json") + .withPropertyValues("spring.ai.mcp.client.stdio.connections.server3.command=python", + "spring.ai.mcp.client.stdio.connections.server3.args[0]=server3.py") + .run(context -> { + McpStdioClientProperties properties = context.getBean(McpStdioClientProperties.class); + assertThat(properties.toServerParameters()).hasSize(3); + assertThat(properties.toServerParameters()).containsKeys("server1", "server2", "server3"); + assertThat(properties.toServerParameters().get("server1")).isInstanceOf(ServerParameters.class); + assertThat(properties.toServerParameters().get("server1").getCommand()).isEqualTo("java"); + assertThat(properties.toServerParameters().get("server1").getArgs()).isInstanceOf(List.class) + .asInstanceOf(InstanceOfAssertFactories.LIST) + .contains("--server.port=8080", "-jar", "server1.jar"); + assertThat(properties.toServerParameters().get("server1").getEnv()).isInstanceOf(Map.class) + .asInstanceOf(InstanceOfAssertFactories.MAP) + .containsEntry("API_KEY", "sk-abc123"); + assertThat(properties.toServerParameters().get("server2")).isInstanceOf(ServerParameters.class); + assertThat(properties.toServerParameters().get("server2").getCommand()).isEqualTo("python"); + assertThat(properties.toServerParameters().get("server2").getArgs()).isInstanceOf(List.class) + .asInstanceOf(InstanceOfAssertFactories.LIST) + .contains("server2.py"); + assertThat(properties.toServerParameters().get("server2").getEnv()).isInstanceOf(Map.class) + .asInstanceOf(InstanceOfAssertFactories.MAP) + .doesNotContainEntry("API_KEY", "sk-abc123"); + assertThat(properties.toServerParameters().get("server3")).isInstanceOf(ServerParameters.class); + assertThat(properties.toServerParameters().get("server3").getCommand()).isEqualTo("python"); + assertThat(properties.toServerParameters().get("server3").getArgs()).isInstanceOf(List.class) + .asInstanceOf(InstanceOfAssertFactories.LIST) + .contains("server3.py"); + assertThat(properties.toServerParameters().get("server3").getEnv()).isInstanceOf(Map.class) + .asInstanceOf(InstanceOfAssertFactories.MAP) + .doesNotContainEntry("API_KEY", "sk-abc123"); + }); + } + + @Test + void connectionsReplaceServersConfigurationToServerParameters() { + this.contextRunner + .withPropertyValues("spring.ai.mcp.client.stdio.serversConfiguration=classpath:test-mcp-servers.json") + .withPropertyValues("spring.ai.mcp.client.stdio.connections.server1.command=python", + "spring.ai.mcp.client.stdio.connections.server1.args[0]=server1.py") + .run(context -> { + McpStdioClientProperties properties = context.getBean(McpStdioClientProperties.class); + assertThat(properties.toServerParameters()).hasSize(2); + assertThat(properties.toServerParameters()).containsKeys("server1", "server2"); + assertThat(properties.toServerParameters().get("server1")).isInstanceOf(ServerParameters.class); + assertThat(properties.toServerParameters().get("server1").getCommand()).isEqualTo("python"); + assertThat(properties.toServerParameters().get("server1").getArgs()).isInstanceOf(List.class) + .asInstanceOf(InstanceOfAssertFactories.LIST) + .doesNotContain("--server.port=8080", "-jar", "server1.jar"); + assertThat(properties.toServerParameters().get("server1").getArgs()).isInstanceOf(List.class) + .asInstanceOf(InstanceOfAssertFactories.LIST) + .contains("server1.py"); + }); + } + + @Test + void connectionsReplaceConnectionsToServerParameters() { + this.contextRunner + .withPropertyValues("spring.ai.mcp.client.stdio.connections.server1.command=java", + "spring.ai.mcp.client.stdio.connections.server1.args[0]=--server.port=8080", + "spring.ai.mcp.client.stdio.connections.server1.args[1]=-jar", + "spring.ai.mcp.client.stdio.connections.server1.args[2]=server1.jar", + "spring.ai.mcp.client.stdio.connections.server1.env.API_KEY=sk-abc123", + "spring.ai.mcp.client.stdio.connections.server1.command=python", + "spring.ai.mcp.client.stdio.connections.server1.args[0]=server1.py") + .run(context -> { + McpStdioClientProperties properties = context.getBean(McpStdioClientProperties.class); + assertThat(properties.toServerParameters()).hasSize(1); + assertThat(properties.toServerParameters()).containsKeys("server1"); + assertThat(properties.toServerParameters().get("server1")).isInstanceOf(ServerParameters.class); + assertThat(properties.toServerParameters().get("server1").getCommand()).isEqualTo("python"); + assertThat(properties.toServerParameters().get("server1").getArgs()).isInstanceOf(List.class) + .asInstanceOf(InstanceOfAssertFactories.LIST) + .doesNotContain("--server.port=8080"); + assertThat(properties.toServerParameters().get("server1").getArgs()).isInstanceOf(List.class) + .asInstanceOf(InstanceOfAssertFactories.LIST) + .contains("server1.py", "-jar", "server1.jar"); + assertThat(properties.toServerParameters().get("server1").getEnv()).isInstanceOf(Map.class) + .asInstanceOf(InstanceOfAssertFactories.MAP) + .containsEntry("API_KEY", "sk-abc123"); + }); + } + + @Configuration + @EnableConfigurationProperties(McpStdioClientProperties.class) + static class TestConfiguration { + + } + +} diff --git a/auto-configurations/mcp/spring-ai-autoconfigure-mcp-client-common/src/test/resources/test-mcp-servers.json b/auto-configurations/mcp/spring-ai-autoconfigure-mcp-client-common/src/test/resources/test-mcp-servers.json new file mode 100644 index 00000000000..9152d3b2873 --- /dev/null +++ b/auto-configurations/mcp/spring-ai-autoconfigure-mcp-client-common/src/test/resources/test-mcp-servers.json @@ -0,0 +1,21 @@ +{ + "mcpServers": { + "server1": { + "command": "java", + "args": [ + "--server.port=8080", + "-jar", + "server1.jar" + ], + "env": { + "API_KEY": "sk-abc123" + } + }, + "server2": { + "command": "python", + "args": [ + "server2.py" + ] + } + } +} \ No newline at end of file