feat(mcp): Add configurable SSE endpoint support for MCP client transports
Enhance MCP client transports with configurable SSE endpoint support: - Add sseEndpoint parameter to SseParameters record - Update HttpClientSseClientTransport to use builder pattern with sseEndpoint support - Update WebFluxSseClientTransport to use builder pattern with sseEndpoint support - Set default SSE endpoint to /sse when not explicitly configured - Add tests for MCP client properties - Enhance MCP client transport tests and refactor config Signed-off-by: Christian Tzolov <christian.tzolov@broadcom.com>
This commit is contained in:
@@ -94,8 +94,14 @@ public class SseHttpClientTransportAutoConfiguration {
|
||||
|
||||
for (Map.Entry<String, SseParameters> serverParameters : sseProperties.getConnections().entrySet()) {
|
||||
|
||||
var transport = new HttpClientSseClientTransport(HttpClient.newBuilder(), serverParameters.getValue().url(),
|
||||
objectMapper);
|
||||
String baseUrl = serverParameters.getValue().url();
|
||||
String sseEndpoint = serverParameters.getValue().sseEndpoint() != null
|
||||
? serverParameters.getValue().sseEndpoint() : "/sse";
|
||||
var transport = HttpClientSseClientTransport.builder(baseUrl)
|
||||
.sseEndpoint(sseEndpoint)
|
||||
.clientBuilder(HttpClient.newBuilder())
|
||||
.objectMapper(objectMapper)
|
||||
.build();
|
||||
sseTransports.add(new NamedClientMcpTransport(serverParameters.getKey(), transport));
|
||||
}
|
||||
|
||||
|
||||
@@ -90,7 +90,12 @@ public class SseWebFluxTransportAutoConfiguration {
|
||||
|
||||
for (Map.Entry<String, SseParameters> serverParameters : sseProperties.getConnections().entrySet()) {
|
||||
var webClientBuilder = webClientBuilderTemplate.clone().baseUrl(serverParameters.getValue().url());
|
||||
var transport = new WebFluxSseClientTransport(webClientBuilder, objectMapper);
|
||||
String sseEndpoint = serverParameters.getValue().sseEndpoint() != null
|
||||
? serverParameters.getValue().sseEndpoint() : "/sse";
|
||||
var transport = WebFluxSseClientTransport.builder(webClientBuilder)
|
||||
.sseEndpoint(sseEndpoint)
|
||||
.objectMapper(objectMapper)
|
||||
.build();
|
||||
sseTransports.add(new NamedClientMcpTransport(serverParameters.getKey(), transport));
|
||||
}
|
||||
|
||||
|
||||
@@ -67,8 +67,9 @@ public class McpSseClientProperties {
|
||||
* Parameters for configuring an SSE connection to an MCP server.
|
||||
*
|
||||
* @param url the URL endpoint for SSE communication with the MCP server
|
||||
* @param sseEndpoint the SSE endpoint for the MCP server
|
||||
*/
|
||||
public record SseParameters(String url) {
|
||||
public record SseParameters(String url, String sseEndpoint) {
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -16,14 +16,27 @@
|
||||
|
||||
package org.springframework.ai.mcp.client.autoconfigure;
|
||||
|
||||
import java.lang.reflect.Field;
|
||||
import java.util.List;
|
||||
|
||||
import com.fasterxml.jackson.databind.ObjectMapper;
|
||||
import io.modelcontextprotocol.client.transport.HttpClientSseClientTransport;
|
||||
import org.junit.jupiter.api.Test;
|
||||
|
||||
import org.springframework.boot.autoconfigure.AutoConfigurations;
|
||||
import org.springframework.boot.test.context.FilteredClassLoader;
|
||||
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;
|
||||
|
||||
/**
|
||||
* Tests for {@link SseHttpClientTransportAutoConfiguration}.
|
||||
*
|
||||
* @author Christian Tzolov
|
||||
*/
|
||||
public class SseHttpClientTransportAutoConfigurationTests {
|
||||
|
||||
private final ApplicationContextRunner applicationContext = new ApplicationContextRunner()
|
||||
@@ -31,13 +44,11 @@ public class SseHttpClientTransportAutoConfigurationTests {
|
||||
|
||||
@Test
|
||||
void mcpHttpClientTransportsNotPresentIfMissingWebFluxSseClientTransportPresent() {
|
||||
|
||||
this.applicationContext.run(context -> assertThat(context.containsBean("mcpHttpClientTransports")).isFalse());
|
||||
}
|
||||
|
||||
@Test
|
||||
void mcpHttpClientTransportsPresentIfMissingWebFluxSseClientTransportNotPresent() {
|
||||
|
||||
this.applicationContext
|
||||
.withClassLoader(
|
||||
new FilteredClassLoader("io.modelcontextprotocol.client.transport.WebFluxSseClientTransport"))
|
||||
@@ -46,7 +57,6 @@ public class SseHttpClientTransportAutoConfigurationTests {
|
||||
|
||||
@Test
|
||||
void mcpHttpClientTransportsNotPresentIfMcpClientDisabled() {
|
||||
|
||||
this.applicationContext
|
||||
.withClassLoader(
|
||||
new FilteredClassLoader("io.modelcontextprotocol.client.transport.WebFluxSseClientTransport"))
|
||||
@@ -54,4 +64,140 @@ public class SseHttpClientTransportAutoConfigurationTests {
|
||||
.run(context -> assertThat(context.containsBean("mcpHttpClientTransports")).isFalse());
|
||||
}
|
||||
|
||||
@Test
|
||||
void noTransportsCreatedWithEmptyConnections() {
|
||||
this.applicationContext
|
||||
.withClassLoader(
|
||||
new FilteredClassLoader("io.modelcontextprotocol.client.transport.WebFluxSseClientTransport"))
|
||||
.run(context -> {
|
||||
List<NamedClientMcpTransport> transports = context.getBean("mcpHttpClientTransports", List.class);
|
||||
assertThat(transports).isEmpty();
|
||||
});
|
||||
}
|
||||
|
||||
@Test
|
||||
void singleConnectionCreatesOneTransport() {
|
||||
this.applicationContext
|
||||
.withClassLoader(
|
||||
new FilteredClassLoader("io.modelcontextprotocol.client.transport.WebFluxSseClientTransport"))
|
||||
.withPropertyValues("spring.ai.mcp.client.sse.connections.server1.url=http://localhost:8080")
|
||||
.run(context -> {
|
||||
List<NamedClientMcpTransport> transports = context.getBean("mcpHttpClientTransports", List.class);
|
||||
assertThat(transports).hasSize(1);
|
||||
assertThat(transports.get(0).name()).isEqualTo("server1");
|
||||
assertThat(transports.get(0).transport()).isInstanceOf(HttpClientSseClientTransport.class);
|
||||
});
|
||||
}
|
||||
|
||||
@Test
|
||||
void multipleConnectionsCreateMultipleTransports() {
|
||||
this.applicationContext
|
||||
.withClassLoader(
|
||||
new FilteredClassLoader("io.modelcontextprotocol.client.transport.WebFluxSseClientTransport"))
|
||||
.withPropertyValues("spring.ai.mcp.client.sse.connections.server1.url=http://localhost:8080",
|
||||
"spring.ai.mcp.client.sse.connections.server2.url=http://otherserver:8081")
|
||||
.run(context -> {
|
||||
List<NamedClientMcpTransport> transports = context.getBean("mcpHttpClientTransports", List.class);
|
||||
assertThat(transports).hasSize(2);
|
||||
assertThat(transports).extracting("name").containsExactlyInAnyOrder("server1", "server2");
|
||||
assertThat(transports).extracting("transport")
|
||||
.allMatch(transport -> transport instanceof HttpClientSseClientTransport);
|
||||
for (NamedClientMcpTransport transport : transports) {
|
||||
assertThat(transport.transport()).isInstanceOf(HttpClientSseClientTransport.class);
|
||||
assertThat(getSseEndpoint((HttpClientSseClientTransport) transport.transport())).isEqualTo("/sse");
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
@Test
|
||||
void customSseEndpointIsRespected() {
|
||||
this.applicationContext
|
||||
.withClassLoader(
|
||||
new FilteredClassLoader("io.modelcontextprotocol.client.transport.WebFluxSseClientTransport"))
|
||||
.withPropertyValues("spring.ai.mcp.client.sse.connections.server1.url=http://localhost:8080",
|
||||
"spring.ai.mcp.client.sse.connections.server1.sse-endpoint=/custom-sse")
|
||||
.run(context -> {
|
||||
List<NamedClientMcpTransport> transports = context.getBean("mcpHttpClientTransports", List.class);
|
||||
assertThat(transports).hasSize(1);
|
||||
assertThat(transports.get(0).name()).isEqualTo("server1");
|
||||
assertThat(transports.get(0).transport()).isInstanceOf(HttpClientSseClientTransport.class);
|
||||
|
||||
assertThat(getSseEndpoint((HttpClientSseClientTransport) transports.get(0).transport()))
|
||||
.isEqualTo("/custom-sse");
|
||||
});
|
||||
}
|
||||
|
||||
@Test
|
||||
void customObjectMapperIsUsed() {
|
||||
this.applicationContext
|
||||
.withClassLoader(
|
||||
new FilteredClassLoader("io.modelcontextprotocol.client.transport.WebFluxSseClientTransport"))
|
||||
.withUserConfiguration(CustomObjectMapperConfiguration.class)
|
||||
.withPropertyValues("spring.ai.mcp.client.sse.connections.server1.url=http://localhost:8080")
|
||||
.run(context -> {
|
||||
assertThat(context.getBean(ObjectMapper.class)).isNotNull();
|
||||
List<NamedClientMcpTransport> transports = context.getBean("mcpHttpClientTransports", List.class);
|
||||
assertThat(transports).hasSize(1);
|
||||
});
|
||||
}
|
||||
|
||||
@Test
|
||||
void defaultSseEndpointIsUsedWhenNotSpecified() {
|
||||
this.applicationContext
|
||||
.withClassLoader(
|
||||
new FilteredClassLoader("io.modelcontextprotocol.client.transport.WebFluxSseClientTransport"))
|
||||
.withPropertyValues("spring.ai.mcp.client.sse.connections.server1.url=http://localhost:8080")
|
||||
.run(context -> {
|
||||
List<NamedClientMcpTransport> transports = context.getBean("mcpHttpClientTransports", List.class);
|
||||
assertThat(transports).hasSize(1);
|
||||
assertThat(transports.get(0).name()).isEqualTo("server1");
|
||||
assertThat(transports.get(0).transport()).isInstanceOf(HttpClientSseClientTransport.class);
|
||||
// Default SSE endpoint is "/sse" as specified in the configuration class
|
||||
});
|
||||
}
|
||||
|
||||
@Test
|
||||
void mixedConnectionsWithAndWithoutCustomSseEndpoint() {
|
||||
this.applicationContext
|
||||
.withClassLoader(
|
||||
new FilteredClassLoader("io.modelcontextprotocol.client.transport.WebFluxSseClientTransport"))
|
||||
.withPropertyValues("spring.ai.mcp.client.sse.connections.server1.url=http://localhost:8080",
|
||||
"spring.ai.mcp.client.sse.connections.server1.sse-endpoint=/custom-sse",
|
||||
"spring.ai.mcp.client.sse.connections.server2.url=http://otherserver:8081")
|
||||
.run(context -> {
|
||||
List<NamedClientMcpTransport> transports = context.getBean("mcpHttpClientTransports", List.class);
|
||||
assertThat(transports).hasSize(2);
|
||||
assertThat(transports).extracting("name").containsExactlyInAnyOrder("server1", "server2");
|
||||
assertThat(transports).extracting("transport")
|
||||
.allMatch(transport -> transport instanceof HttpClientSseClientTransport);
|
||||
for (NamedClientMcpTransport transport : transports) {
|
||||
assertThat(transport.transport()).isInstanceOf(HttpClientSseClientTransport.class);
|
||||
if (transport.name().equals("server1")) {
|
||||
assertThat(getSseEndpoint((HttpClientSseClientTransport) transport.transport()))
|
||||
.isEqualTo("/custom-sse");
|
||||
}
|
||||
else {
|
||||
assertThat(getSseEndpoint((HttpClientSseClientTransport) transport.transport()))
|
||||
.isEqualTo("/sse");
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
private String getSseEndpoint(HttpClientSseClientTransport transport) {
|
||||
Field privateField = ReflectionUtils.findField(HttpClientSseClientTransport.class, "sseEndpoint");
|
||||
ReflectionUtils.makeAccessible(privateField);
|
||||
return (String) ReflectionUtils.getField(privateField, transport);
|
||||
}
|
||||
|
||||
@Configuration
|
||||
static class CustomObjectMapperConfiguration {
|
||||
|
||||
@Bean
|
||||
ObjectMapper objectMapper() {
|
||||
return new ObjectMapper();
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -16,14 +16,28 @@
|
||||
|
||||
package org.springframework.ai.mcp.client.autoconfigure;
|
||||
|
||||
import java.lang.reflect.Field;
|
||||
import java.util.List;
|
||||
|
||||
import com.fasterxml.jackson.databind.ObjectMapper;
|
||||
import io.modelcontextprotocol.client.transport.WebFluxSseClientTransport;
|
||||
import org.junit.jupiter.api.Test;
|
||||
|
||||
import org.springframework.boot.autoconfigure.AutoConfigurations;
|
||||
import org.springframework.boot.test.context.FilteredClassLoader;
|
||||
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 org.springframework.web.reactive.function.client.WebClient;
|
||||
|
||||
import static org.assertj.core.api.Assertions.assertThat;
|
||||
|
||||
/**
|
||||
* Tests for {@link SseWebFluxTransportAutoConfiguration}.
|
||||
*
|
||||
* @author Christian Tzolov
|
||||
*/
|
||||
public class SseWebFluxTransportAutoConfigurationTests {
|
||||
|
||||
private final ApplicationContextRunner applicationContext = new ApplicationContextRunner()
|
||||
@@ -31,13 +45,11 @@ public class SseWebFluxTransportAutoConfigurationTests {
|
||||
|
||||
@Test
|
||||
void webFluxClientTransportsPresentIfWebFluxSseClientTransportPresent() {
|
||||
|
||||
this.applicationContext.run(context -> assertThat(context.containsBean("webFluxClientTransports")).isTrue());
|
||||
}
|
||||
|
||||
@Test
|
||||
void webFluxClientTransportsNotPresentIfMissingWebFluxSseClientTransportNotPresent() {
|
||||
|
||||
this.applicationContext
|
||||
.withClassLoader(
|
||||
new FilteredClassLoader("io.modelcontextprotocol.client.transport.WebFluxSseClientTransport"))
|
||||
@@ -46,9 +58,148 @@ public class SseWebFluxTransportAutoConfigurationTests {
|
||||
|
||||
@Test
|
||||
void webFluxClientTransportsNotPresentIfMcpClientDisabled() {
|
||||
|
||||
this.applicationContext.withPropertyValues("spring.ai.mcp.client.enabled", "false")
|
||||
.run(context -> assertThat(context.containsBean("webFluxClientTransports")).isFalse());
|
||||
}
|
||||
|
||||
@Test
|
||||
void noTransportsCreatedWithEmptyConnections() {
|
||||
this.applicationContext.run(context -> {
|
||||
List<NamedClientMcpTransport> transports = context.getBean("webFluxClientTransports", List.class);
|
||||
assertThat(transports).isEmpty();
|
||||
});
|
||||
}
|
||||
|
||||
@Test
|
||||
void singleConnectionCreatesOneTransport() {
|
||||
this.applicationContext
|
||||
.withPropertyValues("spring.ai.mcp.client.sse.connections.server1.url=http://localhost:8080")
|
||||
.run(context -> {
|
||||
List<NamedClientMcpTransport> transports = context.getBean("webFluxClientTransports", List.class);
|
||||
assertThat(transports).hasSize(1);
|
||||
assertThat(transports.get(0).name()).isEqualTo("server1");
|
||||
assertThat(transports.get(0).transport()).isInstanceOf(WebFluxSseClientTransport.class);
|
||||
});
|
||||
}
|
||||
|
||||
@Test
|
||||
void multipleConnectionsCreateMultipleTransports() {
|
||||
this.applicationContext
|
||||
.withPropertyValues("spring.ai.mcp.client.sse.connections.server1.url=http://localhost:8080",
|
||||
"spring.ai.mcp.client.sse.connections.server2.url=http://otherserver:8081")
|
||||
.run(context -> {
|
||||
List<NamedClientMcpTransport> transports = context.getBean("webFluxClientTransports", List.class);
|
||||
assertThat(transports).hasSize(2);
|
||||
assertThat(transports).extracting("name").containsExactlyInAnyOrder("server1", "server2");
|
||||
assertThat(transports).extracting("transport")
|
||||
.allMatch(transport -> transport instanceof WebFluxSseClientTransport);
|
||||
for (NamedClientMcpTransport transport : transports) {
|
||||
assertThat(transport.transport()).isInstanceOf(WebFluxSseClientTransport.class);
|
||||
assertThat(getSseEndpoint((WebFluxSseClientTransport) transport.transport())).isEqualTo("/sse");
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
@Test
|
||||
void customSseEndpointIsRespected() {
|
||||
this.applicationContext
|
||||
.withPropertyValues("spring.ai.mcp.client.sse.connections.server1.url=http://localhost:8080",
|
||||
"spring.ai.mcp.client.sse.connections.server1.sse-endpoint=/custom-sse")
|
||||
.run(context -> {
|
||||
List<NamedClientMcpTransport> transports = context.getBean("webFluxClientTransports", List.class);
|
||||
assertThat(transports).hasSize(1);
|
||||
assertThat(transports.get(0).name()).isEqualTo("server1");
|
||||
assertThat(transports.get(0).transport()).isInstanceOf(WebFluxSseClientTransport.class);
|
||||
|
||||
assertThat(getSseEndpoint((WebFluxSseClientTransport) transports.get(0).transport()))
|
||||
.isEqualTo("/custom-sse");
|
||||
});
|
||||
}
|
||||
|
||||
@Test
|
||||
void customWebClientBuilderIsUsed() {
|
||||
this.applicationContext.withUserConfiguration(CustomWebClientConfiguration.class)
|
||||
.withPropertyValues("spring.ai.mcp.client.sse.connections.server1.url=http://localhost:8080")
|
||||
.run(context -> {
|
||||
assertThat(context.getBean(WebClient.Builder.class)).isNotNull();
|
||||
List<NamedClientMcpTransport> transports = context.getBean("webFluxClientTransports", List.class);
|
||||
assertThat(transports).hasSize(1);
|
||||
});
|
||||
}
|
||||
|
||||
@Test
|
||||
void customObjectMapperIsUsed() {
|
||||
this.applicationContext.withUserConfiguration(CustomObjectMapperConfiguration.class)
|
||||
.withPropertyValues("spring.ai.mcp.client.sse.connections.server1.url=http://localhost:8080")
|
||||
.run(context -> {
|
||||
assertThat(context.getBean(ObjectMapper.class)).isNotNull();
|
||||
List<NamedClientMcpTransport> transports = context.getBean("webFluxClientTransports", List.class);
|
||||
assertThat(transports).hasSize(1);
|
||||
});
|
||||
}
|
||||
|
||||
@Test
|
||||
void defaultSseEndpointIsUsedWhenNotSpecified() {
|
||||
this.applicationContext
|
||||
.withPropertyValues("spring.ai.mcp.client.sse.connections.server1.url=http://localhost:8080")
|
||||
.run(context -> {
|
||||
List<NamedClientMcpTransport> transports = context.getBean("webFluxClientTransports", List.class);
|
||||
assertThat(transports).hasSize(1);
|
||||
assertThat(transports.get(0).name()).isEqualTo("server1");
|
||||
assertThat(transports.get(0).transport()).isInstanceOf(WebFluxSseClientTransport.class);
|
||||
// Default SSE endpoint is "/sse" as specified in the configuration class
|
||||
});
|
||||
}
|
||||
|
||||
@Test
|
||||
void mixedConnectionsWithAndWithoutCustomSseEndpoint() {
|
||||
this.applicationContext
|
||||
.withPropertyValues("spring.ai.mcp.client.sse.connections.server1.url=http://localhost:8080",
|
||||
"spring.ai.mcp.client.sse.connections.server1.sse-endpoint=/custom-sse",
|
||||
"spring.ai.mcp.client.sse.connections.server2.url=http://otherserver:8081")
|
||||
.run(context -> {
|
||||
List<NamedClientMcpTransport> transports = context.getBean("webFluxClientTransports", List.class);
|
||||
assertThat(transports).hasSize(2);
|
||||
assertThat(transports).extracting("name").containsExactlyInAnyOrder("server1", "server2");
|
||||
assertThat(transports).extracting("transport")
|
||||
.allMatch(transport -> transport instanceof WebFluxSseClientTransport);
|
||||
for (NamedClientMcpTransport transport : transports) {
|
||||
assertThat(transport.transport()).isInstanceOf(WebFluxSseClientTransport.class);
|
||||
if (transport.name().equals("server1")) {
|
||||
assertThat(getSseEndpoint((WebFluxSseClientTransport) transport.transport()))
|
||||
.isEqualTo("/custom-sse");
|
||||
}
|
||||
else {
|
||||
assertThat(getSseEndpoint((WebFluxSseClientTransport) transport.transport())).isEqualTo("/sse");
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
private String getSseEndpoint(WebFluxSseClientTransport transport) {
|
||||
Field privateField = ReflectionUtils.findField(WebFluxSseClientTransport.class, "sseEndpoint");
|
||||
ReflectionUtils.makeAccessible(privateField);
|
||||
return (String) ReflectionUtils.getField(privateField, transport);
|
||||
}
|
||||
|
||||
@Configuration
|
||||
static class CustomWebClientConfiguration {
|
||||
|
||||
@Bean
|
||||
WebClient.Builder webClientBuilder() {
|
||||
return WebClient.builder().baseUrl("http://custom-base-url");
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@Configuration
|
||||
static class CustomObjectMapperConfiguration {
|
||||
|
||||
@Bean
|
||||
ObjectMapper objectMapper() {
|
||||
return new ObjectMapper();
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -0,0 +1,289 @@
|
||||
/*
|
||||
* 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.autoconfigure.properties;
|
||||
|
||||
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 java.time.Duration;
|
||||
|
||||
import static org.assertj.core.api.Assertions.assertThat;
|
||||
|
||||
/**
|
||||
* Tests for {@link McpClientCommonProperties}.
|
||||
*
|
||||
* @author Christian Tzolov
|
||||
*/
|
||||
class McpClientCommonPropertiesTests {
|
||||
|
||||
private final ApplicationContextRunner contextRunner = new ApplicationContextRunner()
|
||||
.withUserConfiguration(TestConfiguration.class);
|
||||
|
||||
@Test
|
||||
void defaultValues() {
|
||||
this.contextRunner.run(context -> {
|
||||
McpClientCommonProperties properties = context.getBean(McpClientCommonProperties.class);
|
||||
assertThat(properties.isEnabled()).isTrue();
|
||||
assertThat(properties.getName()).isEqualTo("spring-ai-mcp-client");
|
||||
assertThat(properties.getVersion()).isEqualTo("1.0.0");
|
||||
assertThat(properties.isInitialized()).isTrue();
|
||||
assertThat(properties.getRequestTimeout()).isEqualTo(Duration.ofSeconds(20));
|
||||
assertThat(properties.getType()).isEqualTo(McpClientCommonProperties.ClientType.SYNC);
|
||||
assertThat(properties.isRootChangeNotification()).isTrue();
|
||||
});
|
||||
}
|
||||
|
||||
@Test
|
||||
void customValues() {
|
||||
this.contextRunner
|
||||
.withPropertyValues("spring.ai.mcp.client.enabled=false", "spring.ai.mcp.client.name=custom-client",
|
||||
"spring.ai.mcp.client.version=2.0.0", "spring.ai.mcp.client.initialized=false",
|
||||
"spring.ai.mcp.client.request-timeout=30s", "spring.ai.mcp.client.type=ASYNC",
|
||||
"spring.ai.mcp.client.root-change-notification=false")
|
||||
.run(context -> {
|
||||
McpClientCommonProperties properties = context.getBean(McpClientCommonProperties.class);
|
||||
assertThat(properties.isEnabled()).isFalse();
|
||||
assertThat(properties.getName()).isEqualTo("custom-client");
|
||||
assertThat(properties.getVersion()).isEqualTo("2.0.0");
|
||||
assertThat(properties.isInitialized()).isFalse();
|
||||
assertThat(properties.getRequestTimeout()).isEqualTo(Duration.ofSeconds(30));
|
||||
assertThat(properties.getType()).isEqualTo(McpClientCommonProperties.ClientType.ASYNC);
|
||||
assertThat(properties.isRootChangeNotification()).isFalse();
|
||||
});
|
||||
}
|
||||
|
||||
@Test
|
||||
void setterGetterMethods() {
|
||||
McpClientCommonProperties properties = new McpClientCommonProperties();
|
||||
|
||||
// Test enabled property
|
||||
properties.setEnabled(false);
|
||||
assertThat(properties.isEnabled()).isFalse();
|
||||
|
||||
// Test name property
|
||||
properties.setName("test-client");
|
||||
assertThat(properties.getName()).isEqualTo("test-client");
|
||||
|
||||
// Test version property
|
||||
properties.setVersion("3.0.0");
|
||||
assertThat(properties.getVersion()).isEqualTo("3.0.0");
|
||||
|
||||
// Test initialized property
|
||||
properties.setInitialized(false);
|
||||
assertThat(properties.isInitialized()).isFalse();
|
||||
|
||||
// Test requestTimeout property
|
||||
Duration timeout = Duration.ofMinutes(5);
|
||||
properties.setRequestTimeout(timeout);
|
||||
assertThat(properties.getRequestTimeout()).isEqualTo(timeout);
|
||||
|
||||
// Test type property
|
||||
properties.setType(McpClientCommonProperties.ClientType.ASYNC);
|
||||
assertThat(properties.getType()).isEqualTo(McpClientCommonProperties.ClientType.ASYNC);
|
||||
|
||||
// Test rootChangeNotification property
|
||||
properties.setRootChangeNotification(false);
|
||||
assertThat(properties.isRootChangeNotification()).isFalse();
|
||||
}
|
||||
|
||||
@Test
|
||||
void durationPropertyBinding() {
|
||||
this.contextRunner.withPropertyValues("spring.ai.mcp.client.request-timeout=PT1M30S").run(context -> {
|
||||
McpClientCommonProperties properties = context.getBean(McpClientCommonProperties.class);
|
||||
assertThat(properties.getRequestTimeout()).isEqualTo(Duration.ofSeconds(90));
|
||||
});
|
||||
}
|
||||
|
||||
@Test
|
||||
void enumPropertyBinding() {
|
||||
this.contextRunner.withPropertyValues("spring.ai.mcp.client.type=ASYNC").run(context -> {
|
||||
McpClientCommonProperties properties = context.getBean(McpClientCommonProperties.class);
|
||||
assertThat(properties.getType()).isEqualTo(McpClientCommonProperties.ClientType.ASYNC);
|
||||
});
|
||||
}
|
||||
|
||||
@Test
|
||||
void propertiesFileBinding() {
|
||||
this.contextRunner
|
||||
.withPropertyValues("spring.ai.mcp.client.enabled=false", "spring.ai.mcp.client.name=test-mcp-client",
|
||||
"spring.ai.mcp.client.version=0.5.0", "spring.ai.mcp.client.initialized=false",
|
||||
"spring.ai.mcp.client.request-timeout=45s", "spring.ai.mcp.client.type=ASYNC",
|
||||
"spring.ai.mcp.client.root-change-notification=false")
|
||||
.run(context -> {
|
||||
McpClientCommonProperties properties = context.getBean(McpClientCommonProperties.class);
|
||||
assertThat(properties.isEnabled()).isFalse();
|
||||
assertThat(properties.getName()).isEqualTo("test-mcp-client");
|
||||
assertThat(properties.getVersion()).isEqualTo("0.5.0");
|
||||
assertThat(properties.isInitialized()).isFalse();
|
||||
assertThat(properties.getRequestTimeout()).isEqualTo(Duration.ofSeconds(45));
|
||||
assertThat(properties.getType()).isEqualTo(McpClientCommonProperties.ClientType.ASYNC);
|
||||
assertThat(properties.isRootChangeNotification()).isFalse();
|
||||
});
|
||||
}
|
||||
|
||||
@Test
|
||||
void invalidEnumValue() {
|
||||
this.contextRunner.withPropertyValues("spring.ai.mcp.client.type=INVALID_TYPE").run(context -> {
|
||||
assertThat(context).hasFailed();
|
||||
assertThat(context.getStartupFailure()).hasRootCauseInstanceOf(IllegalArgumentException.class);
|
||||
// The error message doesn't contain the exact enum value, so we'll check for
|
||||
// a more general message
|
||||
assertThat(context.getStartupFailure().getMessage()).contains("Could not bind properties");
|
||||
});
|
||||
}
|
||||
|
||||
@Test
|
||||
void invalidDurationFormat() {
|
||||
this.contextRunner.withPropertyValues("spring.ai.mcp.client.request-timeout=invalid-duration").run(context -> {
|
||||
assertThat(context).hasFailed();
|
||||
// The error message doesn't contain the property name, so we'll check for a
|
||||
// more general message
|
||||
assertThat(context.getStartupFailure().getMessage()).contains("Could not bind properties");
|
||||
});
|
||||
}
|
||||
|
||||
@Test
|
||||
void yamlConfigurationBinding() {
|
||||
this.contextRunner
|
||||
.withPropertyValues("spring.ai.mcp.client.enabled=false", "spring.ai.mcp.client.name=test-mcp-client-yaml",
|
||||
"spring.ai.mcp.client.version=0.6.0", "spring.ai.mcp.client.initialized=false",
|
||||
"spring.ai.mcp.client.request-timeout=60s", "spring.ai.mcp.client.type=ASYNC",
|
||||
"spring.ai.mcp.client.root-change-notification=false")
|
||||
.run(context -> {
|
||||
McpClientCommonProperties properties = context.getBean(McpClientCommonProperties.class);
|
||||
assertThat(properties.isEnabled()).isFalse();
|
||||
assertThat(properties.getName()).isEqualTo("test-mcp-client-yaml");
|
||||
assertThat(properties.getVersion()).isEqualTo("0.6.0");
|
||||
assertThat(properties.isInitialized()).isFalse();
|
||||
assertThat(properties.getRequestTimeout()).isEqualTo(Duration.ofSeconds(60));
|
||||
assertThat(properties.getType()).isEqualTo(McpClientCommonProperties.ClientType.ASYNC);
|
||||
assertThat(properties.isRootChangeNotification()).isFalse();
|
||||
});
|
||||
}
|
||||
|
||||
@Test
|
||||
void configPrefixConstant() {
|
||||
assertThat(McpClientCommonProperties.CONFIG_PREFIX).isEqualTo("spring.ai.mcp.client");
|
||||
}
|
||||
|
||||
@Test
|
||||
void clientTypeEnumValues() {
|
||||
assertThat(McpClientCommonProperties.ClientType.values())
|
||||
.containsExactly(McpClientCommonProperties.ClientType.SYNC, McpClientCommonProperties.ClientType.ASYNC);
|
||||
}
|
||||
|
||||
@Test
|
||||
void disabledProperties() {
|
||||
this.contextRunner.withPropertyValues("spring.ai.mcp.client.enabled=false").run(context -> {
|
||||
McpClientCommonProperties properties = context.getBean(McpClientCommonProperties.class);
|
||||
assertThat(properties.isEnabled()).isFalse();
|
||||
// Other properties should still have their default values
|
||||
assertThat(properties.getName()).isEqualTo("spring-ai-mcp-client");
|
||||
assertThat(properties.getVersion()).isEqualTo("1.0.0");
|
||||
assertThat(properties.isInitialized()).isTrue();
|
||||
assertThat(properties.getRequestTimeout()).isEqualTo(Duration.ofSeconds(20));
|
||||
assertThat(properties.getType()).isEqualTo(McpClientCommonProperties.ClientType.SYNC);
|
||||
assertThat(properties.isRootChangeNotification()).isTrue();
|
||||
});
|
||||
}
|
||||
|
||||
@Test
|
||||
void notInitializedProperties() {
|
||||
this.contextRunner.withPropertyValues("spring.ai.mcp.client.initialized=false").run(context -> {
|
||||
McpClientCommonProperties properties = context.getBean(McpClientCommonProperties.class);
|
||||
assertThat(properties.isInitialized()).isFalse();
|
||||
// Other properties should still have their default values
|
||||
assertThat(properties.isEnabled()).isTrue();
|
||||
assertThat(properties.getName()).isEqualTo("spring-ai-mcp-client");
|
||||
assertThat(properties.getVersion()).isEqualTo("1.0.0");
|
||||
assertThat(properties.getRequestTimeout()).isEqualTo(Duration.ofSeconds(20));
|
||||
assertThat(properties.getType()).isEqualTo(McpClientCommonProperties.ClientType.SYNC);
|
||||
assertThat(properties.isRootChangeNotification()).isTrue();
|
||||
});
|
||||
}
|
||||
|
||||
@Test
|
||||
void rootChangeNotificationDisabled() {
|
||||
this.contextRunner.withPropertyValues("spring.ai.mcp.client.root-change-notification=false").run(context -> {
|
||||
McpClientCommonProperties properties = context.getBean(McpClientCommonProperties.class);
|
||||
assertThat(properties.isRootChangeNotification()).isFalse();
|
||||
// Other properties should still have their default values
|
||||
assertThat(properties.isEnabled()).isTrue();
|
||||
assertThat(properties.getName()).isEqualTo("spring-ai-mcp-client");
|
||||
assertThat(properties.getVersion()).isEqualTo("1.0.0");
|
||||
assertThat(properties.isInitialized()).isTrue();
|
||||
assertThat(properties.getRequestTimeout()).isEqualTo(Duration.ofSeconds(20));
|
||||
assertThat(properties.getType()).isEqualTo(McpClientCommonProperties.ClientType.SYNC);
|
||||
});
|
||||
}
|
||||
|
||||
@Test
|
||||
void customRequestTimeout() {
|
||||
this.contextRunner.withPropertyValues("spring.ai.mcp.client.request-timeout=120s").run(context -> {
|
||||
McpClientCommonProperties properties = context.getBean(McpClientCommonProperties.class);
|
||||
assertThat(properties.getRequestTimeout()).isEqualTo(Duration.ofSeconds(120));
|
||||
// Other properties should still have their default values
|
||||
assertThat(properties.isEnabled()).isTrue();
|
||||
assertThat(properties.getName()).isEqualTo("spring-ai-mcp-client");
|
||||
assertThat(properties.getVersion()).isEqualTo("1.0.0");
|
||||
assertThat(properties.isInitialized()).isTrue();
|
||||
assertThat(properties.getType()).isEqualTo(McpClientCommonProperties.ClientType.SYNC);
|
||||
assertThat(properties.isRootChangeNotification()).isTrue();
|
||||
});
|
||||
}
|
||||
|
||||
@Test
|
||||
void asyncClientType() {
|
||||
this.contextRunner.withPropertyValues("spring.ai.mcp.client.type=ASYNC").run(context -> {
|
||||
McpClientCommonProperties properties = context.getBean(McpClientCommonProperties.class);
|
||||
assertThat(properties.getType()).isEqualTo(McpClientCommonProperties.ClientType.ASYNC);
|
||||
// Other properties should still have their default values
|
||||
assertThat(properties.isEnabled()).isTrue();
|
||||
assertThat(properties.getName()).isEqualTo("spring-ai-mcp-client");
|
||||
assertThat(properties.getVersion()).isEqualTo("1.0.0");
|
||||
assertThat(properties.isInitialized()).isTrue();
|
||||
assertThat(properties.getRequestTimeout()).isEqualTo(Duration.ofSeconds(20));
|
||||
assertThat(properties.isRootChangeNotification()).isTrue();
|
||||
});
|
||||
}
|
||||
|
||||
@Test
|
||||
void customNameAndVersion() {
|
||||
this.contextRunner
|
||||
.withPropertyValues("spring.ai.mcp.client.name=custom-mcp-client", "spring.ai.mcp.client.version=2.5.0")
|
||||
.run(context -> {
|
||||
McpClientCommonProperties properties = context.getBean(McpClientCommonProperties.class);
|
||||
assertThat(properties.getName()).isEqualTo("custom-mcp-client");
|
||||
assertThat(properties.getVersion()).isEqualTo("2.5.0");
|
||||
// Other properties should still have their default values
|
||||
assertThat(properties.isEnabled()).isTrue();
|
||||
assertThat(properties.isInitialized()).isTrue();
|
||||
assertThat(properties.getRequestTimeout()).isEqualTo(Duration.ofSeconds(20));
|
||||
assertThat(properties.getType()).isEqualTo(McpClientCommonProperties.ClientType.SYNC);
|
||||
assertThat(properties.isRootChangeNotification()).isTrue();
|
||||
});
|
||||
}
|
||||
|
||||
@Configuration
|
||||
@EnableConfigurationProperties(McpClientCommonProperties.class)
|
||||
static class TestConfiguration {
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,291 @@
|
||||
/*
|
||||
* 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.autoconfigure.properties;
|
||||
|
||||
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 java.util.Map;
|
||||
|
||||
import static org.assertj.core.api.Assertions.assertThat;
|
||||
|
||||
/**
|
||||
* Tests for {@link McpSseClientProperties}.
|
||||
*
|
||||
* @author Christian Tzolov
|
||||
*/
|
||||
class McpSseClientPropertiesTests {
|
||||
|
||||
private final ApplicationContextRunner contextRunner = new ApplicationContextRunner()
|
||||
.withUserConfiguration(TestConfiguration.class);
|
||||
|
||||
@Test
|
||||
void defaultValues() {
|
||||
this.contextRunner.run(context -> {
|
||||
McpSseClientProperties properties = context.getBean(McpSseClientProperties.class);
|
||||
assertThat(properties.getConnections()).isNotNull();
|
||||
assertThat(properties.getConnections()).isEmpty();
|
||||
});
|
||||
}
|
||||
|
||||
@Test
|
||||
void singleConnection() {
|
||||
this.contextRunner
|
||||
.withPropertyValues("spring.ai.mcp.client.sse.connections.server1.url=http://localhost:8080/events")
|
||||
.run(context -> {
|
||||
McpSseClientProperties properties = context.getBean(McpSseClientProperties.class);
|
||||
assertThat(properties.getConnections()).hasSize(1);
|
||||
assertThat(properties.getConnections()).containsKey("server1");
|
||||
assertThat(properties.getConnections().get("server1").url()).isEqualTo("http://localhost:8080/events");
|
||||
assertThat(properties.getConnections().get("server1").sseEndpoint()).isNull();
|
||||
});
|
||||
}
|
||||
|
||||
@Test
|
||||
void multipleConnections() {
|
||||
this.contextRunner
|
||||
.withPropertyValues("spring.ai.mcp.client.sse.connections.server1.url=http://localhost:8080/events",
|
||||
"spring.ai.mcp.client.sse.connections.server2.url=http://otherserver:8081/events")
|
||||
.run(context -> {
|
||||
McpSseClientProperties properties = context.getBean(McpSseClientProperties.class);
|
||||
assertThat(properties.getConnections()).hasSize(2);
|
||||
assertThat(properties.getConnections()).containsKeys("server1", "server2");
|
||||
assertThat(properties.getConnections().get("server1").url()).isEqualTo("http://localhost:8080/events");
|
||||
assertThat(properties.getConnections().get("server1").sseEndpoint()).isNull();
|
||||
assertThat(properties.getConnections().get("server2").url())
|
||||
.isEqualTo("http://otherserver:8081/events");
|
||||
assertThat(properties.getConnections().get("server2").sseEndpoint()).isNull();
|
||||
});
|
||||
}
|
||||
|
||||
@Test
|
||||
void connectionWithEmptyUrl() {
|
||||
this.contextRunner.withPropertyValues("spring.ai.mcp.client.sse.connections.server1.url=").run(context -> {
|
||||
McpSseClientProperties properties = context.getBean(McpSseClientProperties.class);
|
||||
assertThat(properties.getConnections()).hasSize(1);
|
||||
assertThat(properties.getConnections()).containsKey("server1");
|
||||
assertThat(properties.getConnections().get("server1").url()).isEmpty();
|
||||
assertThat(properties.getConnections().get("server1").sseEndpoint()).isNull();
|
||||
});
|
||||
}
|
||||
|
||||
@Test
|
||||
void connectionWithNullUrl() {
|
||||
// This test verifies that a null URL is not allowed in the SseParameters record
|
||||
// Since records require all parameters to be provided, this test is more of a
|
||||
// documentation
|
||||
// of expected behavior rather than a functional test
|
||||
McpSseClientProperties properties = new McpSseClientProperties();
|
||||
Map<String, McpSseClientProperties.SseParameters> connections = properties.getConnections();
|
||||
|
||||
// We can't create an SseParameters with null URL due to record constraints
|
||||
// But we can verify that the connections map is initialized and empty
|
||||
assertThat(connections).isNotNull();
|
||||
assertThat(connections).isEmpty();
|
||||
}
|
||||
|
||||
@Test
|
||||
void sseParametersRecord() {
|
||||
String url = "http://test-server:8080/events";
|
||||
String sseUrl = "/sse";
|
||||
McpSseClientProperties.SseParameters params = new McpSseClientProperties.SseParameters(url, sseUrl);
|
||||
|
||||
assertThat(params.url()).isEqualTo(url);
|
||||
assertThat(params.sseEndpoint()).isEqualTo(sseUrl);
|
||||
}
|
||||
|
||||
@Test
|
||||
void sseParametersRecordWithNullSseEdnpoint() {
|
||||
String url = "http://test-server:8080/events";
|
||||
McpSseClientProperties.SseParameters params = new McpSseClientProperties.SseParameters(url, null);
|
||||
|
||||
assertThat(params.url()).isEqualTo(url);
|
||||
assertThat(params.sseEndpoint()).isNull();
|
||||
}
|
||||
|
||||
@Test
|
||||
void configPrefixConstant() {
|
||||
assertThat(McpSseClientProperties.CONFIG_PREFIX).isEqualTo("spring.ai.mcp.client.sse");
|
||||
}
|
||||
|
||||
@Test
|
||||
void yamlConfigurationBinding() {
|
||||
this.contextRunner
|
||||
.withPropertyValues("spring.ai.mcp.client.sse.connections.server1.url=http://localhost:8080/events",
|
||||
"spring.ai.mcp.client.sse.connections.server2.url=http://otherserver:8081/events")
|
||||
.run(context -> {
|
||||
McpSseClientProperties properties = context.getBean(McpSseClientProperties.class);
|
||||
assertThat(properties.getConnections()).hasSize(2);
|
||||
assertThat(properties.getConnections()).containsKeys("server1", "server2");
|
||||
assertThat(properties.getConnections().get("server1").url()).isEqualTo("http://localhost:8080/events");
|
||||
assertThat(properties.getConnections().get("server1").sseEndpoint()).isNull();
|
||||
assertThat(properties.getConnections().get("server2").url())
|
||||
.isEqualTo("http://otherserver:8081/events");
|
||||
assertThat(properties.getConnections().get("server2").sseEndpoint()).isNull();
|
||||
});
|
||||
}
|
||||
|
||||
@Test
|
||||
void connectionMapManipulation() {
|
||||
this.contextRunner.run(context -> {
|
||||
McpSseClientProperties properties = context.getBean(McpSseClientProperties.class);
|
||||
Map<String, McpSseClientProperties.SseParameters> connections = properties.getConnections();
|
||||
|
||||
// Add a connection
|
||||
connections.put("server1",
|
||||
new McpSseClientProperties.SseParameters("http://localhost:8080/events", "/sse"));
|
||||
assertThat(properties.getConnections()).hasSize(1);
|
||||
assertThat(properties.getConnections().get("server1").url()).isEqualTo("http://localhost:8080/events");
|
||||
assertThat(properties.getConnections().get("server1").sseEndpoint()).isEqualTo("/sse");
|
||||
|
||||
// Add another connection
|
||||
connections.put("server2",
|
||||
new McpSseClientProperties.SseParameters("http://otherserver:8081/events", null));
|
||||
assertThat(properties.getConnections()).hasSize(2);
|
||||
assertThat(properties.getConnections().get("server2").url()).isEqualTo("http://otherserver:8081/events");
|
||||
assertThat(properties.getConnections().get("server2").sseEndpoint()).isNull();
|
||||
|
||||
// Replace a connection
|
||||
connections.put("server1",
|
||||
new McpSseClientProperties.SseParameters("http://newserver:8082/events", "/events"));
|
||||
assertThat(properties.getConnections()).hasSize(2);
|
||||
assertThat(properties.getConnections().get("server1").url()).isEqualTo("http://newserver:8082/events");
|
||||
assertThat(properties.getConnections().get("server1").sseEndpoint()).isEqualTo("/events");
|
||||
|
||||
// Remove a connection
|
||||
connections.remove("server1");
|
||||
assertThat(properties.getConnections()).hasSize(1);
|
||||
assertThat(properties.getConnections()).containsKey("server2");
|
||||
assertThat(properties.getConnections()).doesNotContainKey("server1");
|
||||
});
|
||||
}
|
||||
|
||||
@Test
|
||||
void specialCharactersInUrl() {
|
||||
this.contextRunner.withPropertyValues(
|
||||
"spring.ai.mcp.client.sse.connections.server1.url=http://localhost:8080/events?param=value&other=123")
|
||||
.run(context -> {
|
||||
McpSseClientProperties properties = context.getBean(McpSseClientProperties.class);
|
||||
assertThat(properties.getConnections()).hasSize(1);
|
||||
assertThat(properties.getConnections().get("server1").url())
|
||||
.isEqualTo("http://localhost:8080/events?param=value&other=123");
|
||||
assertThat(properties.getConnections().get("server1").sseEndpoint()).isNull();
|
||||
});
|
||||
}
|
||||
|
||||
@Test
|
||||
void specialCharactersInConnectionName() {
|
||||
this.contextRunner
|
||||
.withPropertyValues(
|
||||
"spring.ai.mcp.client.sse.connections.server-with-dashes.url=http://localhost:8080/events")
|
||||
.run(context -> {
|
||||
McpSseClientProperties properties = context.getBean(McpSseClientProperties.class);
|
||||
assertThat(properties.getConnections()).hasSize(1);
|
||||
assertThat(properties.getConnections()).containsKey("server-with-dashes");
|
||||
assertThat(properties.getConnections().get("server-with-dashes").url())
|
||||
.isEqualTo("http://localhost:8080/events");
|
||||
assertThat(properties.getConnections().get("server-with-dashes").sseEndpoint()).isNull();
|
||||
});
|
||||
}
|
||||
|
||||
@Test
|
||||
void connectionWithSseEndpoint() {
|
||||
this.contextRunner
|
||||
.withPropertyValues("spring.ai.mcp.client.sse.connections.server1.url=http://localhost:8080",
|
||||
"spring.ai.mcp.client.sse.connections.server1.sse-endpoint=/events")
|
||||
.run(context -> {
|
||||
McpSseClientProperties properties = context.getBean(McpSseClientProperties.class);
|
||||
assertThat(properties.getConnections()).hasSize(1);
|
||||
assertThat(properties.getConnections()).containsKey("server1");
|
||||
assertThat(properties.getConnections().get("server1").url()).isEqualTo("http://localhost:8080");
|
||||
assertThat(properties.getConnections().get("server1").sseEndpoint()).isEqualTo("/events");
|
||||
});
|
||||
}
|
||||
|
||||
@Test
|
||||
void multipleConnectionsWithSseEndpoint() {
|
||||
this.contextRunner
|
||||
.withPropertyValues("spring.ai.mcp.client.sse.connections.server1.url=http://localhost:8080",
|
||||
"spring.ai.mcp.client.sse.connections.server1.sse-endpoint=/events",
|
||||
"spring.ai.mcp.client.sse.connections.server2.url=http://otherserver:8081",
|
||||
"spring.ai.mcp.client.sse.connections.server2.sse-endpoint=/sse")
|
||||
.run(context -> {
|
||||
McpSseClientProperties properties = context.getBean(McpSseClientProperties.class);
|
||||
assertThat(properties.getConnections()).hasSize(2);
|
||||
assertThat(properties.getConnections()).containsKeys("server1", "server2");
|
||||
assertThat(properties.getConnections().get("server1").url()).isEqualTo("http://localhost:8080");
|
||||
assertThat(properties.getConnections().get("server1").sseEndpoint()).isEqualTo("/events");
|
||||
assertThat(properties.getConnections().get("server2").url()).isEqualTo("http://otherserver:8081");
|
||||
assertThat(properties.getConnections().get("server2").sseEndpoint()).isEqualTo("/sse");
|
||||
});
|
||||
}
|
||||
|
||||
@Test
|
||||
void connectionWithEmptySseEndpoint() {
|
||||
this.contextRunner
|
||||
.withPropertyValues("spring.ai.mcp.client.sse.connections.server1.url=http://localhost:8080",
|
||||
"spring.ai.mcp.client.sse.connections.server1.sse-endpoint=")
|
||||
.run(context -> {
|
||||
McpSseClientProperties properties = context.getBean(McpSseClientProperties.class);
|
||||
assertThat(properties.getConnections()).hasSize(1);
|
||||
assertThat(properties.getConnections()).containsKey("server1");
|
||||
assertThat(properties.getConnections().get("server1").url()).isEqualTo("http://localhost:8080");
|
||||
assertThat(properties.getConnections().get("server1").sseEndpoint()).isEmpty();
|
||||
});
|
||||
}
|
||||
|
||||
@Test
|
||||
void mixedConnectionsWithAndWithoutSseEndpoint() {
|
||||
this.contextRunner
|
||||
.withPropertyValues("spring.ai.mcp.client.sse.connections.server1.url=http://localhost:8080",
|
||||
"spring.ai.mcp.client.sse.connections.server1.sse-endpoint=/events",
|
||||
"spring.ai.mcp.client.sse.connections.server2.url=http://otherserver:8081")
|
||||
.run(context -> {
|
||||
McpSseClientProperties properties = context.getBean(McpSseClientProperties.class);
|
||||
assertThat(properties.getConnections()).hasSize(2);
|
||||
assertThat(properties.getConnections()).containsKeys("server1", "server2");
|
||||
assertThat(properties.getConnections().get("server1").url()).isEqualTo("http://localhost:8080");
|
||||
assertThat(properties.getConnections().get("server1").sseEndpoint()).isEqualTo("/events");
|
||||
assertThat(properties.getConnections().get("server2").url()).isEqualTo("http://otherserver:8081");
|
||||
assertThat(properties.getConnections().get("server2").sseEndpoint()).isNull();
|
||||
});
|
||||
}
|
||||
|
||||
@Test
|
||||
void specialCharactersInSseEndpoint() {
|
||||
this.contextRunner
|
||||
.withPropertyValues("spring.ai.mcp.client.sse.connections.server1.url=http://localhost:8080",
|
||||
"spring.ai.mcp.client.sse.connections.server1.sse-endpoint=/events/stream?format=json&timeout=30")
|
||||
.run(context -> {
|
||||
McpSseClientProperties properties = context.getBean(McpSseClientProperties.class);
|
||||
assertThat(properties.getConnections()).hasSize(1);
|
||||
assertThat(properties.getConnections()).containsKey("server1");
|
||||
assertThat(properties.getConnections().get("server1").url()).isEqualTo("http://localhost:8080");
|
||||
assertThat(properties.getConnections().get("server1").sseEndpoint())
|
||||
.isEqualTo("/events/stream?format=json&timeout=30");
|
||||
});
|
||||
}
|
||||
|
||||
@Configuration
|
||||
@EnableConfigurationProperties(McpSseClientProperties.class)
|
||||
static class TestConfiguration {
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
Reference in New Issue
Block a user