Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion MIGRATION-2.0.md
Original file line number Diff line number Diff line change
Expand Up @@ -213,7 +213,7 @@ Schemas that explicitly declare a different dialect via `$schema` are accepted w

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

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

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

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -55,7 +55,7 @@ public void execute(String serverUrl) {

var customizer = new OAuth2ClientCredentialsSyncHttpRequestCustomizer(authorizedClientManager, REGISTRATION_ID);
HttpClientStreamableHttpTransport transport = HttpClientStreamableHttpTransport.builder(serverUrl)
.httpRequestCustomizer(customizer)
.addHttpRequestCustomizer(customizer)
.build();

var client = McpClient.sync(transport)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -8,14 +8,15 @@
import java.net.URI;
import java.net.http.HttpClient;
import java.net.http.HttpRequest;
import java.net.http.HttpResponse;
import java.time.Duration;
import java.util.ArrayList;
import java.util.List;
import java.util.Optional;
import java.util.concurrent.atomic.AtomicReference;
import java.util.function.Consumer;
import java.util.function.Function;

import io.modelcontextprotocol.client.transport.customizer.DelegatingMcpAsyncHttpClientRequestCustomizer;
import io.modelcontextprotocol.client.transport.customizer.McpAsyncHttpClientRequestCustomizer;
import io.modelcontextprotocol.client.transport.customizer.McpSyncHttpClientRequestCustomizer;
import io.modelcontextprotocol.common.McpTransportContext;
Expand Down Expand Up @@ -203,7 +204,7 @@ public static class Builder {

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

private McpAsyncHttpClientRequestCustomizer httpRequestCustomizer = McpAsyncHttpClientRequestCustomizer.NOOP;
private final List<McpAsyncHttpClientRequestCustomizer> httpRequestCustomizers = new ArrayList<>();

private Duration connectTimeout = Duration.ofSeconds(10);

Expand Down Expand Up @@ -286,36 +287,92 @@ public Builder jsonMapper(McpJsonMapper jsonMapper) {

/**
* Sets the customizer for {@link HttpRequest.Builder}, to modify requests before
* executing them.
* executing them. The customizer is internally converted to
* {@link McpAsyncHttpClientRequestCustomizer}.
* <p>
* This overrides the customizer from
* {@link #asyncHttpRequestCustomizer(McpAsyncHttpClientRequestCustomizer)}.
* This replaces all customizers previously registered on this builder.
* <p>
* Do NOT use a blocking {@link McpSyncHttpClientRequestCustomizer} in a
* non-blocking context. Use
* {@link #asyncHttpRequestCustomizer(McpAsyncHttpClientRequestCustomizer)}
* instead.
* @param syncHttpRequestCustomizer the request customizer
* @return this builder
* @deprecated Use {@link #addHttpRequestCustomizer} instead.
*/
@Deprecated
public Builder httpRequestCustomizer(McpSyncHttpClientRequestCustomizer syncHttpRequestCustomizer) {
this.httpRequestCustomizer = McpAsyncHttpClientRequestCustomizer.fromSync(syncHttpRequestCustomizer);
return this;
Assert.notNull(syncHttpRequestCustomizer, "syncHttpRequestCustomizer must not be null");
this.httpRequestCustomizers.clear();
return addHttpRequestCustomizer(syncHttpRequestCustomizer);
}

/**
* Sets the customizer for {@link HttpRequest.Builder}, to modify requests before
* executing them.
* <p>
* This overrides the customizer from
* {@link #httpRequestCustomizer(McpSyncHttpClientRequestCustomizer)}.
* This replaces all customizers previously registered on this builder.
* <p>
* Do NOT use a blocking implementation in a non-blocking context.
* @param asyncHttpRequestCustomizer the request customizer
* @return this builder
* @deprecated Use {@link #addAsyncHttpRequestCustomizer} instead.
*/
@Deprecated
public Builder asyncHttpRequestCustomizer(McpAsyncHttpClientRequestCustomizer asyncHttpRequestCustomizer) {
this.httpRequestCustomizer = asyncHttpRequestCustomizer;
Assert.notNull(asyncHttpRequestCustomizer, "asyncHttpRequestCustomizer must not be null");
this.httpRequestCustomizers.clear();
return addAsyncHttpRequestCustomizer(asyncHttpRequestCustomizer);
}

/**
* Adds a customizer for {@link HttpRequest.Builder}, to modify requests before
* executing them. Customizers are applied in the order they are added, after
* those already registered on this builder. The customizer is internally
* converted to {@link McpAsyncHttpClientRequestCustomizer}.
* <p>
* Do NOT use a blocking {@link McpSyncHttpClientRequestCustomizer} in a
* non-blocking context. Use
* {@link #addAsyncHttpRequestCustomizer(McpAsyncHttpClientRequestCustomizer)}
* instead.
* @param syncHttpRequestCustomizer the request customizer
* @return this builder
*/
public Builder addHttpRequestCustomizer(McpSyncHttpClientRequestCustomizer syncHttpRequestCustomizer) {
Assert.notNull(syncHttpRequestCustomizer, "syncHttpRequestCustomizer must not be null");
this.httpRequestCustomizers.add(McpAsyncHttpClientRequestCustomizer.fromSync(syncHttpRequestCustomizer));
return this;
}

/**
* Adds a customizer for {@link HttpRequest.Builder}, to modify requests before
* executing them. Customizers are applied in the order they are added, after
* those already registered on this builder.
* <p>
* Do NOT use a blocking implementation in a non-blocking context.
* @param asyncHttpRequestCustomizer the request customizer
* @return this builder
*/
public Builder addAsyncHttpRequestCustomizer(McpAsyncHttpClientRequestCustomizer asyncHttpRequestCustomizer) {
Assert.notNull(asyncHttpRequestCustomizer, "asyncHttpRequestCustomizer must not be null");
this.httpRequestCustomizers.add(asyncHttpRequestCustomizer);
return this;
}

/**
* Provides access to the mutable list of request customizers registered on this
* builder, so they can be inspected, reordered, added or removed. Customizers are
* applied in list order. Synchronous customizers registered through
* {@link #httpRequestCustomizer(McpSyncHttpClientRequestCustomizer)} or
* {@link #addHttpRequestCustomizer(McpSyncHttpClientRequestCustomizer)} appear in
* the list wrapped as {@link McpAsyncHttpClientRequestCustomizer}.
* @param customizersConsumer a consumer of the list of customizers
* @return this builder
*/
public Builder asyncHttpRequestCustomizers(
Consumer<List<McpAsyncHttpClientRequestCustomizer>> customizersConsumer) {
Assert.notNull(customizersConsumer, "customizersConsumer must not be null");
customizersConsumer.accept(this.httpRequestCustomizers);
return this;
}

Expand Down Expand Up @@ -369,10 +426,19 @@ public Builder maxResponseSize(int maxResponseSize) {
public HttpClientSseClientTransport build() {
HttpClient httpClient = this.clientBuilder.connectTimeout(this.connectTimeout).build();
return new HttpClientSseClientTransport(httpClient, requestBuilder, baseUri, sseEndpoint,
jsonMapper == null ? McpJsonDefaults.getMapper() : jsonMapper, httpRequestCustomizer,
jsonMapper == null ? McpJsonDefaults.getMapper() : jsonMapper, httpRequestCustomizer(),
messageEndpointValidator, maxResponseSize);
}

private McpAsyncHttpClientRequestCustomizer httpRequestCustomizer() {
Assert.noNullElements(this.httpRequestCustomizers, "httpRequestCustomizers must not contain null elements");
return switch (this.httpRequestCustomizers.size()) {
case 0 -> McpAsyncHttpClientRequestCustomizer.NOOP;
case 1 -> this.httpRequestCustomizers.get(0);
default -> new DelegatingMcpAsyncHttpClientRequestCustomizer(List.copyOf(this.httpRequestCustomizers));
};
}

}

@Override
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@
import java.net.http.HttpResponse;
import java.nio.ByteBuffer;
import java.time.Duration;
import java.util.ArrayList;
import java.util.Collections;
import java.util.Comparator;
import java.util.List;
Expand All @@ -22,6 +23,7 @@
import java.util.function.Function;

import io.modelcontextprotocol.client.McpAsyncClient;
import io.modelcontextprotocol.client.transport.customizer.DelegatingMcpAsyncHttpClientRequestCustomizer;
import io.modelcontextprotocol.client.transport.customizer.McpAsyncHttpClientRequestCustomizer;
import io.modelcontextprotocol.client.transport.customizer.McpHttpClientAuthorizationErrorHandler;
import io.modelcontextprotocol.client.transport.customizer.McpHttpClientTransportAuthorizationErrorHandler;
Expand Down Expand Up @@ -672,7 +674,7 @@ public static class Builder {

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

private McpAsyncHttpClientRequestCustomizer httpRequestCustomizer = McpAsyncHttpClientRequestCustomizer.NOOP;
private final List<McpAsyncHttpClientRequestCustomizer> httpRequestCustomizers = new ArrayList<>();

private Duration connectTimeout = Duration.ofSeconds(10);

Expand Down Expand Up @@ -776,36 +778,92 @@ public Builder openConnectionOnStartup(boolean openConnectionOnStartup) {

/**
* Sets the customizer for {@link HttpRequest.Builder}, to modify requests before
* executing them.
* executing them. The customizer is internally converted to
* {@link McpAsyncHttpClientRequestCustomizer}.
* <p>
* This overrides the customizer from
* {@link #asyncHttpRequestCustomizer(McpAsyncHttpClientRequestCustomizer)}.
* This replaces all customizers previously registered on this builder.
* <p>
* Do NOT use a blocking {@link McpSyncHttpClientRequestCustomizer} in a
* non-blocking context. Use
* {@link #asyncHttpRequestCustomizer(McpAsyncHttpClientRequestCustomizer)}
* instead.
* @param syncHttpRequestCustomizer the request customizer
* @return this builder
* @deprecated Use {@link #addHttpRequestCustomizer} instead.
*/
@Deprecated
public Builder httpRequestCustomizer(McpSyncHttpClientRequestCustomizer syncHttpRequestCustomizer) {
this.httpRequestCustomizer = McpAsyncHttpClientRequestCustomizer.fromSync(syncHttpRequestCustomizer);
return this;
Assert.notNull(syncHttpRequestCustomizer, "syncHttpRequestCustomizer must not be null");
this.httpRequestCustomizers.clear();
return addHttpRequestCustomizer(syncHttpRequestCustomizer);
}

/**
* Sets the customizer for {@link HttpRequest.Builder}, to modify requests before
* executing them.
* <p>
* This overrides the customizer from
* {@link #httpRequestCustomizer(McpSyncHttpClientRequestCustomizer)}.
* This replaces all customizers previously registered on this builder.
* <p>
* Do NOT use a blocking implementation in a non-blocking context.
* @param asyncHttpRequestCustomizer the request customizer
* @return this builder
* @deprecated Use {@link #addAsyncHttpRequestCustomizer} instead.
*/
@Deprecated
public Builder asyncHttpRequestCustomizer(McpAsyncHttpClientRequestCustomizer asyncHttpRequestCustomizer) {
this.httpRequestCustomizer = asyncHttpRequestCustomizer;
Assert.notNull(asyncHttpRequestCustomizer, "asyncHttpRequestCustomizer must not be null");
this.httpRequestCustomizers.clear();
return addAsyncHttpRequestCustomizer(asyncHttpRequestCustomizer);
}

/**
* Adds a customizer for {@link HttpRequest.Builder}, to modify requests before
* executing them. Customizers are applied in the order they are added, after
* those already registered on this builder. The customizer is internally
* converted to {@link McpAsyncHttpClientRequestCustomizer}.
* <p>
* Do NOT use a blocking {@link McpSyncHttpClientRequestCustomizer} in a
* non-blocking context. Use
* {@link #addAsyncHttpRequestCustomizer(McpAsyncHttpClientRequestCustomizer)}
* instead.
* @param syncHttpRequestCustomizer the request customizer
* @return this builder
*/
public Builder addHttpRequestCustomizer(McpSyncHttpClientRequestCustomizer syncHttpRequestCustomizer) {
Assert.notNull(syncHttpRequestCustomizer, "syncHttpRequestCustomizer must not be null");
this.httpRequestCustomizers.add(McpAsyncHttpClientRequestCustomizer.fromSync(syncHttpRequestCustomizer));
return this;
}

/**
* Adds a customizer for {@link HttpRequest.Builder}, to modify requests before
* executing them. Customizers are applied in the order they are added, after
* those already registered on this builder.
* <p>
* Do NOT use a blocking implementation in a non-blocking context.
* @param asyncHttpRequestCustomizer the request customizer
* @return this builder
*/
public Builder addAsyncHttpRequestCustomizer(McpAsyncHttpClientRequestCustomizer asyncHttpRequestCustomizer) {
Assert.notNull(asyncHttpRequestCustomizer, "asyncHttpRequestCustomizer must not be null");
this.httpRequestCustomizers.add(asyncHttpRequestCustomizer);
return this;
}

/**
* Provides access to the mutable list of request customizers registered on this
* builder, so they can be inspected, reordered, added or removed. Customizers are
* applied in list order. Synchronous customizers registered through
* {@link #httpRequestCustomizer(McpSyncHttpClientRequestCustomizer)} or
* {@link #addHttpRequestCustomizer(McpSyncHttpClientRequestCustomizer)} appear in
* the list wrapped as {@link McpAsyncHttpClientRequestCustomizer}.
* @param customizersConsumer a consumer of the list of customizers
* @return this builder
*/
public Builder asyncHttpRequestCustomizers(
Consumer<List<McpAsyncHttpClientRequestCustomizer>> customizersConsumer) {
Assert.notNull(customizersConsumer, "customizersConsumer must not be null");
customizersConsumer.accept(this.httpRequestCustomizers);
return this;
}

Expand Down Expand Up @@ -910,7 +968,16 @@ public HttpClientStreamableHttpTransport build() {
HttpClient httpClient = this.clientBuilder.connectTimeout(this.connectTimeout).build();
return new HttpClientStreamableHttpTransport(jsonMapper == null ? McpJsonDefaults.getMapper() : jsonMapper,
httpClient, requestBuilder, baseUri, endpoint, resumableStreams, openConnectionOnStartup,
httpRequestCustomizer, authorizationErrorHandler, supportedProtocolVersions, maxResponseSize);
httpRequestCustomizer(), authorizationErrorHandler, supportedProtocolVersions, maxResponseSize);
}

private McpAsyncHttpClientRequestCustomizer httpRequestCustomizer() {
Assert.noNullElements(this.httpRequestCustomizers, "httpRequestCustomizers must not contain null elements");
return switch (this.httpRequestCustomizers.size()) {
case 0 -> McpAsyncHttpClientRequestCustomizer.NOOP;
case 1 -> this.httpRequestCustomizers.get(0);
default -> new DelegatingMcpAsyncHttpClientRequestCustomizer(List.copyOf(this.httpRequestCustomizers));
};
}

}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@
import static org.assertj.core.api.Assertions.assertThatThrownBy;
import static org.assertj.core.api.Assertions.fail;
import static org.junit.jupiter.api.Assertions.assertInstanceOf;
import static org.junit.jupiter.api.Assumptions.assumeTrue;

import java.time.Duration;
import java.util.ArrayList;
Expand All @@ -23,6 +24,7 @@
import java.util.function.Consumer;
import java.util.function.Function;

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

abstract protected McpClientTransport createMcpTransport();

// Creates a client transport that applies request customizers, in the order they
// appear in the list. Subclasses whose transport supports
// McpAsyncHttpClientRequestCustomizer should override this; tests relying on it are
// skipped otherwise.
protected McpClientTransport createMcpTransport(
Consumer<List<McpAsyncHttpClientRequestCustomizer>> requestCustomizers) {
return null;
}

protected Duration getRequestTimeout() {
return Duration.ofSeconds(14);
}
Expand Down Expand Up @@ -126,6 +137,30 @@ <T> void verifyCallSucceedsWithImplicitInitialization(Function<McpAsyncClient, M
});
}

@Test
void testRequestCustomizersAreAppliedInOrder() {
var invocations = new CopyOnWriteArrayList<String>();
Function<String, McpAsyncHttpClientRequestCustomizer> recording = name -> (builder, method, endpoint, body,
context) -> {
invocations.add(name);
return Mono.just(builder);
};

var transport = createMcpTransport(customizers -> {
customizers.add(recording.apply("second"));
customizers.add(recording.apply("third"));
customizers.add(0, recording.apply("first"));
});
assumeTrue(transport != null, "Transport does not support request customizers");

withClient(transport, mcpAsyncClient -> {
StepVerifier.create(mcpAsyncClient.initialize()).expectNextCount(1).verifyComplete();
// Later requests may run concurrently, e.g. the Streamable HTTP GET
// stream, so only check the first one.
assertThat(invocations).startsWith("first", "second", "third");
});
}

@Test
void testConstructorWithInvalidArguments() {
assertThatThrownBy(() -> McpClient.async(null).build()).isInstanceOf(IllegalArgumentException.class)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,11 @@

package io.modelcontextprotocol.client;

import java.util.List;
import java.util.function.Consumer;

import io.modelcontextprotocol.client.transport.HttpClientStreamableHttpTransport;
import io.modelcontextprotocol.client.transport.customizer.McpAsyncHttpClientRequestCustomizer;
import io.modelcontextprotocol.spec.McpClientTransport;
import org.junit.jupiter.api.AfterAll;
import org.junit.jupiter.api.BeforeAll;
Expand All @@ -29,6 +33,12 @@ protected McpClientTransport createMcpTransport() {
return HttpClientStreamableHttpTransport.builder(host).build();
}

@Override
protected McpClientTransport createMcpTransport(
Consumer<List<McpAsyncHttpClientRequestCustomizer>> requestCustomizers) {
return HttpClientStreamableHttpTransport.builder(host).asyncHttpRequestCustomizers(requestCustomizers).build();
}

@BeforeAll
static void startContainer() {
container.start();
Expand Down
Loading
Loading