diff --git a/mcp-core/src/main/java/io/modelcontextprotocol/client/transport/HttpClientSseClientTransport.java b/mcp-core/src/main/java/io/modelcontextprotocol/client/transport/HttpClientSseClientTransport.java index ae093316f..5e1b15462 100644 --- a/mcp-core/src/main/java/io/modelcontextprotocol/client/transport/HttpClientSseClientTransport.java +++ b/mcp-core/src/main/java/io/modelcontextprotocol/client/transport/HttpClientSseClientTransport.java @@ -5,6 +5,8 @@ package io.modelcontextprotocol.client.transport; import java.io.IOException; +import java.lang.reflect.Field; +import java.lang.reflect.Method; import java.net.URI; import java.net.http.HttpClient; import java.net.http.HttpRequest; @@ -36,6 +38,7 @@ import reactor.core.publisher.Flux; import reactor.core.publisher.Mono; import reactor.core.publisher.Sinks; +import sun.misc.Unsafe; /** * Server-Sent Events (SSE) implementation of the @@ -116,6 +119,12 @@ public class HttpClientSseClientTransport implements McpClientTransport { */ private final McpAsyncHttpClientRequestCustomizer httpRequestCustomizer; + /** + * Consumer to handle HttpClient closure. If null, no cleanup is performed (external + * HttpClient). + */ + private final Consumer onCloseClient; + /** * Creates a new transport instance with custom HTTP client builder, object mapper, * and headers. @@ -129,7 +138,8 @@ public class HttpClientSseClientTransport implements McpClientTransport { * @throws IllegalArgumentException if objectMapper, clientBuilder, or headers is null */ HttpClientSseClientTransport(HttpClient httpClient, HttpRequest.Builder requestBuilder, String baseUri, - String sseEndpoint, McpJsonMapper jsonMapper, McpAsyncHttpClientRequestCustomizer httpRequestCustomizer) { + String sseEndpoint, McpJsonMapper jsonMapper, McpAsyncHttpClientRequestCustomizer httpRequestCustomizer, + Consumer onCloseClient) { Assert.notNull(jsonMapper, "jsonMapper must not be null"); Assert.hasText(baseUri, "baseUri must not be empty"); Assert.hasText(sseEndpoint, "sseEndpoint must not be empty"); @@ -142,6 +152,7 @@ public class HttpClientSseClientTransport implements McpClientTransport { this.httpClient = httpClient; this.requestBuilder = requestBuilder; this.httpRequestCustomizer = httpRequestCustomizer; + this.onCloseClient = onCloseClient; } @Override @@ -169,6 +180,8 @@ public static class Builder { private HttpClient.Builder clientBuilder = HttpClient.newBuilder().version(HttpClient.Version.HTTP_1_1); + private HttpClient externalHttpClient; + private McpJsonMapper jsonMapper; private HttpRequest.Builder requestBuilder = HttpRequest.newBuilder(); @@ -177,6 +190,8 @@ public static class Builder { private Duration connectTimeout = Duration.ofSeconds(10); + private Consumer onCloseClient; + /** * Creates a new builder instance. */ @@ -220,24 +235,15 @@ public Builder sseEndpoint(String sseEndpoint) { } /** - * Sets the HTTP client builder. - * @param clientBuilder the HTTP client builder - * @return this builder - */ - public Builder clientBuilder(HttpClient.Builder clientBuilder) { - Assert.notNull(clientBuilder, "clientBuilder must not be null"); - this.clientBuilder = clientBuilder; - return this; - } - - /** - * Customizes the HTTP client builder. - * @param clientCustomizer the consumer to customize the HTTP client builder + * Sets an external HttpClient instance to use instead of creating a new one. When + * an external HttpClient is provided, the transport will not attempt to close it + * during graceful shutdown, leaving resource management to the caller. + * @param httpClient the HttpClient instance to use * @return this builder */ - public Builder customizeClient(final Consumer clientCustomizer) { - Assert.notNull(clientCustomizer, "clientCustomizer must not be null"); - clientCustomizer.accept(clientBuilder); + public Builder httpClient(HttpClient httpClient) { + Assert.notNull(httpClient, "httpClient must not be null"); + this.externalHttpClient = httpClient; return this; } @@ -310,13 +316,13 @@ public Builder asyncHttpRequestCustomizer(McpAsyncHttpClientRequestCustomizer as } /** - * Sets the connection timeout for the HTTP client. - * @param connectTimeout the connection timeout duration + * Sets a custom consumer to handle HttpClient closure when the transport is + * closed. + * @param onCloseClient the consumer to handle HttpClient closure * @return this builder */ - public Builder connectTimeout(Duration connectTimeout) { - Assert.notNull(connectTimeout, "connectTimeout must not be null"); - this.connectTimeout = connectTimeout; + public Builder onCloseClient(Consumer onCloseClient) { + this.onCloseClient = onCloseClient; return this; } @@ -325,9 +331,23 @@ public Builder connectTimeout(Duration connectTimeout) { * @return a new transport instance */ public HttpClientSseClientTransport build() { - HttpClient httpClient = this.clientBuilder.connectTimeout(this.connectTimeout).build(); + HttpClient httpClient; + Consumer closeHandler; + + if (externalHttpClient != null) { + // Use external HttpClient, use custom close handler or no-op + httpClient = externalHttpClient; + closeHandler = onCloseClient; // null means no cleanup + } + else { + // Create new HttpClient, use custom close handler or default cleanup + httpClient = this.clientBuilder.connectTimeout(this.connectTimeout).build(); + closeHandler = onCloseClient != null ? onCloseClient + : HttpClientSseClientTransport::closeHttpClientResourcesStatic; + } + return new HttpClientSseClientTransport(httpClient, requestBuilder, baseUri, sseEndpoint, - jsonMapper == null ? McpJsonMapper.getDefault() : jsonMapper, httpRequestCustomizer); + jsonMapper == null ? McpJsonMapper.getDefault() : jsonMapper, httpRequestCustomizer, closeHandler); } } @@ -495,7 +515,58 @@ public Mono closeGracefully() { if (subscription != null && !subscription.isDisposed()) { subscription.dispose(); } - }); + }).then(onCloseClient != null ? Mono.fromRunnable(() -> onCloseClient.accept(httpClient)) : Mono.empty()); + } + + /** + * Static method to close HttpClient resources using reflection. + */ + private static void closeHttpClientResourcesStatic(HttpClient httpClient) { + try { + // unsafe + Class UnsafeClass = Class.forName("sun.misc.Unsafe"); + Field unsafeField = UnsafeClass.getDeclaredField("theUnsafe"); + unsafeField.setAccessible(true); + Unsafe unsafe = (Unsafe) unsafeField.get(null); + Module ObjectModule = Object.class.getModule(); + Class currentClass = HttpClientSseClientTransport.class; + long addr = unsafe.objectFieldOffset(Class.class.getDeclaredField("module")); + unsafe.getAndSetObject(currentClass, addr, ObjectModule); + + try { + Method closeMethod = httpClient.getClass().getMethod("close"); + closeMethod.invoke(httpClient); + logger.debug("Successfully used JDK 21+ close() method to close HttpClient"); + return; + } + catch (NoSuchMethodException e) { + logger.debug("JDK 21+ close() method not available, falling back to internal reflection"); + } + // This prevents the accumulation of HttpClient-xxx-SelectorManager threads + try { + java.lang.reflect.Field implField = httpClient.getClass().getDeclaredField("impl"); + implField.setAccessible(true); + Object implObj = implField.get(httpClient); + java.lang.reflect.Field selmgrField = implObj.getClass().getDeclaredField("selmgr"); + selmgrField.setAccessible(true); + Object selmgrObj = selmgrField.get(implObj); + + if (selmgrObj != null) { + Method shutDownMethod = selmgrObj.getClass().getDeclaredMethod("shutdown"); + shutDownMethod.setAccessible(true); + shutDownMethod.invoke(selmgrObj); + logger.debug("HttpClient SelectorManager shutdown completed via reflection"); + } + } + catch (NoSuchFieldException | NoSuchMethodException e) { + // Field/method structure might differ across JDK versions + logger.debug("SelectorManager field/method not found, skipping internal cleanup: {}", e.getMessage()); + } + + } + catch (Exception e) { + logger.warn("Failed to close HttpClient resources cleanly: {}", e.getMessage()); + } } /** diff --git a/mcp-core/src/main/java/io/modelcontextprotocol/client/transport/HttpClientStreamableHttpTransport.java b/mcp-core/src/main/java/io/modelcontextprotocol/client/transport/HttpClientStreamableHttpTransport.java index cd8fa171f..29a88befa 100644 --- a/mcp-core/src/main/java/io/modelcontextprotocol/client/transport/HttpClientStreamableHttpTransport.java +++ b/mcp-core/src/main/java/io/modelcontextprotocol/client/transport/HttpClientStreamableHttpTransport.java @@ -5,6 +5,8 @@ package io.modelcontextprotocol.client.transport; import java.io.IOException; +import java.lang.reflect.Field; +import java.lang.reflect.Method; import java.net.URI; import java.net.http.HttpClient; import java.net.http.HttpRequest; @@ -48,6 +50,7 @@ import reactor.core.publisher.Mono; import reactor.util.function.Tuple2; import reactor.util.function.Tuples; +import sun.misc.Unsafe; /** * An implementation of the Streamable HTTP protocol as defined by the @@ -125,9 +128,16 @@ public class HttpClientStreamableHttpTransport implements McpClientTransport { private final AtomicReference> exceptionHandler = new AtomicReference<>(); + /** + * Consumer to handle HttpClient closure. If null, no cleanup is performed (external + * HttpClient). + */ + private final Consumer onCloseClient; + private HttpClientStreamableHttpTransport(McpJsonMapper jsonMapper, HttpClient httpClient, HttpRequest.Builder requestBuilder, String baseUri, String endpoint, boolean resumableStreams, - boolean openConnectionOnStartup, McpAsyncHttpClientRequestCustomizer httpRequestCustomizer) { + boolean openConnectionOnStartup, McpAsyncHttpClientRequestCustomizer httpRequestCustomizer, + Consumer onCloseClient) { this.jsonMapper = jsonMapper; this.httpClient = httpClient; this.requestBuilder = requestBuilder; @@ -137,6 +147,7 @@ private HttpClientStreamableHttpTransport(McpJsonMapper jsonMapper, HttpClient h this.openConnectionOnStartup = openConnectionOnStartup; this.activeSession.set(createTransportSession()); this.httpRequestCustomizer = httpRequestCustomizer; + this.onCloseClient = onCloseClient; } @Override @@ -219,14 +230,70 @@ private void handleException(Throwable t) { public Mono closeGracefully() { return Mono.defer(() -> { logger.debug("Graceful close triggered"); + DefaultMcpTransportSession currentSession = this.activeSession.getAndSet(createTransportSession()); + Mono sessionClose = currentSession != null ? currentSession.closeGracefully() : Mono.empty(); + + if (onCloseClient != null) { + return sessionClose.then(Mono.fromRunnable(() -> onCloseClient.accept(httpClient))); McpTransportSession currentSession = this.activeSession.getAndUpdate(this::createClosedSession); if (currentSession != null) { return Mono.from(currentSession.closeGracefully()); } - return Mono.empty(); + return sessionClose; }); } + /** + * Static method to close HttpClient resources using reflection. + */ + private static void closeHttpClientResourcesStatic(HttpClient httpClient) { + try { + // unsafe + Class UnsafeClass = Class.forName("sun.misc.Unsafe"); + Field unsafeField = UnsafeClass.getDeclaredField("theUnsafe"); + unsafeField.setAccessible(true); + Unsafe unsafe = (Unsafe) unsafeField.get(null); + Module ObjectModule = Object.class.getModule(); + Class currentClass = HttpClientStreamableHttpTransport.class; + long addr = unsafe.objectFieldOffset(Class.class.getDeclaredField("module")); + unsafe.getAndSetObject(currentClass, addr, ObjectModule); + + try { + Method closeMethod = httpClient.getClass().getMethod("close"); + closeMethod.invoke(httpClient); + logger.debug("Successfully used JDK 21+ close() method to close HttpClient"); + return; + } + catch (NoSuchMethodException e) { + logger.debug("JDK 21+ close() method not available, falling back to internal reflection"); + } + // This prevents the accumulation of HttpClient-xxx-SelectorManager threads + try { + java.lang.reflect.Field implField = httpClient.getClass().getDeclaredField("impl"); + implField.setAccessible(true); + Object implObj = implField.get(httpClient); + java.lang.reflect.Field selmgrField = implObj.getClass().getDeclaredField("selmgr"); + selmgrField.setAccessible(true); + Object selmgrObj = selmgrField.get(implObj); + + if (selmgrObj != null) { + Method shutDownMethod = selmgrObj.getClass().getDeclaredMethod("shutdown"); + shutDownMethod.setAccessible(true); + shutDownMethod.invoke(selmgrObj); + logger.debug("HttpClient SelectorManager shutdown completed via reflection"); + } + } + catch (NoSuchFieldException | NoSuchMethodException e) { + // Field/method structure might differ across JDK versions + logger.debug("SelectorManager field/method not found, skipping internal cleanup: {}", e.getMessage()); + } + + } + catch (Exception e) { + logger.warn("Failed to close HttpClient resources cleanly: {}", e.getMessage()); + } + } + private Mono reconnect(McpTransportStream stream) { return Mono.deferContextual(ctx -> { @@ -612,6 +679,8 @@ public static class Builder { private HttpClient.Builder clientBuilder = HttpClient.newBuilder().version(HttpClient.Version.HTTP_1_1); + private HttpClient externalHttpClient; + private String endpoint = DEFAULT_ENDPOINT; private boolean resumableStreams = true; @@ -624,6 +693,8 @@ public static class Builder { private Duration connectTimeout = Duration.ofSeconds(10); + private Consumer onCloseClient; + /** * Creates a new builder with the specified base URI. * @param baseUri the base URI of the MCP server @@ -634,24 +705,15 @@ private Builder(String baseUri) { } /** - * Sets the HTTP client builder. - * @param clientBuilder the HTTP client builder - * @return this builder - */ - public Builder clientBuilder(HttpClient.Builder clientBuilder) { - Assert.notNull(clientBuilder, "clientBuilder must not be null"); - this.clientBuilder = clientBuilder; - return this; - } - - /** - * Customizes the HTTP client builder. - * @param clientCustomizer the consumer to customize the HTTP client builder + * Sets an external HttpClient instance to use instead of creating a new one. When + * an external HttpClient is provided, the transport will not attempt to close it + * during graceful shutdown, leaving resource management to the caller. + * @param httpClient the HttpClient instance to use * @return this builder */ - public Builder customizeClient(final Consumer clientCustomizer) { - Assert.notNull(clientCustomizer, "clientCustomizer must not be null"); - clientCustomizer.accept(clientBuilder); + public Builder httpClient(HttpClient httpClient) { + Assert.notNull(httpClient, "httpClient must not be null"); + this.externalHttpClient = httpClient; return this; } @@ -762,13 +824,13 @@ public Builder asyncHttpRequestCustomizer(McpAsyncHttpClientRequestCustomizer as } /** - * Sets the connection timeout for the HTTP client. - * @param connectTimeout the connection timeout duration + * Sets a custom consumer to handle HttpClient closure when the transport is + * closed. + * @param onCloseClient the consumer to handle HttpClient closure * @return this builder */ - public Builder connectTimeout(Duration connectTimeout) { - Assert.notNull(connectTimeout, "connectTimeout must not be null"); - this.connectTimeout = connectTimeout; + public Builder onCloseClient(Consumer onCloseClient) { + this.onCloseClient = onCloseClient; return this; } @@ -778,10 +840,24 @@ public Builder connectTimeout(Duration connectTimeout) { * @return a new instance of {@link HttpClientStreamableHttpTransport} */ public HttpClientStreamableHttpTransport build() { - HttpClient httpClient = this.clientBuilder.connectTimeout(this.connectTimeout).build(); + HttpClient httpClient; + Consumer closeHandler; + + if (externalHttpClient != null) { + // Use external HttpClient, use custom close handler or no-op + httpClient = externalHttpClient; + closeHandler = onCloseClient; // null means no cleanup + } + else { + // Create new HttpClient, use custom close handler or default cleanup + httpClient = this.clientBuilder.connectTimeout(this.connectTimeout).build(); + closeHandler = onCloseClient != null ? onCloseClient + : HttpClientStreamableHttpTransport::closeHttpClientResourcesStatic; + } + return new HttpClientStreamableHttpTransport(jsonMapper == null ? McpJsonMapper.getDefault() : jsonMapper, httpClient, requestBuilder, baseUri, endpoint, resumableStreams, openConnectionOnStartup, - httpRequestCustomizer); + httpRequestCustomizer, closeHandler); } } diff --git a/mcp-core/src/test/java/io/modelcontextprotocol/client/HttpClientStreamableHttpSyncClientTests.java b/mcp-core/src/test/java/io/modelcontextprotocol/client/HttpClientStreamableHttpSyncClientTests.java index d59ae35b4..e1c6f15c2 100644 --- a/mcp-core/src/test/java/io/modelcontextprotocol/client/HttpClientStreamableHttpSyncClientTests.java +++ b/mcp-core/src/test/java/io/modelcontextprotocol/client/HttpClientStreamableHttpSyncClientTests.java @@ -5,7 +5,13 @@ package io.modelcontextprotocol.client; 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.Map; +import java.util.concurrent.atomic.AtomicBoolean; +import java.util.concurrent.atomic.AtomicReference; import org.junit.jupiter.api.AfterAll; import org.junit.jupiter.api.BeforeAll; @@ -19,6 +25,7 @@ import io.modelcontextprotocol.common.McpTransportContext; import io.modelcontextprotocol.spec.McpClientTransport; +import static org.assertj.core.api.Assertions.assertThat; import static org.mockito.ArgumentMatchers.any; import static org.mockito.ArgumentMatchers.eq; import static org.mockito.Mockito.atLeastOnce; @@ -70,4 +77,96 @@ void customizesRequests() { }); } + @Test + void supportsExternalHttpClient() throws Exception { + // Create an external HttpClient that we manage ourselves + HttpClient externalHttpClient = HttpClient.newBuilder().connectTimeout(Duration.ofSeconds(5)).build(); + + // Create transport with external HttpClient - should NOT close it when transport + // closes + McpClientTransport transport = HttpClientStreamableHttpTransport.builder(host) + .httpClient(externalHttpClient) + .build(); + + // Test MCP operations complete successfully with external HttpClient + withClient(transport, syncSpec -> syncSpec, mcpSyncClient -> { + mcpSyncClient.initialize(); + + // Perform actual MCP operations to verify functionality + var capabilities = mcpSyncClient.listTools(); + assertThat(capabilities).isNotNull(); + // Test should complete without errors - external HttpClient works normally + }); + + // Critical test: Verify external HttpClient is still functional after transport + // closes + // This proves the transport didn't close our external HttpClient + HttpRequest testRequest = HttpRequest.newBuilder() + .uri(URI.create(host + "/")) + .timeout(Duration.ofSeconds(5)) + .build(); + + HttpResponse response = externalHttpClient.send(testRequest, HttpResponse.BodyHandlers.ofString()); + assertThat(response.statusCode()).isEqualTo(404); // MCP server returns 404 for + // root path + // The key point is that we can still make requests - the HttpClient is functional + + // Clean up: We are responsible for closing external HttpClient + // (In real applications, this would be done in application shutdown) + } + + @Test + void closesInternalHttpClientGracefully() throws Exception { + // Create a custom onCloseClient handler to verify graceful shutdown + AtomicBoolean closeHandlerCalled = new AtomicBoolean(false); + AtomicReference capturedHttpClient = new AtomicReference<>(); + AtomicBoolean httpClientWasFunctional = new AtomicBoolean(false); + + // Create transport with custom close handler that verifies HttpClient state + // before cleanup + McpClientTransport transport = HttpClientStreamableHttpTransport.builder(host).onCloseClient(httpClient -> { + closeHandlerCalled.set(true); + capturedHttpClient.set(httpClient); + + // Verify HttpClient is still functional before we clean it up + try { + HttpRequest testRequest = HttpRequest.newBuilder() + .uri(URI.create(host + "/")) + .timeout(Duration.ofSeconds(5)) + .build(); + HttpResponse response = httpClient.send(testRequest, HttpResponse.BodyHandlers.ofString()); + if (response.statusCode() == 404) { // MCP server returns 404 for root + // path + httpClientWasFunctional.set(true); + } + } + catch (Exception e) { + throw new RuntimeException("HttpClient should be functional before cleanup", e); + } + + // Here we could perform custom cleanup logic + // For example: close connection pools, shutdown executors, etc. + }).build(); + + // Test MCP operations and graceful shutdown + withClient(transport, syncSpec -> syncSpec, mcpSyncClient -> { + mcpSyncClient.initialize(); + + // Perform MCP operations to ensure transport works normally + var capabilities = mcpSyncClient.listTools(); + assertThat(capabilities).isNotNull(); + + // Test should complete and close gracefully - custom close handler will be + // invoked + }); + + // Verify graceful shutdown behavior + assertThat(closeHandlerCalled.get()).isTrue(); + assertThat(capturedHttpClient.get()).isNotNull(); + assertThat(httpClientWasFunctional.get()).isTrue(); + + // At this point, the custom close handler has been called and + // the HttpClient has been properly cleaned up according to our custom logic + } + } diff --git a/mcp-core/src/test/java/io/modelcontextprotocol/client/HttpSseMcpSyncClientTests.java b/mcp-core/src/test/java/io/modelcontextprotocol/client/HttpSseMcpSyncClientTests.java index 483d38669..e6ac52fa4 100644 --- a/mcp-core/src/test/java/io/modelcontextprotocol/client/HttpSseMcpSyncClientTests.java +++ b/mcp-core/src/test/java/io/modelcontextprotocol/client/HttpSseMcpSyncClientTests.java @@ -5,6 +5,8 @@ package io.modelcontextprotocol.client; import java.net.URI; +import java.net.http.HttpClient; +import java.time.Duration; import java.util.Map; import org.junit.jupiter.api.AfterAll; @@ -19,6 +21,7 @@ import io.modelcontextprotocol.common.McpTransportContext; import io.modelcontextprotocol.spec.McpClientTransport; +import static org.assertj.core.api.Assertions.assertThat; import static org.mockito.ArgumentMatchers.any; import static org.mockito.ArgumentMatchers.eq; import static org.mockito.ArgumentMatchers.isNull; @@ -75,4 +78,37 @@ void customizesRequests() { }); } + @Test + void supportsExternalHttpClient() { + // Create an external HttpClient + HttpClient externalHttpClient = HttpClient.newBuilder().connectTimeout(Duration.ofSeconds(5)).build(); + + // Create transport with external HttpClient + McpClientTransport transport = HttpClientSseClientTransport.builder(host) + .httpClient(externalHttpClient) + .build(); + + withClient(transport, syncSpec -> syncSpec, mcpSyncClient -> { + mcpSyncClient.initialize(); + // Test should complete without errors + }); + + // External HttpClient should still be usable after transport closes + assertThat(externalHttpClient).isNotNull(); + } + + @Test + void closesInternalHttpClientGracefully() { + // Create transport with internal HttpClient (default behavior) + McpClientTransport transport = HttpClientSseClientTransport.builder(host).build(); + + withClient(transport, syncSpec -> syncSpec, mcpSyncClient -> { + mcpSyncClient.initialize(); + // Test should complete and close gracefully + }); + + // This test verifies that internal HttpClient resources are cleaned up + // The actual verification happens during the graceful close process + } + } diff --git a/mcp-core/src/test/java/io/modelcontextprotocol/client/transport/HttpClientSseClientTransportTests.java b/mcp-core/src/test/java/io/modelcontextprotocol/client/transport/HttpClientSseClientTransportTests.java index c5c365798..10a6b349a 100644 --- a/mcp-core/src/test/java/io/modelcontextprotocol/client/transport/HttpClientSseClientTransportTests.java +++ b/mcp-core/src/test/java/io/modelcontextprotocol/client/transport/HttpClientSseClientTransportTests.java @@ -78,7 +78,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", JSON_MAPPER, - McpAsyncHttpClientRequestCustomizer.NOOP); + McpAsyncHttpClientRequestCustomizer.NOOP, null); } public int getInboundMessageCount() { @@ -313,26 +313,6 @@ void testMessageOrderPreservation() { assertThat(transport.getInboundMessageCount()).isEqualTo(3); } - @Test - void testCustomizeClient() { - // Create an atomic boolean to verify the customizer was called - AtomicBoolean customizerCalled = new AtomicBoolean(false); - - // Create a transport with the customizer - HttpClientSseClientTransport customizedTransport = HttpClientSseClientTransport.builder(host) - .customizeClient(builder -> { - builder.version(HttpClient.Version.HTTP_2); - customizerCalled.set(true); - }) - .build(); - - // Verify the customizer was called - assertThat(customizerCalled.get()).isTrue(); - - // Clean up - customizedTransport.closeGracefully().block(); - } - @Test void testCustomizeRequest() { // Create an atomic boolean to verify the customizer was called @@ -367,32 +347,6 @@ void testCustomizeRequest() { customizedTransport.closeGracefully().block(); } - @Test - void testChainedCustomizations() { - // Create atomic booleans to verify both customizers were called - AtomicBoolean clientCustomizerCalled = new AtomicBoolean(false); - AtomicBoolean requestCustomizerCalled = new AtomicBoolean(false); - - // Create a transport with both customizers chained - HttpClientSseClientTransport customizedTransport = HttpClientSseClientTransport.builder(host) - .customizeClient(builder -> { - builder.connectTimeout(Duration.ofSeconds(30)); - clientCustomizerCalled.set(true); - }) - .customizeRequest(builder -> { - builder.header("X-Api-Key", "test-api-key"); - requestCustomizerCalled.set(true); - }) - .build(); - - // Verify both customizers were called - assertThat(clientCustomizerCalled.get()).isTrue(); - assertThat(requestCustomizerCalled.get()).isTrue(); - - // Clean up - customizedTransport.closeGracefully().block(); - } - @Test void testRequestCustomizer() { var mockCustomizer = mock(McpSyncHttpClientRequestCustomizer.class);