Skip to content

Commit 8590a8d

Browse files
committed
Compose request customizers on the HTTP client transport builders
Fixes #1073 Signed-off-by: Daniel Garnier-Moiroux <git@garnier.wf>
1 parent f856940 commit 8590a8d

16 files changed

Lines changed: 373 additions & 38 deletions

‎MIGRATION-2.0.md‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -213,7 +213,7 @@ Schemas that explicitly declare a different dialect via `$schema` are accepted w
213213

214214
The deprecated `Builder.customizeRequest(Consumer<HttpRequest.Builder>)` method on `HttpClientSseClientTransport` and `HttpClientStreamableHttpTransport` has been removed.
215215

216-
**Action:** Use `requestBuilder(HttpRequest.Builder)` for static request setup, or `httpRequestCustomizer(McpSyncHttpClientRequestCustomizer)` for per-request customization.
216+
**Action:** Use `requestBuilder(HttpRequest.Builder)` for static request setup, or `addHttpRequestCustomizer(McpSyncHttpClientRequestCustomizer)` for per-request customization.
217217

218218
### `protocolVersions()` default now advertises all known versions
219219

‎conformance-tests/client-spring-http-client/src/main/java/io/modelcontextprotocol/conformance/client/scenario/PreRegistrationScenario.java‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -55,7 +55,7 @@ public void execute(String serverUrl) {
5555

5656
var customizer = new OAuth2ClientCredentialsSyncHttpRequestCustomizer(authorizedClientManager, REGISTRATION_ID);
5757
HttpClientStreamableHttpTransport transport = HttpClientStreamableHttpTransport.builder(serverUrl)
58-
.httpRequestCustomizer(customizer)
58+
.addHttpRequestCustomizer(customizer)
5959
.build();
6060

6161
var client = McpClient.sync(transport)

‎mcp-core/src/main/java/io/modelcontextprotocol/client/transport/HttpClientSseClientTransport.java‎

Lines changed: 77 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -8,14 +8,15 @@
88
import java.net.URI;
99
import java.net.http.HttpClient;
1010
import java.net.http.HttpRequest;
11-
import java.net.http.HttpResponse;
1211
import java.time.Duration;
12+
import java.util.ArrayList;
1313
import java.util.List;
1414
import java.util.Optional;
1515
import java.util.concurrent.atomic.AtomicReference;
1616
import java.util.function.Consumer;
1717
import java.util.function.Function;
1818

19+
import io.modelcontextprotocol.client.transport.customizer.DelegatingMcpAsyncHttpClientRequestCustomizer;
1920
import io.modelcontextprotocol.client.transport.customizer.McpAsyncHttpClientRequestCustomizer;
2021
import io.modelcontextprotocol.client.transport.customizer.McpSyncHttpClientRequestCustomizer;
2122
import io.modelcontextprotocol.common.McpTransportContext;
@@ -203,7 +204,7 @@ public static class Builder {
203204

204205
private HttpRequest.Builder requestBuilder = HttpRequest.newBuilder();
205206

206-
private McpAsyncHttpClientRequestCustomizer httpRequestCustomizer = McpAsyncHttpClientRequestCustomizer.NOOP;
207+
private final List<McpAsyncHttpClientRequestCustomizer> httpRequestCustomizers = new ArrayList<>();
207208

208209
private Duration connectTimeout = Duration.ofSeconds(10);
209210

@@ -286,36 +287,92 @@ public Builder jsonMapper(McpJsonMapper jsonMapper) {
286287

287288
/**
288289
* Sets the customizer for {@link HttpRequest.Builder}, to modify requests before
289-
* executing them.
290+
* executing them. The customizer is internally converted to
291+
* {@link McpAsyncHttpClientRequestCustomizer}.
290292
* <p>
291-
* This overrides the customizer from
292-
* {@link #asyncHttpRequestCustomizer(McpAsyncHttpClientRequestCustomizer)}.
293+
* This replaces all customizers previously registered on this builder.
293294
* <p>
294295
* Do NOT use a blocking {@link McpSyncHttpClientRequestCustomizer} in a
295296
* non-blocking context. Use
296297
* {@link #asyncHttpRequestCustomizer(McpAsyncHttpClientRequestCustomizer)}
297298
* instead.
298299
* @param syncHttpRequestCustomizer the request customizer
299300
* @return this builder
301+
* @deprecated Use {@link #addHttpRequestCustomizer} instead.
300302
*/
303+
@Deprecated
301304
public Builder httpRequestCustomizer(McpSyncHttpClientRequestCustomizer syncHttpRequestCustomizer) {
302-
this.httpRequestCustomizer = McpAsyncHttpClientRequestCustomizer.fromSync(syncHttpRequestCustomizer);
303-
return this;
305+
Assert.notNull(syncHttpRequestCustomizer, "syncHttpRequestCustomizer must not be null");
306+
this.httpRequestCustomizers.clear();
307+
return addHttpRequestCustomizer(syncHttpRequestCustomizer);
304308
}
305309

306310
/**
307311
* Sets the customizer for {@link HttpRequest.Builder}, to modify requests before
308312
* executing them.
309313
* <p>
310-
* This overrides the customizer from
311-
* {@link #httpRequestCustomizer(McpSyncHttpClientRequestCustomizer)}.
314+
* This replaces all customizers previously registered on this builder.
312315
* <p>
313316
* Do NOT use a blocking implementation in a non-blocking context.
314317
* @param asyncHttpRequestCustomizer the request customizer
315318
* @return this builder
319+
* @deprecated Use {@link #addAsyncHttpRequestCustomizer} instead.
316320
*/
321+
@Deprecated
317322
public Builder asyncHttpRequestCustomizer(McpAsyncHttpClientRequestCustomizer asyncHttpRequestCustomizer) {
318-
this.httpRequestCustomizer = asyncHttpRequestCustomizer;
323+
Assert.notNull(asyncHttpRequestCustomizer, "asyncHttpRequestCustomizer must not be null");
324+
this.httpRequestCustomizers.clear();
325+
return addAsyncHttpRequestCustomizer(asyncHttpRequestCustomizer);
326+
}
327+
328+
/**
329+
* Adds a customizer for {@link HttpRequest.Builder}, to modify requests before
330+
* executing them. Customizers are applied in the order they are added, after
331+
* those already registered on this builder. The customizer is internally
332+
* converted to {@link McpAsyncHttpClientRequestCustomizer}.
333+
* <p>
334+
* Do NOT use a blocking {@link McpSyncHttpClientRequestCustomizer} in a
335+
* non-blocking context. Use
336+
* {@link #addAsyncHttpRequestCustomizer(McpAsyncHttpClientRequestCustomizer)}
337+
* instead.
338+
* @param syncHttpRequestCustomizer the request customizer
339+
* @return this builder
340+
*/
341+
public Builder addHttpRequestCustomizer(McpSyncHttpClientRequestCustomizer syncHttpRequestCustomizer) {
342+
Assert.notNull(syncHttpRequestCustomizer, "syncHttpRequestCustomizer must not be null");
343+
this.httpRequestCustomizers.add(McpAsyncHttpClientRequestCustomizer.fromSync(syncHttpRequestCustomizer));
344+
return this;
345+
}
346+
347+
/**
348+
* Adds a customizer for {@link HttpRequest.Builder}, to modify requests before
349+
* executing them. Customizers are applied in the order they are added, after
350+
* those already registered on this builder.
351+
* <p>
352+
* Do NOT use a blocking implementation in a non-blocking context.
353+
* @param asyncHttpRequestCustomizer the request customizer
354+
* @return this builder
355+
*/
356+
public Builder addAsyncHttpRequestCustomizer(McpAsyncHttpClientRequestCustomizer asyncHttpRequestCustomizer) {
357+
Assert.notNull(asyncHttpRequestCustomizer, "asyncHttpRequestCustomizer must not be null");
358+
this.httpRequestCustomizers.add(asyncHttpRequestCustomizer);
359+
return this;
360+
}
361+
362+
/**
363+
* Provides access to the mutable list of request customizers registered on this
364+
* builder, so they can be inspected, reordered, added or removed. Customizers are
365+
* applied in list order. Synchronous customizers registered through
366+
* {@link #httpRequestCustomizer(McpSyncHttpClientRequestCustomizer)} or
367+
* {@link #addHttpRequestCustomizer(McpSyncHttpClientRequestCustomizer)} appear in
368+
* the list wrapped as {@link McpAsyncHttpClientRequestCustomizer}.
369+
* @param customizersConsumer a consumer of the list of customizers
370+
* @return this builder
371+
*/
372+
public Builder asyncHttpRequestCustomizers(
373+
Consumer<List<McpAsyncHttpClientRequestCustomizer>> customizersConsumer) {
374+
Assert.notNull(customizersConsumer, "customizersConsumer must not be null");
375+
customizersConsumer.accept(this.httpRequestCustomizers);
319376
return this;
320377
}
321378

@@ -369,10 +426,19 @@ public Builder maxResponseSize(int maxResponseSize) {
369426
public HttpClientSseClientTransport build() {
370427
HttpClient httpClient = this.clientBuilder.connectTimeout(this.connectTimeout).build();
371428
return new HttpClientSseClientTransport(httpClient, requestBuilder, baseUri, sseEndpoint,
372-
jsonMapper == null ? McpJsonDefaults.getMapper() : jsonMapper, httpRequestCustomizer,
429+
jsonMapper == null ? McpJsonDefaults.getMapper() : jsonMapper, httpRequestCustomizer(),
373430
messageEndpointValidator, maxResponseSize);
374431
}
375432

433+
private McpAsyncHttpClientRequestCustomizer httpRequestCustomizer() {
434+
Assert.noNullElements(this.httpRequestCustomizers, "httpRequestCustomizers must not contain null elements");
435+
return switch (this.httpRequestCustomizers.size()) {
436+
case 0 -> McpAsyncHttpClientRequestCustomizer.NOOP;
437+
case 1 -> this.httpRequestCustomizers.get(0);
438+
default -> new DelegatingMcpAsyncHttpClientRequestCustomizer(List.copyOf(this.httpRequestCustomizers));
439+
};
440+
}
441+
376442
}
377443

378444
@Override

‎mcp-core/src/main/java/io/modelcontextprotocol/client/transport/HttpClientStreamableHttpTransport.java‎

Lines changed: 77 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@
1111
import java.net.http.HttpResponse;
1212
import java.nio.ByteBuffer;
1313
import java.time.Duration;
14+
import java.util.ArrayList;
1415
import java.util.Collections;
1516
import java.util.Comparator;
1617
import java.util.List;
@@ -22,6 +23,7 @@
2223
import java.util.function.Function;
2324

2425
import io.modelcontextprotocol.client.McpAsyncClient;
26+
import io.modelcontextprotocol.client.transport.customizer.DelegatingMcpAsyncHttpClientRequestCustomizer;
2527
import io.modelcontextprotocol.client.transport.customizer.McpAsyncHttpClientRequestCustomizer;
2628
import io.modelcontextprotocol.client.transport.customizer.McpHttpClientAuthorizationErrorHandler;
2729
import io.modelcontextprotocol.client.transport.customizer.McpHttpClientTransportAuthorizationErrorHandler;
@@ -672,7 +674,7 @@ public static class Builder {
672674

673675
private HttpRequest.Builder requestBuilder = HttpRequest.newBuilder();
674676

675-
private McpAsyncHttpClientRequestCustomizer httpRequestCustomizer = McpAsyncHttpClientRequestCustomizer.NOOP;
677+
private final List<McpAsyncHttpClientRequestCustomizer> httpRequestCustomizers = new ArrayList<>();
676678

677679
private Duration connectTimeout = Duration.ofSeconds(10);
678680

@@ -776,36 +778,92 @@ public Builder openConnectionOnStartup(boolean openConnectionOnStartup) {
776778

777779
/**
778780
* Sets the customizer for {@link HttpRequest.Builder}, to modify requests before
779-
* executing them.
781+
* executing them. The customizer is internally converted to
782+
* {@link McpAsyncHttpClientRequestCustomizer}.
780783
* <p>
781-
* This overrides the customizer from
782-
* {@link #asyncHttpRequestCustomizer(McpAsyncHttpClientRequestCustomizer)}.
784+
* This replaces all customizers previously registered on this builder.
783785
* <p>
784786
* Do NOT use a blocking {@link McpSyncHttpClientRequestCustomizer} in a
785787
* non-blocking context. Use
786788
* {@link #asyncHttpRequestCustomizer(McpAsyncHttpClientRequestCustomizer)}
787789
* instead.
788790
* @param syncHttpRequestCustomizer the request customizer
789791
* @return this builder
792+
* @deprecated Use {@link #addHttpRequestCustomizer} instead.
790793
*/
794+
@Deprecated
791795
public Builder httpRequestCustomizer(McpSyncHttpClientRequestCustomizer syncHttpRequestCustomizer) {
792-
this.httpRequestCustomizer = McpAsyncHttpClientRequestCustomizer.fromSync(syncHttpRequestCustomizer);
793-
return this;
796+
Assert.notNull(syncHttpRequestCustomizer, "syncHttpRequestCustomizer must not be null");
797+
this.httpRequestCustomizers.clear();
798+
return addHttpRequestCustomizer(syncHttpRequestCustomizer);
794799
}
795800

796801
/**
797802
* Sets the customizer for {@link HttpRequest.Builder}, to modify requests before
798803
* executing them.
799804
* <p>
800-
* This overrides the customizer from
801-
* {@link #httpRequestCustomizer(McpSyncHttpClientRequestCustomizer)}.
805+
* This replaces all customizers previously registered on this builder.
802806
* <p>
803807
* Do NOT use a blocking implementation in a non-blocking context.
804808
* @param asyncHttpRequestCustomizer the request customizer
805809
* @return this builder
810+
* @deprecated Use {@link #addAsyncHttpRequestCustomizer} instead.
806811
*/
812+
@Deprecated
807813
public Builder asyncHttpRequestCustomizer(McpAsyncHttpClientRequestCustomizer asyncHttpRequestCustomizer) {
808-
this.httpRequestCustomizer = asyncHttpRequestCustomizer;
814+
Assert.notNull(asyncHttpRequestCustomizer, "asyncHttpRequestCustomizer must not be null");
815+
this.httpRequestCustomizers.clear();
816+
return addAsyncHttpRequestCustomizer(asyncHttpRequestCustomizer);
817+
}
818+
819+
/**
820+
* Adds a customizer for {@link HttpRequest.Builder}, to modify requests before
821+
* executing them. Customizers are applied in the order they are added, after
822+
* those already registered on this builder. The customizer is internally
823+
* converted to {@link McpAsyncHttpClientRequestCustomizer}.
824+
* <p>
825+
* Do NOT use a blocking {@link McpSyncHttpClientRequestCustomizer} in a
826+
* non-blocking context. Use
827+
* {@link #addAsyncHttpRequestCustomizer(McpAsyncHttpClientRequestCustomizer)}
828+
* instead.
829+
* @param syncHttpRequestCustomizer the request customizer
830+
* @return this builder
831+
*/
832+
public Builder addHttpRequestCustomizer(McpSyncHttpClientRequestCustomizer syncHttpRequestCustomizer) {
833+
Assert.notNull(syncHttpRequestCustomizer, "syncHttpRequestCustomizer must not be null");
834+
this.httpRequestCustomizers.add(McpAsyncHttpClientRequestCustomizer.fromSync(syncHttpRequestCustomizer));
835+
return this;
836+
}
837+
838+
/**
839+
* Adds a customizer for {@link HttpRequest.Builder}, to modify requests before
840+
* executing them. Customizers are applied in the order they are added, after
841+
* those already registered on this builder.
842+
* <p>
843+
* Do NOT use a blocking implementation in a non-blocking context.
844+
* @param asyncHttpRequestCustomizer the request customizer
845+
* @return this builder
846+
*/
847+
public Builder addAsyncHttpRequestCustomizer(McpAsyncHttpClientRequestCustomizer asyncHttpRequestCustomizer) {
848+
Assert.notNull(asyncHttpRequestCustomizer, "asyncHttpRequestCustomizer must not be null");
849+
this.httpRequestCustomizers.add(asyncHttpRequestCustomizer);
850+
return this;
851+
}
852+
853+
/**
854+
* Provides access to the mutable list of request customizers registered on this
855+
* builder, so they can be inspected, reordered, added or removed. Customizers are
856+
* applied in list order. Synchronous customizers registered through
857+
* {@link #httpRequestCustomizer(McpSyncHttpClientRequestCustomizer)} or
858+
* {@link #addHttpRequestCustomizer(McpSyncHttpClientRequestCustomizer)} appear in
859+
* the list wrapped as {@link McpAsyncHttpClientRequestCustomizer}.
860+
* @param customizersConsumer a consumer of the list of customizers
861+
* @return this builder
862+
*/
863+
public Builder asyncHttpRequestCustomizers(
864+
Consumer<List<McpAsyncHttpClientRequestCustomizer>> customizersConsumer) {
865+
Assert.notNull(customizersConsumer, "customizersConsumer must not be null");
866+
customizersConsumer.accept(this.httpRequestCustomizers);
809867
return this;
810868
}
811869

@@ -910,7 +968,16 @@ public HttpClientStreamableHttpTransport build() {
910968
HttpClient httpClient = this.clientBuilder.connectTimeout(this.connectTimeout).build();
911969
return new HttpClientStreamableHttpTransport(jsonMapper == null ? McpJsonDefaults.getMapper() : jsonMapper,
912970
httpClient, requestBuilder, baseUri, endpoint, resumableStreams, openConnectionOnStartup,
913-
httpRequestCustomizer, authorizationErrorHandler, supportedProtocolVersions, maxResponseSize);
971+
httpRequestCustomizer(), authorizationErrorHandler, supportedProtocolVersions, maxResponseSize);
972+
}
973+
974+
private McpAsyncHttpClientRequestCustomizer httpRequestCustomizer() {
975+
Assert.noNullElements(this.httpRequestCustomizers, "httpRequestCustomizers must not contain null elements");
976+
return switch (this.httpRequestCustomizers.size()) {
977+
case 0 -> McpAsyncHttpClientRequestCustomizer.NOOP;
978+
case 1 -> this.httpRequestCustomizers.get(0);
979+
default -> new DelegatingMcpAsyncHttpClientRequestCustomizer(List.copyOf(this.httpRequestCustomizers));
980+
};
914981
}
915982

916983
}

‎mcp-test/src/main/java/io/modelcontextprotocol/client/AbstractMcpAsyncClientTests.java‎

Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@
1010
import static org.assertj.core.api.Assertions.assertThatThrownBy;
1111
import static org.assertj.core.api.Assertions.fail;
1212
import static org.junit.jupiter.api.Assertions.assertInstanceOf;
13+
import static org.junit.jupiter.api.Assumptions.assumeTrue;
1314

1415
import java.time.Duration;
1516
import java.util.ArrayList;
@@ -23,6 +24,7 @@
2324
import java.util.function.Consumer;
2425
import java.util.function.Function;
2526

27+
import io.modelcontextprotocol.client.transport.customizer.McpAsyncHttpClientRequestCustomizer;
2628
import io.modelcontextprotocol.spec.McpSchema.ElicitFormRequest;
2729
import org.junit.jupiter.api.Test;
2830
import org.junit.jupiter.params.ParameterizedTest;
@@ -67,6 +69,15 @@ public abstract class AbstractMcpAsyncClientTests {
6769

6870
abstract protected McpClientTransport createMcpTransport();
6971

72+
// Creates a client transport that applies request customizers, in the order they
73+
// appear in the list. Subclasses whose transport supports
74+
// McpAsyncHttpClientRequestCustomizer should override this; tests relying on it are
75+
// skipped otherwise.
76+
protected McpClientTransport createMcpTransport(
77+
Consumer<List<McpAsyncHttpClientRequestCustomizer>> requestCustomizers) {
78+
return null;
79+
}
80+
7081
protected Duration getRequestTimeout() {
7182
return Duration.ofSeconds(14);
7283
}
@@ -126,6 +137,30 @@ <T> void verifyCallSucceedsWithImplicitInitialization(Function<McpAsyncClient, M
126137
});
127138
}
128139

140+
@Test
141+
void testRequestCustomizersAreAppliedInOrder() {
142+
var invocations = new CopyOnWriteArrayList<String>();
143+
Function<String, McpAsyncHttpClientRequestCustomizer> recording = name -> (builder, method, endpoint, body,
144+
context) -> {
145+
invocations.add(name);
146+
return Mono.just(builder);
147+
};
148+
149+
var transport = createMcpTransport(customizers -> {
150+
customizers.add(recording.apply("second"));
151+
customizers.add(recording.apply("third"));
152+
customizers.add(0, recording.apply("first"));
153+
});
154+
assumeTrue(transport != null, "Transport does not support request customizers");
155+
156+
withClient(transport, mcpAsyncClient -> {
157+
StepVerifier.create(mcpAsyncClient.initialize()).expectNextCount(1).verifyComplete();
158+
// Later requests may run concurrently, e.g. the Streamable HTTP GET
159+
// stream, so only check the first one.
160+
assertThat(invocations).startsWith("first", "second", "third");
161+
});
162+
}
163+
129164
@Test
130165
void testConstructorWithInvalidArguments() {
131166
assertThatThrownBy(() -> McpClient.async(null).build()).isInstanceOf(IllegalArgumentException.class)

‎mcp-test/src/test/java/io/modelcontextprotocol/client/HttpClientStreamableHttpAsyncClientTests.java‎

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,11 @@
44

55
package io.modelcontextprotocol.client;
66

7+
import java.util.List;
8+
import java.util.function.Consumer;
9+
710
import io.modelcontextprotocol.client.transport.HttpClientStreamableHttpTransport;
11+
import io.modelcontextprotocol.client.transport.customizer.McpAsyncHttpClientRequestCustomizer;
812
import io.modelcontextprotocol.spec.McpClientTransport;
913
import org.junit.jupiter.api.AfterAll;
1014
import org.junit.jupiter.api.BeforeAll;
@@ -29,6 +33,12 @@ protected McpClientTransport createMcpTransport() {
2933
return HttpClientStreamableHttpTransport.builder(host).build();
3034
}
3135

36+
@Override
37+
protected McpClientTransport createMcpTransport(
38+
Consumer<List<McpAsyncHttpClientRequestCustomizer>> requestCustomizers) {
39+
return HttpClientStreamableHttpTransport.builder(host).asyncHttpRequestCustomizers(requestCustomizers).build();
40+
}
41+
3242
@BeforeAll
3343
static void startContainer() {
3444
container.start();

0 commit comments

Comments
 (0)