Skip to content

Commit dce4b6a

Browse files
committed
Introduce DelegatingMcp(Async)HttpRequestCustomizer
1 parent e00b44e commit dce4b6a

File tree

4 files changed

+195
-0
lines changed

4 files changed

+195
-0
lines changed
Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,38 @@
1+
/*
2+
* Copyright 2024-2025 the original author or authors.
3+
*/
4+
package io.modelcontextprotocol.client.transport.customizer;
5+
6+
import io.modelcontextprotocol.util.Assert;
7+
import java.net.URI;
8+
import java.net.http.HttpRequest;
9+
import java.util.List;
10+
import org.reactivestreams.Publisher;
11+
import reactor.core.publisher.Mono;
12+
13+
/**
14+
* Composable {@link McpAsyncHttpRequestCustomizer} that applies multiple customizers, in
15+
* order.
16+
*
17+
* @author Daniel Garnier-Moiroux
18+
*/
19+
public class DelegatingMcpAsyncHttpRequestCustomizer implements McpAsyncHttpRequestCustomizer {
20+
21+
private final List<McpAsyncHttpRequestCustomizer> customizers;
22+
23+
public DelegatingMcpAsyncHttpRequestCustomizer(List<McpAsyncHttpRequestCustomizer> customizers) {
24+
Assert.notNull(customizers, "Customizers must not be null");
25+
this.customizers = customizers;
26+
}
27+
28+
@Override
29+
public Publisher<HttpRequest.Builder> customize(HttpRequest.Builder builder, String method, URI endpoint,
30+
String body) {
31+
var result = Mono.just(builder);
32+
for (var customizer : this.customizers) {
33+
result = result.flatMap(b -> Mono.from(customizer.customize(b, method, endpoint, body)));
34+
}
35+
return result;
36+
}
37+
38+
}
Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,32 @@
1+
/*
2+
* Copyright 2024-2025 the original author or authors.
3+
*/
4+
5+
package io.modelcontextprotocol.client.transport.customizer;
6+
7+
import io.modelcontextprotocol.util.Assert;
8+
import java.net.URI;
9+
import java.net.http.HttpRequest;
10+
import java.util.List;
11+
12+
/**
13+
* Composable {@link McpSyncHttpRequestCustomizer} that applies multiple customizers, in
14+
* order.
15+
*
16+
* @author Daniel Garnier-Moiroux
17+
*/
18+
public class DelegatingMcpSyncHttpRequestCustomizer implements McpSyncHttpRequestCustomizer {
19+
20+
private final List<McpSyncHttpRequestCustomizer> delegates;
21+
22+
public DelegatingMcpSyncHttpRequestCustomizer(List<McpSyncHttpRequestCustomizer> customizers) {
23+
Assert.notNull(customizers, "Customizers must not be null");
24+
this.delegates = customizers;
25+
}
26+
27+
@Override
28+
public void customize(HttpRequest.Builder builder, String method, URI endpoint, String body) {
29+
this.delegates.forEach(delegate -> delegate.customize(builder, method, endpoint, body));
30+
}
31+
32+
}
Lines changed: 67 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,67 @@
1+
/*
2+
* Copyright 2024-2025 the original author or authors.
3+
*/
4+
5+
package io.modelcontextprotocol.client.transport.customizer;
6+
7+
import java.net.URI;
8+
import java.net.http.HttpRequest;
9+
import java.util.List;
10+
import org.junit.jupiter.api.Test;
11+
import reactor.core.publisher.Mono;
12+
import reactor.test.StepVerifier;
13+
14+
import static org.assertj.core.api.Assertions.assertThatThrownBy;
15+
import static org.mockito.ArgumentMatchers.any;
16+
import static org.mockito.Mockito.mock;
17+
import static org.mockito.Mockito.verify;
18+
import static org.mockito.Mockito.when;
19+
20+
/**
21+
* Tests for {@link DelegatingMcpAsyncHttpRequestCustomizer}.
22+
*
23+
* @author Daniel Garnier-Moiroux
24+
*/
25+
class DelegatingMcpAsyncHttpRequestCustomizerTest {
26+
27+
private static final URI TEST_URI = URI.create("https://example.com");
28+
29+
private final HttpRequest.Builder TEST_BUILDER = HttpRequest.newBuilder(TEST_URI);
30+
31+
@Test
32+
void delegates() {
33+
var mockCustomizer = mock(McpAsyncHttpRequestCustomizer.class);
34+
when(mockCustomizer.customize(any(), any(), any(), any()))
35+
.thenAnswer(invocation -> Mono.just(invocation.getArguments()[0]));
36+
var customizer = new DelegatingMcpAsyncHttpRequestCustomizer(List.of(mockCustomizer));
37+
38+
StepVerifier.create(customizer.customize(TEST_BUILDER, "GET", TEST_URI, "{\"everybody\": \"needs somebody\"}"))
39+
.expectNext(TEST_BUILDER)
40+
.verifyComplete();
41+
42+
verify(mockCustomizer).customize(TEST_BUILDER, "GET", TEST_URI, "{\"everybody\": \"needs somebody\"}");
43+
}
44+
45+
@Test
46+
void delegatesInOrder() {
47+
var customizer = new DelegatingMcpAsyncHttpRequestCustomizer(
48+
List.of((builder, method, uri, body) -> Mono.just(builder.copy().header("x-test", "one")),
49+
(builder, method, uri, body) -> Mono.just(builder.copy().header("x-test", "two"))));
50+
51+
var headers = Mono
52+
.from(customizer.customize(TEST_BUILDER, "GET", TEST_URI, "{\"everybody\": \"needs somebody\"}"))
53+
.map(HttpRequest.Builder::build)
54+
.map(HttpRequest::headers)
55+
.flatMapIterable(h -> h.allValues("x-test"));
56+
57+
StepVerifier.create(headers).expectNext("one").expectNext("two").verifyComplete();
58+
}
59+
60+
@Test
61+
void constructorRequiresNonNull() {
62+
assertThatThrownBy(() -> new DelegatingMcpAsyncHttpRequestCustomizer(null))
63+
.isInstanceOf(IllegalArgumentException.class)
64+
.hasMessage("Customizers must not be null");
65+
}
66+
67+
}
Lines changed: 58 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,58 @@
1+
/*
2+
* Copyright 2024-2025 the original author or authors.
3+
*/
4+
5+
package io.modelcontextprotocol.client.transport.customizer;
6+
7+
import java.net.URI;
8+
import java.net.http.HttpRequest;
9+
import java.util.List;
10+
import org.junit.jupiter.api.Test;
11+
import org.mockito.Mockito;
12+
13+
import static org.assertj.core.api.Assertions.assertThat;
14+
import static org.assertj.core.api.Assertions.assertThatThrownBy;
15+
import static org.mockito.Mockito.verify;
16+
17+
/**
18+
* Tests for {@link DelegatingMcpSyncHttpRequestCustomizer}.
19+
*
20+
* @author Daniel Garnier-Moiroux
21+
*/
22+
class DelegatingMcpSyncHttpRequestCustomizerTest {
23+
24+
private static final URI TEST_URI = URI.create("https://example.com");
25+
26+
private final HttpRequest.Builder TEST_BUILDER = HttpRequest.newBuilder(TEST_URI);
27+
28+
@Test
29+
void delegates() {
30+
var mockCustomizer = Mockito.mock(McpSyncHttpRequestCustomizer.class);
31+
var customizer = new DelegatingMcpSyncHttpRequestCustomizer(List.of(mockCustomizer));
32+
33+
customizer.customize(TEST_BUILDER, "GET", TEST_URI, "{\"everybody\": \"needs somebody\"}");
34+
35+
verify(mockCustomizer).customize(TEST_BUILDER, "GET", TEST_URI, "{\"everybody\": \"needs somebody\"}");
36+
}
37+
38+
@Test
39+
void delegatesInOrder() {
40+
var testHeaderName = "x-test";
41+
var customizer = new DelegatingMcpSyncHttpRequestCustomizer(
42+
List.of((builder, method, uri, body) -> builder.header(testHeaderName, "one"),
43+
(builder, method, uri, body) -> builder.header(testHeaderName, "two")));
44+
45+
customizer.customize(TEST_BUILDER, "GET", TEST_URI, "");
46+
var request = TEST_BUILDER.build();
47+
48+
assertThat(request.headers().allValues(testHeaderName)).containsExactly("one", "two");
49+
}
50+
51+
@Test
52+
void constructorRequiresNonNull() {
53+
assertThatThrownBy(() -> new DelegatingMcpAsyncHttpRequestCustomizer(null))
54+
.isInstanceOf(IllegalArgumentException.class)
55+
.hasMessage("Customizers must not be null");
56+
}
57+
58+
}

0 commit comments

Comments
 (0)