Skip to content

Commit 1325697

Browse files
committed
Merge remote-tracking branch 'upstream/main' into add-pagination
2 parents d90da1f + 4186ca1 commit 1325697

19 files changed

Lines changed: 840 additions & 312 deletions

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

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -763,8 +763,8 @@ public static class Builder {
763763

764764
private Duration connectTimeout = Duration.ofSeconds(10);
765765

766-
private List<String> supportedProtocolVersions = List.of(ProtocolVersions.MCP_2024_11_05,
767-
ProtocolVersions.MCP_2025_03_26, ProtocolVersions.MCP_2025_06_18, ProtocolVersions.MCP_2025_11_25);
766+
private List<String> supportedProtocolVersions = List.of(ProtocolVersions.MCP_2025_03_26,
767+
ProtocolVersions.MCP_2025_06_18, ProtocolVersions.MCP_2025_11_25);
768768

769769
private McpHttpClientTransportAuthorizationErrorHandler authorizationErrorHandler = McpHttpClientTransportAuthorizationErrorHandler.NOOP;
770770

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

Lines changed: 5 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -258,16 +258,13 @@ private void handleIncomingErrors() {
258258

259259
@Override
260260
public Mono<Void> sendMessage(JSONRPCMessage message) {
261-
if (this.outboundSink.tryEmitNext(message).isSuccess()) {
262-
// TODO: essentially we could reschedule ourselves in some time and make
263-
// another attempt with the already read data but pause reading until
264-
// success
265-
// In this approach we delegate the retry and the backpressure onto the
266-
// caller. This might be enough for most cases.
261+
try {
262+
// busyLooping retries FAIL_NON_SERIALIZED under concurrent senders
263+
this.outboundSink.emitNext(message, Sinks.EmitFailureHandler.busyLooping(Duration.ofMillis(100)));
267264
return Mono.empty();
268265
}
269-
else {
270-
return Mono.error(new RuntimeException("Failed to enqueue message"));
266+
catch (Sinks.EmissionException e) {
267+
return Mono.error(new RuntimeException("Failed to enqueue message", e));
271268
}
272269
}
273270

mcp-core/src/main/java/io/modelcontextprotocol/server/transport/DefaultServerTransportSecurityValidator.java

Lines changed: 50 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@
77
import java.util.ArrayList;
88
import java.util.List;
99
import java.util.Map;
10+
import java.util.Objects;
1011

1112
import io.modelcontextprotocol.util.Assert;
1213

@@ -22,7 +23,8 @@
2223
* @see ServerTransportSecurityValidator
2324
* @see ServerTransportSecurityException
2425
*/
25-
public final class DefaultServerTransportSecurityValidator implements ServerTransportSecurityValidator {
26+
public final class DefaultServerTransportSecurityValidator
27+
implements ServerTransportSecurityValidator, ServerHttpHeaderValidator {
2628

2729
private static final String ORIGIN_HEADER = "Origin";
2830

@@ -47,27 +49,24 @@ private DefaultServerTransportSecurityValidator(List<String> allowedOrigins, Lis
4749
}
4850

4951
@Override
52+
@Deprecated
5053
public void validateHeaders(Map<String, List<String>> headers) throws ServerTransportSecurityException {
51-
boolean missingHost = true;
52-
for (Map.Entry<String, List<String>> entry : headers.entrySet()) {
53-
if (ORIGIN_HEADER.equalsIgnoreCase(entry.getKey())) {
54-
List<String> values = entry.getValue();
55-
if (values == null || values.isEmpty()) {
56-
throw new ServerTransportSecurityException(403, "Invalid Origin header");
57-
}
58-
validateOrigin(values.get(0));
59-
}
60-
else if (HOST_HEADER.equalsIgnoreCase(entry.getKey())) {
61-
missingHost = false;
62-
List<String> values = entry.getValue();
63-
if (values == null || values.isEmpty()) {
64-
throw new ServerTransportSecurityException(421, "Invalid Host header");
65-
}
66-
validateHost(values.get(0));
67-
}
54+
validate(new MapHeaderAccessor(headers));
55+
}
56+
57+
@Override
58+
public void validate(HeaderAccessor headerAccessor) throws ServerTransportSecurityException {
59+
List<String> originValues = headerAccessor.getHeader(ORIGIN_HEADER);
60+
if (originValues != null && !originValues.isEmpty()) {
61+
validateOrigin(originValues.get(0));
6862
}
69-
if (!allowedHosts.isEmpty() && missingHost) {
70-
throw new ServerTransportSecurityException(421, "Invalid Host header");
63+
64+
if (!allowedHosts.isEmpty()) {
65+
List<String> hostValues = headerAccessor.getHeader(HOST_HEADER);
66+
if (hostValues == null || hostValues.isEmpty()) {
67+
throw new ServerTransportSecurityException(421, "Invalid Host header");
68+
}
69+
validateHost(hostValues.get(0));
7170
}
7271
}
7372

@@ -139,6 +138,37 @@ public static Builder builder() {
139138
return new Builder();
140139
}
141140

141+
/**
142+
* {@link HeaderAccessor} view over a {@code Map<String, List<String>>}, used to
143+
* bridge the deprecated {@link #validateHeaders(Map)} to
144+
* {@link #validate(HeaderAccessor)}.
145+
*/
146+
private static final class MapHeaderAccessor implements HeaderAccessor {
147+
148+
private final Map<String, List<String>> headers;
149+
150+
private MapHeaderAccessor(Map<String, List<String>> headers) {
151+
this.headers = headers;
152+
}
153+
154+
@Override
155+
public List<String> getHeader(String name) {
156+
return headers.entrySet()
157+
.stream()
158+
.filter(entry -> entry.getKey().equalsIgnoreCase(name))
159+
.map(Map.Entry::getValue)
160+
.filter(Objects::nonNull)
161+
.findFirst()
162+
.orElse(List.of());
163+
}
164+
165+
@Override
166+
public List<String> getHeaderNames() {
167+
return List.copyOf(headers.keySet());
168+
}
169+
170+
}
171+
142172
/**
143173
* Builder for creating instances of {@link DefaultServerTransportSecurityValidator}.
144174
*/
Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,34 @@
1+
/*
2+
* Copyright 2026-2026 the original author or authors.
3+
*/
4+
5+
package io.modelcontextprotocol.server.transport;
6+
7+
import java.util.List;
8+
9+
/**
10+
* Abstraction for accessing HTTP headers from an incoming request. Implementations should
11+
* provide case-insensitive header name lookups (e.g., when backed by
12+
* {@code HttpServletRequest}).
13+
*
14+
* @author Neeraj Bhatt
15+
* @since 2.1.0
16+
* @see ServerHttpHeaderValidator
17+
*/
18+
public interface HeaderAccessor {
19+
20+
/**
21+
* Returns the values of the specified header, or an empty list if the header is not
22+
* present.
23+
* @param name the header name (case-insensitive)
24+
* @return the list of header values, never {@code null}
25+
*/
26+
List<String> getHeader(String name);
27+
28+
/**
29+
* Returns all header names present in the request.
30+
* @return the list of header names, never {@code null}
31+
*/
32+
List<String> getHeaderNames();
33+
34+
}
Lines changed: 41 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,41 @@
1+
/*
2+
* Copyright 2026-2026 the original author or authors.
3+
*/
4+
5+
package io.modelcontextprotocol.server.transport;
6+
7+
import java.util.Collections;
8+
import java.util.List;
9+
10+
import jakarta.servlet.http.HttpServletRequest;
11+
12+
/**
13+
* {@link HeaderAccessor} implementation backed by an {@link HttpServletRequest}. Header
14+
* name lookups are case-insensitive as per the Servlet specification.
15+
*
16+
* <p>
17+
* For internal use only.
18+
*
19+
* @author Neeraj Bhatt
20+
* @since 2.1.0
21+
* @see HeaderAccessor
22+
*/
23+
final class HttpServletHeaderAccessor implements HeaderAccessor {
24+
25+
private final HttpServletRequest request;
26+
27+
HttpServletHeaderAccessor(HttpServletRequest request) {
28+
this.request = request;
29+
}
30+
31+
@Override
32+
public List<String> getHeader(String name) {
33+
return Collections.list(this.request.getHeaders(name));
34+
}
35+
36+
@Override
37+
public List<String> getHeaderNames() {
38+
return Collections.list(this.request.getHeaderNames());
39+
}
40+
41+
}

mcp-core/src/main/java/io/modelcontextprotocol/server/transport/HttpServletRequestUtils.java

Lines changed: 0 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -8,11 +8,6 @@
88
import java.io.IOException;
99
import java.io.InputStream;
1010
import java.nio.charset.StandardCharsets;
11-
import java.util.Collections;
12-
import java.util.Enumeration;
13-
import java.util.HashMap;
14-
import java.util.List;
15-
import java.util.Map;
1611

1712
import jakarta.servlet.http.HttpServletRequest;
1813

@@ -26,21 +21,6 @@ final class HttpServletRequestUtils {
2621
private HttpServletRequestUtils() {
2722
}
2823

29-
/**
30-
* Extracts all headers from the HTTP request into a map.
31-
* @param request The HTTP servlet request
32-
* @return A map of header names to their values
33-
*/
34-
static Map<String, List<String>> extractHeaders(HttpServletRequest request) {
35-
Map<String, List<String>> headers = new HashMap<>();
36-
Enumeration<String> names = request.getHeaderNames();
37-
while (names.hasMoreElements()) {
38-
String name = names.nextElement();
39-
headers.put(name, Collections.list(request.getHeaders(name)));
40-
}
41-
return headers;
42-
}
43-
4424
/**
4525
* Reads the request body, decoded using the request's character encoding (or UTF-8 if
4626
* not specified), while bounding the number of bytes read.

mcp-core/src/main/java/io/modelcontextprotocol/server/transport/HttpServletSseServerTransportProvider.java

Lines changed: 25 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -161,7 +161,7 @@ public class HttpServletSseServerTransportProvider extends HttpServlet implement
161161
/**
162162
* Security validator for validating HTTP requests.
163163
*/
164-
private final ServerTransportSecurityValidator securityValidator;
164+
private final ServerHttpHeaderValidator httpHeaderValidator;
165165

166166
/**
167167
* Creates a new HttpServletSseServerTransportProvider instance with a custom SSE
@@ -174,28 +174,28 @@ public class HttpServletSseServerTransportProvider extends HttpServlet implement
174174
* @param keepAliveInterval The interval for keep-alive pings, or null to disable
175175
* keep-alive functionality
176176
* @param contextExtractor The extractor for transport context from the request.
177-
* @param securityValidator The security validator for validating HTTP requests.
177+
* @param httpHeaderValidator The HTTP header validator for validating HTTP requests.
178178
* @param requestMaxSize The maximum size, in bytes, of a single request body. Must be
179179
* positive.
180180
*/
181181
private HttpServletSseServerTransportProvider(McpJsonMapper jsonMapper, String baseUrl, String messageEndpoint,
182182
String sseEndpoint, Duration keepAliveInterval,
183183
McpTransportContextExtractor<HttpServletRequest> contextExtractor,
184-
ServerTransportSecurityValidator securityValidator, int requestMaxSize) {
184+
ServerHttpHeaderValidator httpHeaderValidator, int requestMaxSize) {
185185

186186
Assert.notNull(jsonMapper, "JsonMapper must not be null");
187187
Assert.notNull(messageEndpoint, "messageEndpoint must not be null");
188188
Assert.notNull(sseEndpoint, "sseEndpoint must not be null");
189189
Assert.notNull(contextExtractor, "Context extractor must not be null");
190-
Assert.notNull(securityValidator, "Security validator must not be null");
190+
Assert.notNull(httpHeaderValidator, "HTTP header validator must not be null");
191191
Assert.isTrue(requestMaxSize > 0, "requestMaxSize must be positive");
192192

193193
this.jsonMapper = jsonMapper;
194194
this.baseUrl = baseUrl;
195195
this.messageEndpoint = messageEndpoint;
196196
this.sseEndpoint = sseEndpoint;
197197
this.contextExtractor = contextExtractor;
198-
this.securityValidator = securityValidator;
198+
this.httpHeaderValidator = httpHeaderValidator;
199199
this.requestMaxSize = requestMaxSize;
200200

201201
if (keepAliveInterval != null) {
@@ -293,8 +293,7 @@ protected void doGet(HttpServletRequest request, HttpServletResponse response)
293293
}
294294

295295
try {
296-
Map<String, List<String>> headers = HttpServletRequestUtils.extractHeaders(request);
297-
this.securityValidator.validateHeaders(headers);
296+
this.httpHeaderValidator.validate(new HttpServletHeaderAccessor(request));
298297
}
299298
catch (ServerTransportSecurityException e) {
300299
response.sendError(e.getStatusCode(), e.getMessage());
@@ -371,8 +370,7 @@ protected void doPost(HttpServletRequest request, HttpServletResponse response)
371370
}
372371

373372
try {
374-
Map<String, List<String>> headers = HttpServletRequestUtils.extractHeaders(request);
375-
this.securityValidator.validateHeaders(headers);
373+
this.httpHeaderValidator.validate(new HttpServletHeaderAccessor(request));
376374
}
377375
catch (ServerTransportSecurityException e) {
378376
response.sendError(e.getStatusCode(), e.getMessage());
@@ -619,7 +617,7 @@ public static class Builder {
619617

620618
private Duration keepAliveInterval;
621619

622-
private ServerTransportSecurityValidator securityValidator = ServerTransportSecurityValidator.NOOP;
620+
private ServerHttpHeaderValidator httpHeaderValidator = ServerHttpHeaderValidator.NOOP;
623621

624622
private int requestMaxSize = DEFAULT_REQUEST_MAX_SIZE;
625623

@@ -702,10 +700,25 @@ public Builder keepAliveInterval(Duration keepAliveInterval) {
702700
* @param securityValidator The security validator to use. Must not be null.
703701
* @return This builder instance
704702
* @throws IllegalArgumentException if securityValidator is null
703+
* @deprecated Use {@link #httpHeaderValidator(ServerHttpHeaderValidator)}
704+
* instead.
705705
*/
706+
@Deprecated
706707
public Builder securityValidator(ServerTransportSecurityValidator securityValidator) {
707708
Assert.notNull(securityValidator, "Security validator must not be null");
708-
this.securityValidator = securityValidator;
709+
this.httpHeaderValidator = ServerTransportSecurityValidator.toHttpHeaderValidator(securityValidator);
710+
return this;
711+
}
712+
713+
/**
714+
* Sets the HTTP header validator for validating HTTP requests.
715+
* @param httpHeaderValidator The HTTP header validator to use. Must not be null.
716+
* @return This builder instance
717+
* @throws IllegalArgumentException if httpHeaderValidator is null
718+
*/
719+
public Builder httpHeaderValidator(ServerHttpHeaderValidator httpHeaderValidator) {
720+
Assert.notNull(httpHeaderValidator, "HTTP header validator must not be null");
721+
this.httpHeaderValidator = httpHeaderValidator;
709722
return this;
710723
}
711724

@@ -734,7 +747,7 @@ public HttpServletSseServerTransportProvider build() {
734747
}
735748
return new HttpServletSseServerTransportProvider(
736749
jsonMapper == null ? McpJsonDefaults.getMapper() : jsonMapper, baseUrl, messageEndpoint,
737-
sseEndpoint, keepAliveInterval, contextExtractor, securityValidator, requestMaxSize);
750+
sseEndpoint, keepAliveInterval, contextExtractor, httpHeaderValidator, requestMaxSize);
738751
}
739752

740753
}

0 commit comments

Comments
 (0)