diff --git a/mcp/src/main/java/io/modelcontextprotocol/client/transport/HttpClientSseClientTransport.java b/mcp/src/main/java/io/modelcontextprotocol/client/transport/HttpClientSseClientTransport.java index a92f26c..74a3155 100644 --- a/mcp/src/main/java/io/modelcontextprotocol/client/transport/HttpClientSseClientTransport.java +++ b/mcp/src/main/java/io/modelcontextprotocol/client/transport/HttpClientSseClientTransport.java @@ -22,6 +22,8 @@ import org.slf4j.LoggerFactory; import com.fasterxml.jackson.core.type.TypeReference; import com.fasterxml.jackson.databind.ObjectMapper; +import io.modelcontextprotocol.client.transport.customizer.McpAsyncHttpRequestCustomizer; +import io.modelcontextprotocol.client.transport.customizer.McpSyncHttpRequestCustomizer; import io.modelcontextprotocol.client.transport.ResponseSubscribers.ResponseEvent; import io.modelcontextprotocol.spec.McpClientTransport; import io.modelcontextprotocol.spec.McpSchema; @@ -112,7 +114,7 @@ public class HttpClientSseClientTransport implements McpClientTransport { /** * Customizer to modify requests before they are executed. */ - private final AsyncHttpRequestCustomizer httpRequestCustomizer; + private final McpAsyncHttpRequestCustomizer httpRequestCustomizer; /** * Creates a new transport instance with default HTTP client and object mapper. @@ -186,7 +188,7 @@ public class HttpClientSseClientTransport implements McpClientTransport { @Deprecated(forRemoval = true) HttpClientSseClientTransport(HttpClient httpClient, HttpRequest.Builder requestBuilder, String baseUri, String sseEndpoint, ObjectMapper objectMapper) { - this(httpClient, requestBuilder, baseUri, sseEndpoint, objectMapper, AsyncHttpRequestCustomizer.NOOP); + this(httpClient, requestBuilder, baseUri, sseEndpoint, objectMapper, McpAsyncHttpRequestCustomizer.NOOP); } /** @@ -202,7 +204,7 @@ public class HttpClientSseClientTransport implements McpClientTransport { * @throws IllegalArgumentException if objectMapper, clientBuilder, or headers is null */ HttpClientSseClientTransport(HttpClient httpClient, HttpRequest.Builder requestBuilder, String baseUri, - String sseEndpoint, ObjectMapper objectMapper, AsyncHttpRequestCustomizer httpRequestCustomizer) { + String sseEndpoint, ObjectMapper objectMapper, McpAsyncHttpRequestCustomizer httpRequestCustomizer) { Assert.notNull(objectMapper, "ObjectMapper must not be null"); Assert.hasText(baseUri, "baseUri must not be empty"); Assert.hasText(sseEndpoint, "sseEndpoint must not be empty"); @@ -246,7 +248,7 @@ public class HttpClientSseClientTransport implements McpClientTransport { private HttpRequest.Builder requestBuilder = HttpRequest.newBuilder(); - private AsyncHttpRequestCustomizer httpRequestCustomizer = AsyncHttpRequestCustomizer.NOOP; + private McpAsyncHttpRequestCustomizer httpRequestCustomizer = McpAsyncHttpRequestCustomizer.NOOP; private Duration connectTimeout = Duration.ofSeconds(10); @@ -352,16 +354,16 @@ public class HttpClientSseClientTransport implements McpClientTransport { * executing them. *

* This overrides the customizer from - * {@link #asyncHttpRequestCustomizer(AsyncHttpRequestCustomizer)}. + * {@link #asyncHttpRequestCustomizer(McpAsyncHttpRequestCustomizer)}. *

- * Do NOT use a blocking {@link SyncHttpRequestCustomizer} in a non-blocking - * context. Use {@link #asyncHttpRequestCustomizer(AsyncHttpRequestCustomizer)} + * Do NOT use a blocking {@link McpSyncHttpRequestCustomizer} in a non-blocking + * context. Use {@link #asyncHttpRequestCustomizer(McpAsyncHttpRequestCustomizer)} * instead. * @param syncHttpRequestCustomizer the request customizer * @return this builder */ - public Builder httpRequestCustomizer(SyncHttpRequestCustomizer syncHttpRequestCustomizer) { - this.httpRequestCustomizer = AsyncHttpRequestCustomizer.fromSync(syncHttpRequestCustomizer); + public Builder httpRequestCustomizer(McpSyncHttpRequestCustomizer syncHttpRequestCustomizer) { + this.httpRequestCustomizer = McpAsyncHttpRequestCustomizer.fromSync(syncHttpRequestCustomizer); return this; } @@ -370,13 +372,13 @@ public class HttpClientSseClientTransport implements McpClientTransport { * executing them. *

* This overrides the customizer from - * {@link #httpRequestCustomizer(SyncHttpRequestCustomizer)}. + * {@link #httpRequestCustomizer(McpSyncHttpRequestCustomizer)}. *

* Do NOT use a blocking implementation in a non-blocking context. * @param asyncHttpRequestCustomizer the request customizer * @return this builder */ - public Builder asyncHttpRequestCustomizer(AsyncHttpRequestCustomizer asyncHttpRequestCustomizer) { + public Builder asyncHttpRequestCustomizer(McpAsyncHttpRequestCustomizer asyncHttpRequestCustomizer) { this.httpRequestCustomizer = asyncHttpRequestCustomizer; return this; } diff --git a/mcp/src/main/java/io/modelcontextprotocol/client/transport/HttpClientStreamableHttpTransport.java b/mcp/src/main/java/io/modelcontextprotocol/client/transport/HttpClientStreamableHttpTransport.java index d8c49ae..a3815d0 100644 --- a/mcp/src/main/java/io/modelcontextprotocol/client/transport/HttpClientStreamableHttpTransport.java +++ b/mcp/src/main/java/io/modelcontextprotocol/client/transport/HttpClientStreamableHttpTransport.java @@ -25,6 +25,8 @@ import org.slf4j.LoggerFactory; import com.fasterxml.jackson.core.type.TypeReference; import com.fasterxml.jackson.databind.ObjectMapper; +import io.modelcontextprotocol.client.transport.customizer.McpAsyncHttpRequestCustomizer; +import io.modelcontextprotocol.client.transport.customizer.McpSyncHttpRequestCustomizer; import io.modelcontextprotocol.client.transport.ResponseSubscribers.ResponseEvent; import io.modelcontextprotocol.spec.DefaultMcpTransportSession; import io.modelcontextprotocol.spec.DefaultMcpTransportStream; @@ -113,7 +115,7 @@ public class HttpClientStreamableHttpTransport implements McpClientTransport { private final boolean resumableStreams; - private final AsyncHttpRequestCustomizer httpRequestCustomizer; + private final McpAsyncHttpRequestCustomizer httpRequestCustomizer; private final AtomicReference activeSession = new AtomicReference<>(); @@ -123,7 +125,7 @@ public class HttpClientStreamableHttpTransport implements McpClientTransport { private HttpClientStreamableHttpTransport(ObjectMapper objectMapper, HttpClient httpClient, HttpRequest.Builder requestBuilder, String baseUri, String endpoint, boolean resumableStreams, - boolean openConnectionOnStartup, AsyncHttpRequestCustomizer httpRequestCustomizer) { + boolean openConnectionOnStartup, McpAsyncHttpRequestCustomizer httpRequestCustomizer) { this.objectMapper = objectMapper; this.httpClient = httpClient; this.requestBuilder = requestBuilder; @@ -598,7 +600,7 @@ public class HttpClientStreamableHttpTransport implements McpClientTransport { private HttpRequest.Builder requestBuilder = HttpRequest.newBuilder(); - private AsyncHttpRequestCustomizer httpRequestCustomizer = AsyncHttpRequestCustomizer.NOOP; + private McpAsyncHttpRequestCustomizer httpRequestCustomizer = McpAsyncHttpRequestCustomizer.NOOP; private Duration connectTimeout = Duration.ofSeconds(10); @@ -709,16 +711,16 @@ public class HttpClientStreamableHttpTransport implements McpClientTransport { * executing them. *

* This overrides the customizer from - * {@link #asyncHttpRequestCustomizer(AsyncHttpRequestCustomizer)}. + * {@link #asyncHttpRequestCustomizer(McpAsyncHttpRequestCustomizer)}. *

- * Do NOT use a blocking {@link SyncHttpRequestCustomizer} in a non-blocking - * context. Use {@link #asyncHttpRequestCustomizer(AsyncHttpRequestCustomizer)} + * Do NOT use a blocking {@link McpSyncHttpRequestCustomizer} in a non-blocking + * context. Use {@link #asyncHttpRequestCustomizer(McpAsyncHttpRequestCustomizer)} * instead. * @param syncHttpRequestCustomizer the request customizer * @return this builder */ - public Builder httpRequestCustomizer(SyncHttpRequestCustomizer syncHttpRequestCustomizer) { - this.httpRequestCustomizer = AsyncHttpRequestCustomizer.fromSync(syncHttpRequestCustomizer); + public Builder httpRequestCustomizer(McpSyncHttpRequestCustomizer syncHttpRequestCustomizer) { + this.httpRequestCustomizer = McpAsyncHttpRequestCustomizer.fromSync(syncHttpRequestCustomizer); return this; } @@ -727,13 +729,13 @@ public class HttpClientStreamableHttpTransport implements McpClientTransport { * executing them. *

* This overrides the customizer from - * {@link #httpRequestCustomizer(SyncHttpRequestCustomizer)}. + * {@link #httpRequestCustomizer(McpSyncHttpRequestCustomizer)}. *

* Do NOT use a blocking implementation in a non-blocking context. * @param asyncHttpRequestCustomizer the request customizer * @return this builder */ - public Builder asyncHttpRequestCustomizer(AsyncHttpRequestCustomizer asyncHttpRequestCustomizer) { + public Builder asyncHttpRequestCustomizer(McpAsyncHttpRequestCustomizer asyncHttpRequestCustomizer) { this.httpRequestCustomizer = asyncHttpRequestCustomizer; return this; } diff --git a/mcp/src/main/java/io/modelcontextprotocol/client/transport/customizer/DelegatingMcpAsyncHttpRequestCustomizer.java b/mcp/src/main/java/io/modelcontextprotocol/client/transport/customizer/DelegatingMcpAsyncHttpRequestCustomizer.java new file mode 100644 index 0000000..22ba6a2 --- /dev/null +++ b/mcp/src/main/java/io/modelcontextprotocol/client/transport/customizer/DelegatingMcpAsyncHttpRequestCustomizer.java @@ -0,0 +1,38 @@ +/* + * Copyright 2024-2025 the original author or authors. + */ +package io.modelcontextprotocol.client.transport.customizer; + +import io.modelcontextprotocol.util.Assert; +import java.net.URI; +import java.net.http.HttpRequest; +import java.util.List; +import org.reactivestreams.Publisher; +import reactor.core.publisher.Mono; + +/** + * Composable {@link McpAsyncHttpRequestCustomizer} that applies multiple customizers, in + * order. + * + * @author Daniel Garnier-Moiroux + */ +public class DelegatingMcpAsyncHttpRequestCustomizer implements McpAsyncHttpRequestCustomizer { + + private final List customizers; + + public DelegatingMcpAsyncHttpRequestCustomizer(List customizers) { + Assert.notNull(customizers, "Customizers must not be null"); + this.customizers = customizers; + } + + @Override + public Publisher customize(HttpRequest.Builder builder, String method, URI endpoint, + String body) { + var result = Mono.just(builder); + for (var customizer : this.customizers) { + result = result.flatMap(b -> Mono.from(customizer.customize(b, method, endpoint, body))); + } + return result; + } + +} diff --git a/mcp/src/main/java/io/modelcontextprotocol/client/transport/customizer/DelegatingMcpSyncHttpRequestCustomizer.java b/mcp/src/main/java/io/modelcontextprotocol/client/transport/customizer/DelegatingMcpSyncHttpRequestCustomizer.java new file mode 100644 index 0000000..65649d9 --- /dev/null +++ b/mcp/src/main/java/io/modelcontextprotocol/client/transport/customizer/DelegatingMcpSyncHttpRequestCustomizer.java @@ -0,0 +1,32 @@ +/* + * Copyright 2024-2025 the original author or authors. + */ + +package io.modelcontextprotocol.client.transport.customizer; + +import io.modelcontextprotocol.util.Assert; +import java.net.URI; +import java.net.http.HttpRequest; +import java.util.List; + +/** + * Composable {@link McpSyncHttpRequestCustomizer} that applies multiple customizers, in + * order. + * + * @author Daniel Garnier-Moiroux + */ +public class DelegatingMcpSyncHttpRequestCustomizer implements McpSyncHttpRequestCustomizer { + + private final List delegates; + + public DelegatingMcpSyncHttpRequestCustomizer(List customizers) { + Assert.notNull(customizers, "Customizers must not be null"); + this.delegates = customizers; + } + + @Override + public void customize(HttpRequest.Builder builder, String method, URI endpoint, String body) { + this.delegates.forEach(delegate -> delegate.customize(builder, method, endpoint, body)); + } + +} diff --git a/mcp/src/main/java/io/modelcontextprotocol/client/transport/AsyncHttpRequestCustomizer.java b/mcp/src/main/java/io/modelcontextprotocol/client/transport/customizer/McpAsyncHttpRequestCustomizer.java similarity index 79% rename from mcp/src/main/java/io/modelcontextprotocol/client/transport/AsyncHttpRequestCustomizer.java rename to mcp/src/main/java/io/modelcontextprotocol/client/transport/customizer/McpAsyncHttpRequestCustomizer.java index dee026d..2f685c3 100644 --- a/mcp/src/main/java/io/modelcontextprotocol/client/transport/AsyncHttpRequestCustomizer.java +++ b/mcp/src/main/java/io/modelcontextprotocol/client/transport/customizer/McpAsyncHttpRequestCustomizer.java @@ -2,7 +2,7 @@ * Copyright 2024-2025 the original author or authors. */ -package io.modelcontextprotocol.client.transport; +package io.modelcontextprotocol.client.transport.customizer; import java.net.URI; import java.net.http.HttpRequest; @@ -19,12 +19,12 @@ import reactor.util.annotation.Nullable; * * @author Daniel Garnier-Moiroux */ -public interface AsyncHttpRequestCustomizer { +public interface McpAsyncHttpRequestCustomizer { Publisher customize(HttpRequest.Builder builder, String method, URI endpoint, @Nullable String body); - AsyncHttpRequestCustomizer NOOP = new Noop(); + McpAsyncHttpRequestCustomizer NOOP = new Noop(); /** * Wrap a sync implementation in an async wrapper. @@ -32,14 +32,14 @@ public interface AsyncHttpRequestCustomizer { * Do NOT wrap a blocking implementation for use in a non-blocking context. For a * blocking implementation, consider using {@link Schedulers#boundedElastic()}. */ - static AsyncHttpRequestCustomizer fromSync(SyncHttpRequestCustomizer customizer) { + static McpAsyncHttpRequestCustomizer fromSync(McpSyncHttpRequestCustomizer customizer) { return (builder, method, uri, body) -> Mono.fromSupplier(() -> { customizer.customize(builder, method, uri, body); return builder; }); } - class Noop implements AsyncHttpRequestCustomizer { + class Noop implements McpAsyncHttpRequestCustomizer { @Override public Publisher customize(HttpRequest.Builder builder, String method, URI endpoint, diff --git a/mcp/src/main/java/io/modelcontextprotocol/client/transport/SyncHttpRequestCustomizer.java b/mcp/src/main/java/io/modelcontextprotocol/client/transport/customizer/McpSyncHttpRequestCustomizer.java similarity index 79% rename from mcp/src/main/java/io/modelcontextprotocol/client/transport/SyncHttpRequestCustomizer.java rename to mcp/src/main/java/io/modelcontextprotocol/client/transport/customizer/McpSyncHttpRequestCustomizer.java index 72b6e6c..8d2c4a6 100644 --- a/mcp/src/main/java/io/modelcontextprotocol/client/transport/SyncHttpRequestCustomizer.java +++ b/mcp/src/main/java/io/modelcontextprotocol/client/transport/customizer/McpSyncHttpRequestCustomizer.java @@ -2,7 +2,7 @@ * Copyright 2024-2025 the original author or authors. */ -package io.modelcontextprotocol.client.transport; +package io.modelcontextprotocol.client.transport.customizer; import java.net.URI; import java.net.http.HttpRequest; @@ -14,7 +14,7 @@ import reactor.util.annotation.Nullable; * * @author Daniel Garnier-Moiroux */ -public interface SyncHttpRequestCustomizer { +public interface McpSyncHttpRequestCustomizer { void customize(HttpRequest.Builder builder, String method, URI endpoint, @Nullable String body); diff --git a/mcp/src/test/java/io/modelcontextprotocol/client/transport/HttpClientSseClientTransportTests.java b/mcp/src/test/java/io/modelcontextprotocol/client/transport/HttpClientSseClientTransportTests.java index 46b9207..f5a5ecb 100644 --- a/mcp/src/test/java/io/modelcontextprotocol/client/transport/HttpClientSseClientTransportTests.java +++ b/mcp/src/test/java/io/modelcontextprotocol/client/transport/HttpClientSseClientTransportTests.java @@ -15,6 +15,8 @@ import java.util.concurrent.atomic.AtomicReference; import java.util.function.Function; import com.fasterxml.jackson.databind.ObjectMapper; +import io.modelcontextprotocol.client.transport.customizer.McpAsyncHttpRequestCustomizer; +import io.modelcontextprotocol.client.transport.customizer.McpSyncHttpRequestCustomizer; import io.modelcontextprotocol.spec.McpSchema; import io.modelcontextprotocol.spec.McpSchema.JSONRPCRequest; import org.junit.jupiter.api.AfterAll; @@ -72,7 +74,7 @@ class HttpClientSseClientTransportTests { public TestHttpClientSseClientTransport(final String baseUri) { super(HttpClient.newBuilder().version(HttpClient.Version.HTTP_1_1).build(), HttpRequest.newBuilder().header("Content-Type", "application/json"), baseUri, "/sse", - new ObjectMapper(), AsyncHttpRequestCustomizer.NOOP); + new ObjectMapper(), McpAsyncHttpRequestCustomizer.NOOP); } public int getInboundMessageCount() { @@ -389,7 +391,7 @@ class HttpClientSseClientTransportTests { @Test void testRequestCustomizer() { - var mockCustomizer = mock(SyncHttpRequestCustomizer.class); + var mockCustomizer = mock(McpSyncHttpRequestCustomizer.class); // Create a transport with the customizer var customizedTransport = HttpClientSseClientTransport.builder(host) @@ -423,7 +425,7 @@ class HttpClientSseClientTransportTests { @Test void testAsyncRequestCustomizer() { - var mockCustomizer = mock(AsyncHttpRequestCustomizer.class); + var mockCustomizer = mock(McpAsyncHttpRequestCustomizer.class); when(mockCustomizer.customize(any(), any(), any(), any())) .thenAnswer(invocation -> Mono.just(invocation.getArguments()[0])); diff --git a/mcp/src/test/java/io/modelcontextprotocol/client/transport/HttpClientStreamableHttpTransportEmptyJsonResponseTest.java b/mcp/src/test/java/io/modelcontextprotocol/client/transport/HttpClientStreamableHttpTransportEmptyJsonResponseTest.java index 8b36686..e7d048f 100644 --- a/mcp/src/test/java/io/modelcontextprotocol/client/transport/HttpClientStreamableHttpTransportEmptyJsonResponseTest.java +++ b/mcp/src/test/java/io/modelcontextprotocol/client/transport/HttpClientStreamableHttpTransportEmptyJsonResponseTest.java @@ -22,6 +22,7 @@ import org.junit.jupiter.api.Timeout; import com.sun.net.httpserver.HttpServer; +import io.modelcontextprotocol.client.transport.customizer.McpSyncHttpRequestCustomizer; import io.modelcontextprotocol.server.transport.TomcatTestUtil; import io.modelcontextprotocol.spec.McpSchema; import io.modelcontextprotocol.spec.ProtocolVersions; @@ -70,7 +71,7 @@ public class HttpClientStreamableHttpTransportEmptyJsonResponseTest { void testNotificationInitialized() throws URISyntaxException { var uri = new URI(host + "/mcp"); - var mockRequestCustomizer = mock(SyncHttpRequestCustomizer.class); + var mockRequestCustomizer = mock(McpSyncHttpRequestCustomizer.class); var transport = HttpClientStreamableHttpTransport.builder(host) .httpRequestCustomizer(mockRequestCustomizer) .build(); diff --git a/mcp/src/test/java/io/modelcontextprotocol/client/transport/HttpClientStreamableHttpTransportTest.java b/mcp/src/test/java/io/modelcontextprotocol/client/transport/HttpClientStreamableHttpTransportTest.java index d645bb0..f08518c 100644 --- a/mcp/src/test/java/io/modelcontextprotocol/client/transport/HttpClientStreamableHttpTransportTest.java +++ b/mcp/src/test/java/io/modelcontextprotocol/client/transport/HttpClientStreamableHttpTransportTest.java @@ -4,6 +4,8 @@ package io.modelcontextprotocol.client.transport; +import io.modelcontextprotocol.client.transport.customizer.McpAsyncHttpRequestCustomizer; +import io.modelcontextprotocol.client.transport.customizer.McpSyncHttpRequestCustomizer; import io.modelcontextprotocol.spec.McpSchema; import java.net.URI; import java.net.URISyntaxException; @@ -63,7 +65,7 @@ class HttpClientStreamableHttpTransportTest { @Test void testRequestCustomizer() throws URISyntaxException { var uri = new URI(host + "/mcp"); - var mockRequestCustomizer = mock(SyncHttpRequestCustomizer.class); + var mockRequestCustomizer = mock(McpSyncHttpRequestCustomizer.class); var transport = HttpClientStreamableHttpTransport.builder(host) .httpRequestCustomizer(mockRequestCustomizer) @@ -88,7 +90,7 @@ class HttpClientStreamableHttpTransportTest { @Test void testAsyncRequestCustomizer() throws URISyntaxException { var uri = new URI(host + "/mcp"); - var mockRequestCustomizer = mock(AsyncHttpRequestCustomizer.class); + var mockRequestCustomizer = mock(McpAsyncHttpRequestCustomizer.class); when(mockRequestCustomizer.customize(any(), any(), any(), any())) .thenAnswer(invocation -> Mono.just(invocation.getArguments()[0])); diff --git a/mcp/src/test/java/io/modelcontextprotocol/client/transport/customizer/DelegatingMcpAsyncHttpRequestCustomizerTest.java b/mcp/src/test/java/io/modelcontextprotocol/client/transport/customizer/DelegatingMcpAsyncHttpRequestCustomizerTest.java new file mode 100644 index 0000000..f136cd6 --- /dev/null +++ b/mcp/src/test/java/io/modelcontextprotocol/client/transport/customizer/DelegatingMcpAsyncHttpRequestCustomizerTest.java @@ -0,0 +1,67 @@ +/* + * Copyright 2024-2025 the original author or authors. + */ + +package io.modelcontextprotocol.client.transport.customizer; + +import java.net.URI; +import java.net.http.HttpRequest; +import java.util.List; +import org.junit.jupiter.api.Test; +import reactor.core.publisher.Mono; +import reactor.test.StepVerifier; + +import static org.assertj.core.api.Assertions.assertThatThrownBy; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.when; + +/** + * Tests for {@link DelegatingMcpAsyncHttpRequestCustomizer}. + * + * @author Daniel Garnier-Moiroux + */ +class DelegatingMcpAsyncHttpRequestCustomizerTest { + + private static final URI TEST_URI = URI.create("https://example.com"); + + private final HttpRequest.Builder TEST_BUILDER = HttpRequest.newBuilder(TEST_URI); + + @Test + void delegates() { + var mockCustomizer = mock(McpAsyncHttpRequestCustomizer.class); + when(mockCustomizer.customize(any(), any(), any(), any())) + .thenAnswer(invocation -> Mono.just(invocation.getArguments()[0])); + var customizer = new DelegatingMcpAsyncHttpRequestCustomizer(List.of(mockCustomizer)); + + StepVerifier.create(customizer.customize(TEST_BUILDER, "GET", TEST_URI, "{\"everybody\": \"needs somebody\"}")) + .expectNext(TEST_BUILDER) + .verifyComplete(); + + verify(mockCustomizer).customize(TEST_BUILDER, "GET", TEST_URI, "{\"everybody\": \"needs somebody\"}"); + } + + @Test + void delegatesInOrder() { + var customizer = new DelegatingMcpAsyncHttpRequestCustomizer( + List.of((builder, method, uri, body) -> Mono.just(builder.copy().header("x-test", "one")), + (builder, method, uri, body) -> Mono.just(builder.copy().header("x-test", "two")))); + + var headers = Mono + .from(customizer.customize(TEST_BUILDER, "GET", TEST_URI, "{\"everybody\": \"needs somebody\"}")) + .map(HttpRequest.Builder::build) + .map(HttpRequest::headers) + .flatMapIterable(h -> h.allValues("x-test")); + + StepVerifier.create(headers).expectNext("one").expectNext("two").verifyComplete(); + } + + @Test + void constructorRequiresNonNull() { + assertThatThrownBy(() -> new DelegatingMcpAsyncHttpRequestCustomizer(null)) + .isInstanceOf(IllegalArgumentException.class) + .hasMessage("Customizers must not be null"); + } + +} diff --git a/mcp/src/test/java/io/modelcontextprotocol/client/transport/customizer/DelegatingMcpSyncHttpRequestCustomizerTest.java b/mcp/src/test/java/io/modelcontextprotocol/client/transport/customizer/DelegatingMcpSyncHttpRequestCustomizerTest.java new file mode 100644 index 0000000..4274729 --- /dev/null +++ b/mcp/src/test/java/io/modelcontextprotocol/client/transport/customizer/DelegatingMcpSyncHttpRequestCustomizerTest.java @@ -0,0 +1,58 @@ +/* + * Copyright 2024-2025 the original author or authors. + */ + +package io.modelcontextprotocol.client.transport.customizer; + +import java.net.URI; +import java.net.http.HttpRequest; +import java.util.List; +import org.junit.jupiter.api.Test; +import org.mockito.Mockito; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatThrownBy; +import static org.mockito.Mockito.verify; + +/** + * Tests for {@link DelegatingMcpSyncHttpRequestCustomizer}. + * + * @author Daniel Garnier-Moiroux + */ +class DelegatingMcpSyncHttpRequestCustomizerTest { + + private static final URI TEST_URI = URI.create("https://example.com"); + + private final HttpRequest.Builder TEST_BUILDER = HttpRequest.newBuilder(TEST_URI); + + @Test + void delegates() { + var mockCustomizer = Mockito.mock(McpSyncHttpRequestCustomizer.class); + var customizer = new DelegatingMcpSyncHttpRequestCustomizer(List.of(mockCustomizer)); + + customizer.customize(TEST_BUILDER, "GET", TEST_URI, "{\"everybody\": \"needs somebody\"}"); + + verify(mockCustomizer).customize(TEST_BUILDER, "GET", TEST_URI, "{\"everybody\": \"needs somebody\"}"); + } + + @Test + void delegatesInOrder() { + var testHeaderName = "x-test"; + var customizer = new DelegatingMcpSyncHttpRequestCustomizer( + List.of((builder, method, uri, body) -> builder.header(testHeaderName, "one"), + (builder, method, uri, body) -> builder.header(testHeaderName, "two"))); + + customizer.customize(TEST_BUILDER, "GET", TEST_URI, ""); + var request = TEST_BUILDER.build(); + + assertThat(request.headers().allValues(testHeaderName)).containsExactly("one", "two"); + } + + @Test + void constructorRequiresNonNull() { + assertThatThrownBy(() -> new DelegatingMcpAsyncHttpRequestCustomizer(null)) + .isInstanceOf(IllegalArgumentException.class) + .hasMessage("Customizers must not be null"); + } + +}