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");
+ }
+
+}