Skip to content

Commit 7b232ec

Browse files
authored
Merge branch 'main' into fix/server-discover-500
2 parents 449a7d6 + 4186ca1 commit 7b232ec

22 files changed

Lines changed: 896 additions & 323 deletions

‎CONTRIBUTING.md‎

Lines changed: 15 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -69,15 +69,25 @@ git checkout -b feature/your-feature-name
6969

7070
## Submitting Changes
7171

72+
Please don't submit pull requests:
73+
74+
- With GitHub accounts managed by autonomous AI bots
75+
- For already assigned issues (as the assignee is or plans to work on it)
76+
77+
When submitting changes:
78+
7279
1. For non-trivial changes, please clarify with the maintainers in an issue whether
7380
you can contribute the change and the desired scope of the change.
7481
2. For trivial changes (for example a couple of lines or documentation changes) there
7582
is no need to open an issue first.
76-
3. Push your changes to your fork.
77-
4. Submit a pull request to the main repository.
78-
5. Follow the pull request template.
79-
6. Wait for review.
80-
7. For any follow-up work, please add new commits instead of force-pushing. This will
83+
3. Maintainers will triage issues and act on them. There is no such guarantee on pull requests.
84+
PRs opened without being discussed in an issue MAY be ignored, and maintainers reserve the right
85+
to close these PRs without justification.
86+
4. Push your changes to your fork.
87+
5. Submit a pull request to the main repository.
88+
6. Follow the pull request template.
89+
7. Wait for review.
90+
8. For any follow-up work, please add new commits instead of force-pushing. This will
8191
allow the reviewer to focus on incremental changes instead of having to restart the
8292
review process.
8393

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