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 473f71fbb..f78e8e97d 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 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.McpError; @@ -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. @@ -187,7 +189,7 @@ public HttpClientSseClientTransport(HttpClient.Builder clientBuilder, HttpReques @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); } /** @@ -203,7 +205,7 @@ public HttpClientSseClientTransport(HttpClient.Builder clientBuilder, HttpReques * @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"); @@ -250,7 +252,7 @@ public static class Builder { private HttpRequest.Builder requestBuilder = HttpRequest.newBuilder() .header("Content-Type", "application/json"); - private AsyncHttpRequestCustomizer httpRequestCustomizer = AsyncHttpRequestCustomizer.NOOP; + private McpAsyncHttpRequestCustomizer httpRequestCustomizer = McpAsyncHttpRequestCustomizer.NOOP; /** * Creates a new builder instance. @@ -354,16 +356,16 @@ public Builder objectMapper(ObjectMapper objectMapper) { * 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; } @@ -372,13 +374,13 @@ public Builder httpRequestCustomizer(SyncHttpRequestCustomizer syncHttpRequestCu * 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 3cfa7359b..515a816cb 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 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; @@ -567,7 +569,7 @@ public static class Builder { private HttpRequest.Builder requestBuilder = HttpRequest.newBuilder(); - private AsyncHttpRequestCustomizer httpRequestCustomizer = AsyncHttpRequestCustomizer.NOOP; + private McpAsyncHttpRequestCustomizer httpRequestCustomizer = McpAsyncHttpRequestCustomizer.NOOP; /** * Creates a new builder with the specified base URI. @@ -676,16 +678,16 @@ public Builder openConnectionOnStartup(boolean openConnectionOnStartup) { * 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; } @@ -694,13 +696,13 @@ public Builder httpRequestCustomizer(SyncHttpRequestCustomizer syncHttpRequestCu * 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 000000000..22ba6a265 --- /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 000000000..65649d916 --- /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 dee026d96..2f685c350 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 @@ * * @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 @@ Publisher customize(HttpRequest.Builder builder, String met * 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 72b6e6c1b..8d2c4a698 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 @@ * * @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 46b9207f6..f5a5ecb12 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.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 @@ static class TestHttpClientSseClientTransport extends HttpClientSseClientTranspo 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 @@ void testChainedCustomizations() { @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 @@ void testRequestCustomizer() { @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/HttpClientStreamableHttpTransportTest.java b/mcp/src/test/java/io/modelcontextprotocol/client/transport/HttpClientStreamableHttpTransportTest.java index 479468f63..91ff5eaeb 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 @@ void withTransport(HttpClientStreamableHttpTransport transport, Consumer 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 000000000..f136cd65e --- /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 000000000..427472912 --- /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"); + } + +}