diff --git a/mcp-core/src/main/java/io/modelcontextprotocol/modern/server/transport/HttpServletMcpTransport.java b/mcp-core/src/main/java/io/modelcontextprotocol/modern/server/transport/HttpServletMcpTransport.java
index 2362b03b0..f216ebccf 100644
--- a/mcp-core/src/main/java/io/modelcontextprotocol/modern/server/transport/HttpServletMcpTransport.java
+++ b/mcp-core/src/main/java/io/modelcontextprotocol/modern/server/transport/HttpServletMcpTransport.java
@@ -391,14 +391,27 @@ private void writeJson(HttpServletResponse response, int status, JSONRPCResponse
/**
* Validates {@code Mcp-Method}, {@code MCP-Protocol-Version} and, for the three named
* methods, {@code Mcp-Name} against the request body.
+ *
+ * Each of these headers must appear on exactly one header line. Intermediaries
+ * disagree on which copy of a repeated header counts (the first, the last, or all of
+ * them joined with {@code ", "}), so accepting a repeated header would let a request
+ * be routed or authorized on one value while the server validates another.
+ *
+ * Values already joined into one line by an intermediary are only caught by the body
+ * comparison. A joined {@code Mcp-Name} can still match when the name or URI itself
+ * contains {@code ", "}.
* @return a human-readable mismatch description, or {@code null} if the headers are
* consistent with the body
*/
private String validateHeaders(HttpServletRequest request, JSONRPCRequest jsonRpcRequest) {
- String methodHeader = request.getHeader("Mcp-Method");
- if (methodHeader == null) {
+ List methodHeaders = Collections.list(request.getHeaders("Mcp-Method"));
+ if (methodHeaders.isEmpty()) {
return "Missing required header: Mcp-Method";
}
+ if (methodHeaders.size() > 1) {
+ return "Mcp-Method header has more than one value";
+ }
+ String methodHeader = methodHeaders.get(0);
if (!methodHeader.equals(jsonRpcRequest.method())) {
return "Mcp-Method header does not match request method";
}
@@ -407,10 +420,14 @@ private String validateHeaders(HttpServletRequest request, JSONRPCRequest jsonRp
// which answers it with -32602.
String bodyVersion = protocolVersionFromMeta(jsonRpcRequest.params());
if (bodyVersion != null) {
- String versionHeader = request.getHeader("MCP-Protocol-Version");
- if (versionHeader == null) {
+ List versionHeaders = Collections.list(request.getHeaders("MCP-Protocol-Version"));
+ if (versionHeaders.isEmpty()) {
return "Missing required header: MCP-Protocol-Version";
}
+ if (versionHeaders.size() > 1) {
+ return "MCP-Protocol-Version header has more than one value";
+ }
+ String versionHeader = versionHeaders.get(0);
if (!versionHeader.equals(bodyVersion)) {
return "MCP-Protocol-Version header does not match _meta protocol version";
}
@@ -427,10 +444,14 @@ private String validateHeaders(HttpServletRequest request, JSONRPCRequest jsonRp
if (!(paramsMap.get(nameKey) instanceof String expectedName)) {
return null;
}
- String nameHeader = request.getHeader("Mcp-Name");
- if (nameHeader == null) {
+ List nameHeaders = Collections.list(request.getHeaders("Mcp-Name"));
+ if (nameHeaders.isEmpty()) {
return "Missing required header: Mcp-Name";
}
+ if (nameHeaders.size() > 1) {
+ return "Mcp-Name header has more than one value";
+ }
+ String nameHeader = nameHeaders.get(0);
String decoded = McpHeaderDecoder.decodeValue(nameHeader);
if (decoded == null) {
return "Mcp-Name header has a malformed value";
diff --git a/mcp-test/src/test/java/io/modelcontextprotocol/modern/server/transport/HttpServletMcpTransportIntegrationTests.java b/mcp-test/src/test/java/io/modelcontextprotocol/modern/server/transport/HttpServletMcpTransportIntegrationTests.java
index 68c56aefe..9476e9f8f 100644
--- a/mcp-test/src/test/java/io/modelcontextprotocol/modern/server/transport/HttpServletMcpTransportIntegrationTests.java
+++ b/mcp-test/src/test/java/io/modelcontextprotocol/modern/server/transport/HttpServletMcpTransportIntegrationTests.java
@@ -55,6 +55,7 @@
import org.junit.jupiter.api.BeforeAll;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.params.ParameterizedTest;
+import org.junit.jupiter.params.provider.CsvSource;
import org.junit.jupiter.params.provider.NullAndEmptySource;
import org.junit.jupiter.params.provider.ValueSource;
import reactor.core.publisher.Flux;
@@ -403,6 +404,39 @@ void headerMismatchIsRejected() throws Exception {
assertThat(((Number) error.get("code")).intValue()).isEqualTo(ErrorCodes.HEADER_MISMATCH);
}
+ @ParameterizedTest
+ @CsvSource({ "Mcp-Method, tools/call", "Mcp-Method, tools/list", "Mcp-Name, echo", "Mcp-Name, other",
+ "MCP-Protocol-Version, " + McpSchema.LATEST_PROTOCOL_VERSION })
+ void repeatedStandardHeaderIsRejected(String name, String extraValue) throws Exception {
+ Map params = new HashMap<>();
+ params.put("_meta", meta());
+ params.put("name", "echo");
+ // The request already carries a valid value for every standard header; this adds
+ // a second line for one of them.
+ HttpRequest request = post("tools/call", params).header("Mcp-Name", "echo").header(name, extraValue).build();
+ HttpResponse response = HttpClient.newHttpClient().send(request, HttpResponse.BodyHandlers.ofString());
+
+ assertThat(response.statusCode()).isEqualTo(400);
+ assertThatJson(response.body()).inPath("$.error.code").isEqualTo(ErrorCodes.HEADER_MISMATCH);
+ assertThatJson(response.body()).inPath("$.error.message").isEqualTo(name + " header has more than one value");
+ }
+
+ @Test
+ void commaJoinedMcpMethodIsRejected() throws Exception {
+ Map params = new HashMap<>();
+ params.put("_meta", meta());
+ params.put("name", "echo");
+ HttpRequest request = post("tools/call", params).header("Mcp-Name", "echo")
+ .setHeader("Mcp-Method", "tools/call, tools/list")
+ .build();
+ HttpResponse response = HttpClient.newHttpClient().send(request, HttpResponse.BodyHandlers.ofString());
+
+ assertThat(response.statusCode()).isEqualTo(400);
+ assertThatJson(response.body()).inPath("$.error.code").isEqualTo(ErrorCodes.HEADER_MISMATCH);
+ assertThatJson(response.body()).inPath("$.error.message")
+ .isEqualTo("Mcp-Method header does not match request method");
+ }
+
@Test
void protocolVersionHeaderMismatchIsRejected() throws Exception {
Map meta = meta();