refactor(client): enhance HttpClientSseClientTransport with flexible customization API (#117)
- Add builder customizeClient() and customizeRequest() methods - Enable HTTP client and request configuration through consumer-based customization - Deprecate direct constructors in favor of the more flexible builder approach - Add test coverage for customization capabilities Co-authored-by: Christian Tzolov <christian.tzolov@broadcom.com> Signed-off-by: Christian Tzolov <christian.tzolov@broadcom.com>
This commit is contained in:
parent
fbea833384
commit
fab434c088
3 changed files with 185 additions and 22 deletions
|
|
@ -15,7 +15,7 @@ import org.testcontainers.containers.wait.strategy.Wait;
|
|||
*
|
||||
* @author Christian Tzolov
|
||||
*/
|
||||
@Timeout(15) // Giving extra time beyond the client timeout
|
||||
@Timeout(15)
|
||||
class HttpSseMcpAsyncClientTests extends AbstractMcpAsyncClientTests {
|
||||
|
||||
String host = "http://localhost:3004";
|
||||
|
|
@ -29,7 +29,7 @@ class HttpSseMcpAsyncClientTests extends AbstractMcpAsyncClientTests {
|
|||
|
||||
@Override
|
||||
protected McpClientTransport createMcpTransport() {
|
||||
return new HttpClientSseClientTransport(host);
|
||||
return HttpClientSseClientTransport.builder(host).build();
|
||||
}
|
||||
|
||||
@Override
|
||||
|
|
|
|||
|
|
@ -29,7 +29,7 @@ class HttpSseMcpSyncClientTests extends AbstractMcpSyncClientTests {
|
|||
|
||||
@Override
|
||||
protected McpClientTransport createMcpTransport() {
|
||||
return new HttpClientSseClientTransport(host);
|
||||
return HttpClientSseClientTransport.builder(host).build();
|
||||
}
|
||||
|
||||
@Override
|
||||
|
|
|
|||
|
|
@ -4,9 +4,15 @@
|
|||
|
||||
package io.modelcontextprotocol.client.transport;
|
||||
|
||||
import java.net.URI;
|
||||
import java.net.http.HttpClient;
|
||||
import java.net.http.HttpRequest;
|
||||
import java.time.Duration;
|
||||
import java.util.Map;
|
||||
import java.util.concurrent.atomic.AtomicBoolean;
|
||||
import java.util.concurrent.atomic.AtomicInteger;
|
||||
import java.util.concurrent.atomic.AtomicReference;
|
||||
import java.util.function.Consumer;
|
||||
import java.util.function.Function;
|
||||
|
||||
import io.modelcontextprotocol.spec.McpSchema;
|
||||
|
|
@ -26,6 +32,8 @@ import org.springframework.http.codec.ServerSentEvent;
|
|||
import static org.assertj.core.api.Assertions.assertThat;
|
||||
import static org.assertj.core.api.Assertions.assertThatCode;
|
||||
|
||||
import com.fasterxml.jackson.databind.ObjectMapper;
|
||||
|
||||
/**
|
||||
* Tests for the {@link HttpClientSseClientTransport} class.
|
||||
*
|
||||
|
|
@ -51,8 +59,8 @@ class HttpClientSseClientTransportTests {
|
|||
|
||||
private Sinks.Many<ServerSentEvent<String>> events = Sinks.many().unicast().onBackpressureBuffer();
|
||||
|
||||
public TestHttpClientSseClientTransport(String baseUri) {
|
||||
super(baseUri);
|
||||
public TestHttpClientSseClientTransport(final String baseUri) {
|
||||
super(HttpClient.newHttpClient(), HttpRequest.newBuilder(), baseUri, "/sse", new ObjectMapper());
|
||||
}
|
||||
|
||||
public int getInboundMessageCount() {
|
||||
|
|
@ -191,13 +199,14 @@ class HttpClientSseClientTransportTests {
|
|||
StepVerifier.create(transport.sendMessage(testMessage)).verifyComplete();
|
||||
|
||||
// Message count should remain 0 after shutdown
|
||||
assertThat(transport.getInboundMessageCount()).isEqualTo(0);
|
||||
assertThat(transport.getInboundMessageCount()).isZero();
|
||||
}
|
||||
|
||||
@Test
|
||||
void testRetryBehavior() {
|
||||
// Create a client that simulates connection failures
|
||||
HttpClientSseClientTransport failingTransport = new HttpClientSseClientTransport("http://non-existent-host");
|
||||
HttpClientSseClientTransport failingTransport = HttpClientSseClientTransport.builder("http://non-existent-host")
|
||||
.build();
|
||||
|
||||
// Verify that the transport attempts to reconnect
|
||||
StepVerifier.create(Mono.delay(Duration.ofSeconds(2))).expectNextCount(1).verifyComplete();
|
||||
|
|
@ -275,4 +284,84 @@ class HttpClientSseClientTransportTests {
|
|||
assertThat(transport.getInboundMessageCount()).isEqualTo(3);
|
||||
}
|
||||
|
||||
@Test
|
||||
void testCustomizeClient() {
|
||||
// Create an atomic boolean to verify the customizer was called
|
||||
AtomicBoolean customizerCalled = new AtomicBoolean(false);
|
||||
|
||||
// Create a transport with the customizer
|
||||
HttpClientSseClientTransport customizedTransport = HttpClientSseClientTransport.builder(host)
|
||||
.customizeClient(builder -> {
|
||||
builder.version(HttpClient.Version.HTTP_2);
|
||||
customizerCalled.set(true);
|
||||
})
|
||||
.build();
|
||||
|
||||
// Verify the customizer was called
|
||||
assertThat(customizerCalled.get()).isTrue();
|
||||
|
||||
// Clean up
|
||||
customizedTransport.closeGracefully().block();
|
||||
}
|
||||
|
||||
@Test
|
||||
void testCustomizeRequest() {
|
||||
// Create an atomic boolean to verify the customizer was called
|
||||
AtomicBoolean customizerCalled = new AtomicBoolean(false);
|
||||
|
||||
// Create a reference to store the custom header value
|
||||
AtomicReference<String> headerName = new AtomicReference<>();
|
||||
AtomicReference<String> headerValue = new AtomicReference<>();
|
||||
|
||||
// Create a transport with the customizer
|
||||
HttpClientSseClientTransport customizedTransport = HttpClientSseClientTransport.builder(host)
|
||||
// Create a request customizer that adds a custom header
|
||||
.customizeRequest(builder -> {
|
||||
builder.header("X-Custom-Header", "test-value");
|
||||
customizerCalled.set(true);
|
||||
|
||||
// Create a new request to verify the header was set
|
||||
HttpRequest request = builder.uri(URI.create("http://example.com")).build();
|
||||
headerName.set("X-Custom-Header");
|
||||
headerValue.set(request.headers().firstValue("X-Custom-Header").orElse(null));
|
||||
})
|
||||
.build();
|
||||
|
||||
// Verify the customizer was called
|
||||
assertThat(customizerCalled.get()).isTrue();
|
||||
|
||||
// Verify the header was set correctly
|
||||
assertThat(headerName.get()).isEqualTo("X-Custom-Header");
|
||||
assertThat(headerValue.get()).isEqualTo("test-value");
|
||||
|
||||
// Clean up
|
||||
customizedTransport.closeGracefully().block();
|
||||
}
|
||||
|
||||
@Test
|
||||
void testChainedCustomizations() {
|
||||
// Create atomic booleans to verify both customizers were called
|
||||
AtomicBoolean clientCustomizerCalled = new AtomicBoolean(false);
|
||||
AtomicBoolean requestCustomizerCalled = new AtomicBoolean(false);
|
||||
|
||||
// Create a transport with both customizers chained
|
||||
HttpClientSseClientTransport customizedTransport = HttpClientSseClientTransport.builder(host)
|
||||
.customizeClient(builder -> {
|
||||
builder.connectTimeout(Duration.ofSeconds(30));
|
||||
clientCustomizerCalled.set(true);
|
||||
})
|
||||
.customizeRequest(builder -> {
|
||||
builder.header("X-Api-Key", "test-api-key");
|
||||
requestCustomizerCalled.set(true);
|
||||
})
|
||||
.build();
|
||||
|
||||
// Verify both customizers were called
|
||||
assertThat(clientCustomizerCalled.get()).isTrue();
|
||||
assertThat(requestCustomizerCalled.get()).isTrue();
|
||||
|
||||
// Clean up
|
||||
customizedTransport.closeGracefully().block();
|
||||
}
|
||||
|
||||
}
|
||||
|
|
|
|||
Loading…
Reference in a new issue