Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -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.
* <p>
* 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.
* <p>
* 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<String> 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";
}
Expand All @@ -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<String> 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";
}
Expand All @@ -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<String> 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";
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -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<String, Object> 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<String> 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<String, Object> 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<String> 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<String, Object> meta = meta();
Expand Down
Loading