Skip to content

Commit 8f1b487

Browse files
committed
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 <[email protected]>
1 parent c319792 commit 8f1b487

File tree

5 files changed

+492
-2
lines changed

5 files changed

+492
-2
lines changed

auto-configurations/mcp/spring-ai-autoconfigure-mcp-client-common/src/main/java/org/springframework/ai/mcp/client/common/autoconfigure/StdioTransportAutoConfiguration.java

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,7 @@
2828

2929
import org.springframework.ai.mcp.client.common.autoconfigure.properties.McpClientCommonProperties;
3030
import org.springframework.ai.mcp.client.common.autoconfigure.properties.McpStdioClientProperties;
31+
import org.springframework.beans.factory.ObjectProvider;
3132
import org.springframework.boot.autoconfigure.AutoConfiguration;
3233
import org.springframework.boot.autoconfigure.condition.ConditionalOnClass;
3334
import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
@@ -73,13 +74,16 @@ public class StdioTransportAutoConfiguration {
7374
* @return list of named MCP transports
7475
*/
7576
@Bean
76-
public List<NamedClientMcpTransport> stdioTransports(McpStdioClientProperties stdioProperties) {
77+
public List<NamedClientMcpTransport> stdioTransports(McpStdioClientProperties stdioProperties,
78+
ObjectProvider<ObjectMapper> objectMapperProvider) {
79+
80+
ObjectMapper objectMapper = objectMapperProvider.getIfAvailable(ObjectMapper::new);
7781

7882
List<NamedClientMcpTransport> stdioTransports = new ArrayList<>();
7983

8084
for (Map.Entry<String, ServerParameters> serverParameters : stdioProperties.toServerParameters().entrySet()) {
8185
var transport = new StdioClientTransport(serverParameters.getValue(),
82-
new JacksonMcpJsonMapper(new ObjectMapper()));
86+
new JacksonMcpJsonMapper(objectMapper));
8387
stdioTransports.add(new NamedClientMcpTransport(serverParameters.getKey(), transport));
8488

8589
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,73 @@
1+
/*
2+
* Copyright 2025-2025 the original author or authors.
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* https://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
17+
package org.springframework.ai.mcp.client.common.autoconfigure;
18+
19+
import java.util.List;
20+
import java.util.Map;
21+
22+
import io.modelcontextprotocol.client.McpSyncClient;
23+
import io.modelcontextprotocol.spec.McpSchema;
24+
import org.junit.jupiter.api.Test;
25+
import org.junit.jupiter.api.Timeout;
26+
27+
import org.springframework.boot.autoconfigure.AutoConfigurations;
28+
import org.springframework.boot.test.context.runner.ApplicationContextRunner;
29+
30+
import static org.assertj.core.api.Assertions.assertThat;
31+
32+
/**
33+
* Integration tests for {@link StdioTransportAutoConfiguration} with H2.
34+
*
35+
* @author guan xu
36+
*/
37+
@Timeout(15)
38+
@SuppressWarnings("unchecked")
39+
public class StdioTransportAutoConfigurationIT {
40+
41+
private final ApplicationContextRunner contextRunner = new ApplicationContextRunner().withConfiguration(
42+
AutoConfigurations.of(McpClientAutoConfiguration.class, StdioTransportAutoConfiguration.class));
43+
44+
@Test
45+
void connectionsStdioTest() {
46+
this.contextRunner
47+
.withPropertyValues("spring.ai.mcp.client.stdio.connections.server1.command=npx",
48+
"spring.ai.mcp.client.stdio.connections.server1.args[0]=-y",
49+
"spring.ai.mcp.client.stdio.connections.server1.args[1]=@modelcontextprotocol/server-everything")
50+
.run(context -> {
51+
List<McpSyncClient> mcpClients = (List<McpSyncClient>) context.getBean("mcpSyncClients");
52+
assertThat(mcpClients).isNotNull();
53+
assertThat(mcpClients).hasSize(1);
54+
55+
McpSyncClient mcpClient = mcpClients.get(0);
56+
mcpClient.ping();
57+
58+
McpSchema.ListToolsResult toolsResult = mcpClient.listTools();
59+
assertThat(toolsResult).isNotNull();
60+
assertThat(toolsResult.tools()).isNotEmpty();
61+
62+
McpSchema.CallToolResult result = mcpClient.callTool(McpSchema.CallToolRequest.builder()
63+
.name("add")
64+
.arguments(Map.of("operation", "add", "a", 1, "b", 2))
65+
.build());
66+
assertThat(result).isNotNull();
67+
assertThat(result.content()).isNotEmpty();
68+
69+
mcpClient.closeGracefully();
70+
});
71+
}
72+
73+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,160 @@
1+
/*
2+
* Copyright 2025-2025 the original author or authors.
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* https://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
17+
package org.springframework.ai.mcp.client.common.autoconfigure;
18+
19+
import java.lang.reflect.Field;
20+
import java.util.List;
21+
22+
import com.fasterxml.jackson.databind.ObjectMapper;
23+
import io.modelcontextprotocol.client.transport.StdioClientTransport;
24+
import org.junit.jupiter.api.Test;
25+
26+
import org.springframework.beans.factory.NoSuchBeanDefinitionException;
27+
import org.springframework.boot.autoconfigure.AutoConfigurations;
28+
import org.springframework.boot.test.context.runner.ApplicationContextRunner;
29+
import org.springframework.context.annotation.Bean;
30+
import org.springframework.context.annotation.Configuration;
31+
import org.springframework.util.ReflectionUtils;
32+
33+
import static org.assertj.core.api.Assertions.assertThat;
34+
import static org.assertj.core.api.Assertions.assertThatThrownBy;
35+
36+
/**
37+
* Tests for {@link StdioTransportAutoConfiguration}.
38+
*
39+
* @author guan xu
40+
*/
41+
@SuppressWarnings("unchecked")
42+
public class StdioTransportAutoConfigurationTests {
43+
44+
private final ApplicationContextRunner applicationContext = new ApplicationContextRunner()
45+
.withConfiguration(AutoConfigurations.of(StdioTransportAutoConfiguration.class));
46+
47+
@Test
48+
void stdioTransportsNotPresentIfStdioDisabled() {
49+
this.applicationContext.withPropertyValues("spring.ai.mcp.client.enabled", "false")
50+
.run(context -> assertThat(context.containsBean("stdioTransports")).isFalse());
51+
}
52+
53+
@Test
54+
void noTransportsCreatedWithEmptyConnections() {
55+
this.applicationContext.run(context -> {
56+
List<NamedClientMcpTransport> transports = context.getBean("stdioTransports", List.class);
57+
assertThat(transports).isEmpty();
58+
});
59+
}
60+
61+
@Test
62+
void singleConnectionCreateOneTransport() {
63+
this.applicationContext
64+
.withPropertyValues("spring.ai.mcp.client.stdio.connections.server1.command=java",
65+
"spring.ai.mcp.client.stdio.connections.server1.args[0]=--server.port=8080",
66+
"spring.ai.mcp.client.stdio.connections.server1.args[1]=-jar",
67+
"spring.ai.mcp.client.stdio.connections.server1.args[2]=server1.jar",
68+
"spring.ai.mcp.client.stdio.connections.server1.env.API_KEY=sk-abc123")
69+
.run(context -> {
70+
List<NamedClientMcpTransport> transports = context.getBean("stdioTransports", List.class);
71+
assertThat(transports).hasSize(1);
72+
assertThat(transports.get(0).name()).isEqualTo("server1");
73+
assertThat(transports.get(0).transport()).isInstanceOf(StdioClientTransport.class);
74+
});
75+
}
76+
77+
@Test
78+
void multipleConnectionsCreateMultipleTransports() {
79+
this.applicationContext
80+
.withPropertyValues("spring.ai.mcp.client.stdio.connections.server1.command=java",
81+
"spring.ai.mcp.client.stdio.connections.server1.args[0]=--server.port=8080",
82+
"spring.ai.mcp.client.stdio.connections.server1.args[1]=-jar",
83+
"spring.ai.mcp.client.stdio.connections.server1.args[2]=server1.jar",
84+
"spring.ai.mcp.client.stdio.connections.server2.command=python",
85+
"spring.ai.mcp.client.stdio.connections.server2.args[0]=server2.py")
86+
.run(context -> {
87+
List<NamedClientMcpTransport> transports = context.getBean("stdioTransports", List.class);
88+
assertThat(transports).hasSize(2);
89+
assertThat(transports).extracting("name").containsExactlyInAnyOrder("server1", "server2");
90+
assertThat(transports).extracting("transport")
91+
.allMatch(transport -> transport instanceof StdioClientTransport);
92+
});
93+
}
94+
95+
@Test
96+
void serversConfigurationCreateMultipleTransports() {
97+
this.applicationContext
98+
.withPropertyValues("spring.ai.mcp.client.stdio.serversConfiguration=classpath:test-mcp-servers.json")
99+
.run(context -> {
100+
List<NamedClientMcpTransport> transports = context.getBean("stdioTransports", List.class);
101+
assertThat(transports).hasSize(2);
102+
assertThat(transports).extracting("name").containsExactlyInAnyOrder("server1", "server2");
103+
assertThat(transports).extracting("transport")
104+
.allMatch(transport -> transport instanceof StdioClientTransport);
105+
});
106+
}
107+
108+
@Test
109+
void customObjectMapperIsUsed() {
110+
this.applicationContext
111+
.withPropertyValues("spring.ai.mcp.client.stdio.connections.server1.command=java",
112+
"spring.ai.mcp.client.stdio.connections.server1.args[0]=--server.port=8080",
113+
"spring.ai.mcp.client.stdio.connections.server1.args[1]=-jar",
114+
"spring.ai.mcp.client.stdio.connections.server1.args[2]=server1.jar",
115+
"spring.ai.mcp.client.stdio.connections.server1.env.API_KEY=sk-abc123")
116+
.withUserConfiguration(CustomObjectMapperConfiguration.class)
117+
.run(context -> {
118+
assertThat(context.getBean(ObjectMapper.class)).isNotNull();
119+
List<NamedClientMcpTransport> transports = context.getBean("stdioTransports", List.class);
120+
assertThat(transports).hasSize(1);
121+
assertThat(transports.get(0).name()).isEqualTo("server1");
122+
assertThat(transports.get(0).transport()).isInstanceOf(StdioClientTransport.class);
123+
Field privateField = ReflectionUtils.findField(StdioClientTransport.class, "jsonMapper");
124+
ReflectionUtils.makeAccessible(privateField);
125+
assertThat(privateField.get(transports.get(0).transport())).isNotNull();
126+
});
127+
}
128+
129+
@Test
130+
void newObjectMapperIsUsed() {
131+
this.applicationContext
132+
.withPropertyValues("spring.ai.mcp.client.stdio.connections.server1.command=java",
133+
"spring.ai.mcp.client.stdio.connections.server1.args[0]=--server.port=8080",
134+
"spring.ai.mcp.client.stdio.connections.server1.args[1]=-jar",
135+
"spring.ai.mcp.client.stdio.connections.server1.args[2]=server1.jar",
136+
"spring.ai.mcp.client.stdio.connections.server1.env.API_KEY=sk-abc123")
137+
.run(context -> {
138+
assertThatThrownBy(() -> context.getBean(ObjectMapper.class))
139+
.isInstanceOf(NoSuchBeanDefinitionException.class);
140+
List<NamedClientMcpTransport> transports = context.getBean("stdioTransports", List.class);
141+
assertThat(transports).hasSize(1);
142+
assertThat(transports.get(0).name()).isEqualTo("server1");
143+
assertThat(transports.get(0).transport()).isInstanceOf(StdioClientTransport.class);
144+
Field privateField = ReflectionUtils.findField(StdioClientTransport.class, "jsonMapper");
145+
ReflectionUtils.makeAccessible(privateField);
146+
assertThat(privateField.get(transports.get(0).transport())).isNotNull();
147+
});
148+
}
149+
150+
@Configuration
151+
static class CustomObjectMapperConfiguration {
152+
153+
@Bean
154+
ObjectMapper objectMapper() {
155+
return new ObjectMapper();
156+
}
157+
158+
}
159+
160+
}

0 commit comments

Comments
 (0)