Skip to content

Commit fca4ae9

Browse files
authored
Merge branch 'main' into fix/stdio-malformed-response-recovery
2 parents 4ca7e6b + e6c532e commit fca4ae9

30 files changed

Lines changed: 773 additions & 48 deletions

File tree

‎.github/workflows/publish-snapshot.yml‎

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -41,7 +41,12 @@ jobs:
4141
MAVEN_PASSWORD: ${{ secrets.OSSRH_TOKEN }}
4242
MAVEN_GPG_PASSPHRASE: ${{ secrets.SIGNING_PASSPHRASE }}
4343
run: |
44-
mvn -Pjavadoc -Prelease --batch-mode --update-snapshots deploy
44+
mvn \
45+
-Pjavadoc \
46+
-Prelease \
47+
--batch-mode \
48+
--update-snapshots deploy \
49+
-Dmaven.repository.credentialScope=id
4550
4651
- name: Capture project version
4752
run: echo PROJECT_VERSION=$(mvn help:evaluate -Dexpression=project.version --quiet -DforceStdout) >> $GITHUB_ENV

‎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)

‎docs/index.md‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -36,7 +36,7 @@ enables standardized integration between AI models and tools.
3636
- Supports Synchronous and Asynchronous programming paradigms
3737
- Pluggable JSON serialization (Jackson 2.x and Jackson 3.x)
3838
- Pluggable authorization hooks for server security
39-
- DNS rebinding protection with Host/Origin header validation
39+
- DNS rebinding protection with Host/Origin header validation (opt-in, see [HTTP Transport Security](server.md#http-transport-security))
4040

4141
!!! tip
4242
The core `io.modelcontextprotocol.sdk:mcp` module provides default STDIO, SSE, and Streamable HTTP client and server transport implementations without requiring external web frameworks.

‎docs/server.md‎

Lines changed: 21 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -168,7 +168,7 @@ Key features:
168168
(`keepAliveInterval`, `null` to disable)
169169
- Eviction of idle sessions — no open stream and no request for a full interval — every
170170
30 minutes by default (`sessionSweepInterval`, `null` to keep sessions until deleted)
171-
- Security validation support
171+
- Origin and Host validation (opt-in, see [HTTP Transport Security](#http-transport-security))
172172
- Graceful shutdown support
173173

174174
=== "Streamable HTTP WebFlux (external)"
@@ -261,6 +261,7 @@ Key features:
261261
- SSE endpoint (`/sse`) for server-to-client events
262262
- Message endpoint (configurable) for client-to-server requests
263263
- Error handling and response formatting
264+
- Origin and Host validation (opt-in, see [HTTP Transport Security](#http-transport-security))
264265
- Graceful shutdown support
265266

266267
=== "SSE WebFlux (external)"
@@ -317,6 +318,25 @@ Key features:
317318
- Support for traditional web applications
318319
- Synchronous operation handling
319320

321+
### HTTP Transport Security
322+
323+
!!! warning
324+
HTTP transports do not validate the `Origin` or `Host` headers by default.
325+
326+
To turn validation on, configure a `DefaultServerTransportSecurityValidator` with the allowed origins and hosts:
327+
328+
```java
329+
HttpServletStreamableServerTransportProvider transportProvider =
330+
HttpServletStreamableServerTransportProvider.builder()
331+
.mcpEndpoint("/mcp")
332+
.httpHeaderValidator(DefaultServerTransportSecurityValidator.builder()
333+
.allowedOrigin("http://localhost:*")
334+
.allowedHost("localhost:*")
335+
.build())
336+
.build();
337+
```
338+
339+
The same `httpHeaderValidator(...)` method is available on the Stateless and SSE transport builders.
320340

321341
## Server Capabilities
322342

‎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
}

0 commit comments

Comments
 (0)