Skip to content

Commit e0137c2

Browse files
authored
Add StreamableHttpHttpClientTransportAutoConfigurationTests (#4147)
Signed-off-by: Yanming Zhou <[email protected]>
1 parent deffe80 commit e0137c2

File tree

1 file changed

+180
-0
lines changed

1 file changed

+180
-0
lines changed
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,180 @@
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.autoconfigure;
18+
19+
import com.fasterxml.jackson.databind.ObjectMapper;
20+
import io.modelcontextprotocol.client.transport.HttpClientStreamableHttpTransport;
21+
import org.junit.jupiter.api.Test;
22+
import org.springframework.ai.mcp.client.common.autoconfigure.NamedClientMcpTransport;
23+
import org.springframework.ai.mcp.client.httpclient.autoconfigure.StreamableHttpHttpClientTransportAutoConfiguration;
24+
import org.springframework.boot.autoconfigure.AutoConfigurations;
25+
import org.springframework.boot.test.context.runner.ApplicationContextRunner;
26+
import org.springframework.context.annotation.Bean;
27+
import org.springframework.context.annotation.Configuration;
28+
import org.springframework.util.ReflectionUtils;
29+
30+
import java.lang.reflect.Field;
31+
import java.util.List;
32+
33+
import static org.assertj.core.api.Assertions.assertThat;
34+
35+
/**
36+
* Tests for {@link StreamableHttpHttpClientTransportAutoConfiguration}.
37+
*
38+
* @author Yanming Zhou
39+
*/
40+
public class StreamableHttpHttpClientTransportAutoConfigurationTests {
41+
42+
private final ApplicationContextRunner applicationContext = new ApplicationContextRunner()
43+
.withConfiguration(AutoConfigurations.of(StreamableHttpHttpClientTransportAutoConfiguration.class));
44+
45+
@Test
46+
void mcpHttpClientTransportsNotPresentIfMcpClientDisabled() {
47+
this.applicationContext.withPropertyValues("spring.ai.mcp.client.enabled", "false")
48+
.run(context -> assertThat(context.containsBean("streamableHttpHttpClientTransports")).isFalse());
49+
}
50+
51+
@Test
52+
void noTransportsCreatedWithEmptyConnections() {
53+
this.applicationContext.run(context -> {
54+
List<NamedClientMcpTransport> transports = context.getBean("streamableHttpHttpClientTransports",
55+
List.class);
56+
assertThat(transports).isEmpty();
57+
});
58+
}
59+
60+
@Test
61+
void singleConnectionCreatesOneTransport() {
62+
this.applicationContext
63+
.withPropertyValues("spring.ai.mcp.client.streamable-http.connections.server1.url=http://localhost:8080")
64+
.run(context -> {
65+
List<NamedClientMcpTransport> transports = context.getBean("streamableHttpHttpClientTransports",
66+
List.class);
67+
assertThat(transports).hasSize(1);
68+
assertThat(transports.get(0).name()).isEqualTo("server1");
69+
assertThat(transports.get(0).transport()).isInstanceOf(HttpClientStreamableHttpTransport.class);
70+
});
71+
}
72+
73+
@Test
74+
void multipleConnectionsCreateMultipleTransports() {
75+
this.applicationContext
76+
.withPropertyValues("spring.ai.mcp.client.streamable-http.connections.server1.url=http://localhost:8080",
77+
"spring.ai.mcp.client.streamable-http.connections.server2.url=http://otherserver:8081")
78+
.run(context -> {
79+
List<NamedClientMcpTransport> transports = context.getBean("streamableHttpHttpClientTransports",
80+
List.class);
81+
assertThat(transports).hasSize(2);
82+
assertThat(transports).extracting("name").containsExactlyInAnyOrder("server1", "server2");
83+
assertThat(transports).extracting("transport")
84+
.allMatch(transport -> transport instanceof HttpClientStreamableHttpTransport);
85+
for (NamedClientMcpTransport transport : transports) {
86+
assertThat(transport.transport()).isInstanceOf(HttpClientStreamableHttpTransport.class);
87+
assertThat(getStreamableHttpEndpoint((HttpClientStreamableHttpTransport) transport.transport()))
88+
.isEqualTo("/mcp");
89+
}
90+
});
91+
}
92+
93+
@Test
94+
void customEndpointIsRespected() {
95+
this.applicationContext
96+
.withPropertyValues("spring.ai.mcp.client.streamable-http.connections.server1.url=http://localhost:8080",
97+
"spring.ai.mcp.client.streamable-http.connections.server1.endpoint=/custom-mcp")
98+
.run(context -> {
99+
List<NamedClientMcpTransport> transports = context.getBean("streamableHttpHttpClientTransports",
100+
List.class);
101+
assertThat(transports).hasSize(1);
102+
assertThat(transports.get(0).name()).isEqualTo("server1");
103+
assertThat(transports.get(0).transport()).isInstanceOf(HttpClientStreamableHttpTransport.class);
104+
105+
assertThat(getStreamableHttpEndpoint((HttpClientStreamableHttpTransport) transports.get(0).transport()))
106+
.isEqualTo("/custom-mcp");
107+
});
108+
}
109+
110+
@Test
111+
void customObjectMapperIsUsed() {
112+
this.applicationContext.withUserConfiguration(CustomObjectMapperConfiguration.class)
113+
.withPropertyValues("spring.ai.mcp.client.streamable-http.connections.server1.url=http://localhost:8080")
114+
.run(context -> {
115+
assertThat(context.getBean(ObjectMapper.class)).isNotNull();
116+
List<NamedClientMcpTransport> transports = context.getBean("streamableHttpHttpClientTransports",
117+
List.class);
118+
assertThat(transports).hasSize(1);
119+
});
120+
}
121+
122+
@Test
123+
void defaultEndpointIsUsedWhenNotSpecified() {
124+
this.applicationContext
125+
.withPropertyValues("spring.ai.mcp.client.streamable-http.connections.server1.url=http://localhost:8080")
126+
.run(context -> {
127+
List<NamedClientMcpTransport> transports = context.getBean("streamableHttpHttpClientTransports",
128+
List.class);
129+
assertThat(transports).hasSize(1);
130+
assertThat(transports.get(0).name()).isEqualTo("server1");
131+
assertThat(transports.get(0).transport()).isInstanceOf(HttpClientStreamableHttpTransport.class);
132+
// Default Streamable HTTP endpoint is "/mcp" as specified in the
133+
// configuration class
134+
});
135+
}
136+
137+
@Test
138+
void mixedConnectionsWithAndWithoutCustomEndpoint() {
139+
this.applicationContext
140+
.withPropertyValues("spring.ai.mcp.client.streamable-http.connections.server1.url=http://localhost:8080",
141+
"spring.ai.mcp.client.streamable-http.connections.server1.endpoint=/custom-mcp",
142+
"spring.ai.mcp.client.streamable-http.connections.server2.url=http://otherserver:8081")
143+
.run(context -> {
144+
List<NamedClientMcpTransport> transports = context.getBean("streamableHttpHttpClientTransports",
145+
List.class);
146+
assertThat(transports).hasSize(2);
147+
assertThat(transports).extracting("name").containsExactlyInAnyOrder("server1", "server2");
148+
assertThat(transports).extracting("transport")
149+
.allMatch(transport -> transport instanceof HttpClientStreamableHttpTransport);
150+
for (NamedClientMcpTransport transport : transports) {
151+
assertThat(transport.transport()).isInstanceOf(HttpClientStreamableHttpTransport.class);
152+
if (transport.name().equals("server1")) {
153+
assertThat(getStreamableHttpEndpoint((HttpClientStreamableHttpTransport) transport.transport()))
154+
.isEqualTo("/custom-mcp");
155+
}
156+
else {
157+
assertThat(getStreamableHttpEndpoint((HttpClientStreamableHttpTransport) transport.transport()))
158+
.isEqualTo("/mcp");
159+
}
160+
}
161+
});
162+
}
163+
164+
private String getStreamableHttpEndpoint(HttpClientStreamableHttpTransport transport) {
165+
Field privateField = ReflectionUtils.findField(HttpClientStreamableHttpTransport.class, "endpoint");
166+
ReflectionUtils.makeAccessible(privateField);
167+
return (String) ReflectionUtils.getField(privateField, transport);
168+
}
169+
170+
@Configuration
171+
static class CustomObjectMapperConfiguration {
172+
173+
@Bean
174+
ObjectMapper objectMapper() {
175+
return new ObjectMapper();
176+
}
177+
178+
}
179+
180+
}

0 commit comments

Comments
 (0)