From 4f1052ac8f024a56b6b0f0a0ca41c9e701d4f565 Mon Sep 17 00:00:00 2001 From: cooleditphoto Date: Tue, 2 Jun 2026 22:08:16 -0400 Subject: [PATCH 1/4] feat(mcp-core): support Mcp-Name header for Streamable HTTP (SEP-2243) Add MCP_NAME header constant and inject it in the streamable HTTP client transport (HttpClientStreamableHttpTransport) --- .../HttpClientStreamableHttpTransport.java | 70 +++++++++++++++++-- ...vletStreamableServerTransportProvider.java | 62 ++++++++++++++++ .../spec/HttpHeaders.java | 5 ++ mcp-json-jackson2/pom.xml | 2 + mcp-json-jackson3/pom.xml | 2 + 5 files changed, 137 insertions(+), 4 deletions(-) diff --git a/mcp-core/src/main/java/io/modelcontextprotocol/client/transport/HttpClientStreamableHttpTransport.java b/mcp-core/src/main/java/io/modelcontextprotocol/client/transport/HttpClientStreamableHttpTransport.java index 8e0e06cd4..8d5c60ac4 100644 --- a/mcp-core/src/main/java/io/modelcontextprotocol/client/transport/HttpClientStreamableHttpTransport.java +++ b/mcp-core/src/main/java/io/modelcontextprotocol/client/transport/HttpClientStreamableHttpTransport.java @@ -483,14 +483,34 @@ public Mono sendMessage(McpSchema.JSONRPCMessage sentMessage) { transportSession.sessionId().get()); } + // Extract method and params for Mcp-Name header + String method = null; + Object params = null; + if (sentMessage instanceof McpSchema.JSONRPCRequest request) { + method = request.method(); + params = request.params(); + } + else if (sentMessage instanceof McpSchema.JSONRPCNotification notification) { + method = notification.method(); + params = notification.params(); + } + var builder = requestBuilder.uri(uri) .header(HttpHeaders.ACCEPT, APPLICATION_JSON + ", " + TEXT_EVENT_STREAM) .header(HttpHeaders.CONTENT_TYPE, APPLICATION_JSON_UTF8) .header(HttpHeaders.CACHE_CONTROL, "no-cache") - .header(HttpHeaders.PROTOCOL_VERSION, - ctx.getOrDefault(McpAsyncClient.NEGOTIATED_PROTOCOL_VERSION, - this.latestSupportedProtocolVersion)) - .POST(HttpRequest.BodyPublishers.ofString(jsonBody)); + .header(HttpHeaders.PROTOCOL_VERSION, ctx.getOrDefault(McpAsyncClient.NEGOTIATED_PROTOCOL_VERSION, + this.latestSupportedProtocolVersion)); + + // Add Mcp-Name header if applicable + if (method != null) { + String name = extractNameFromParams(method, params); + if (name != null) { + builder = builder.header(HttpHeaders.MCP_NAME, name); + } + } + + builder = builder.POST(HttpRequest.BodyPublishers.ofString(jsonBody)); var transportContext = ctx.getOrDefault(McpTransportContext.KEY, McpTransportContext.EMPTY); return Mono .from(this.httpRequestCustomizer.customize(builder, "POST", uri, jsonBody, transportContext)); @@ -675,6 +695,48 @@ public T unmarshalFrom(Object data, TypeRef typeRef) { return this.jsonMapper.convertValue(data, typeRef); } + /** + * Extracts the name/URI from the request params based on the method type. + * @param method the MCP method name + * @param params the request parameters + * @return the name/URI if applicable for the method, or null otherwise + */ + private String extractNameFromParams(String method, Object params) { + if (params == null) { + return null; + } + + try { + switch (method) { + case McpSchema.METHOD_TOOLS_CALL -> { + McpSchema.CallToolRequest request = this.jsonMapper.convertValue(params, + new TypeRef() { + }); + return request.name(); + } + case McpSchema.METHOD_RESOURCES_READ -> { + McpSchema.ReadResourceRequest request = this.jsonMapper.convertValue(params, + new TypeRef() { + }); + return request.uri(); + } + case McpSchema.METHOD_PROMPT_GET -> { + McpSchema.GetPromptRequest request = this.jsonMapper.convertValue(params, + new TypeRef() { + }); + return request.name(); + } + default -> { + return null; + } + } + } + catch (Exception e) { + logger.debug("Failed to extract name from params for method {}: {}", method, e.getMessage()); + return null; + } + } + /** * Builder for {@link HttpClientStreamableHttpTransport}. */ diff --git a/mcp-core/src/main/java/io/modelcontextprotocol/server/transport/HttpServletStreamableServerTransportProvider.java b/mcp-core/src/main/java/io/modelcontextprotocol/server/transport/HttpServletStreamableServerTransportProvider.java index d956a8726..f95bda163 100644 --- a/mcp-core/src/main/java/io/modelcontextprotocol/server/transport/HttpServletStreamableServerTransportProvider.java +++ b/mcp-core/src/main/java/io/modelcontextprotocol/server/transport/HttpServletStreamableServerTransportProvider.java @@ -390,6 +390,42 @@ public void onStartAsync(jakarta.servlet.AsyncEvent event) throws IOException { * @throws ServletException If a servlet-specific error occurs * @throws IOException If an I/O error occurs */ + private String extractNameFromParams(String method, Object params) { + if (params == null) { + return null; + } + + try { + switch (method) { + case McpSchema.METHOD_TOOLS_CALL -> { + McpSchema.CallToolRequest request = jsonMapper.convertValue(params, + new TypeRef() { + }); + return request.name(); + } + case McpSchema.METHOD_RESOURCES_READ -> { + McpSchema.ReadResourceRequest request = jsonMapper.convertValue(params, + new TypeRef() { + }); + return request.uri(); + } + case McpSchema.METHOD_PROMPT_GET -> { + McpSchema.GetPromptRequest request = jsonMapper.convertValue(params, + new TypeRef() { + }); + return request.name(); + } + default -> { + return null; + } + } + } + catch (Exception e) { + logger.debug("Failed to extract name from params for method {}: {}", method, e.getMessage()); + return null; + } + } + @Override protected void doPost(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException { @@ -436,6 +472,32 @@ protected void doPost(HttpServletRequest request, HttpServletResponse response) McpSchema.JSONRPCMessage message = McpSchema.deserializeJsonRpcMessage(jsonMapper, body.toString()); + if (message instanceof McpSchema.JSONRPCRequest jsonrpcRequest) { + String method = jsonrpcRequest.method(); + if (McpSchema.METHOD_TOOLS_CALL.equals(method) || McpSchema.METHOD_RESOURCES_READ.equals(method) + || McpSchema.METHOD_PROMPT_GET.equals(method)) { + String expectedName = extractNameFromParams(method, jsonrpcRequest.params()); + String headerName = request.getHeader(HttpHeaders.MCP_NAME); + + if (headerName == null || headerName.isBlank()) { + this.responseError(response, HttpServletResponse.SC_BAD_REQUEST, + McpError.builder(McpSchema.ErrorCodes.INVALID_REQUEST) + .message("Mcp-Name header required for method " + method) + .build()); + return; + } + + if (expectedName == null || !headerName.equals(expectedName)) { + this.responseError(response, HttpServletResponse.SC_BAD_REQUEST, + McpError.builder(McpSchema.ErrorCodes.INVALID_REQUEST) + .message("Mcp-Name header mismatch: expected '" + expectedName + "' but was '" + + headerName + "'") + .build()); + return; + } + } + } + // Handle initialization request if (message instanceof McpSchema.JSONRPCRequest jsonrpcRequest && jsonrpcRequest.method().equals(McpSchema.METHOD_INITIALIZE)) { diff --git a/mcp-core/src/main/java/io/modelcontextprotocol/spec/HttpHeaders.java b/mcp-core/src/main/java/io/modelcontextprotocol/spec/HttpHeaders.java index 6afc2c119..2d55a75ed 100644 --- a/mcp-core/src/main/java/io/modelcontextprotocol/spec/HttpHeaders.java +++ b/mcp-core/src/main/java/io/modelcontextprotocol/spec/HttpHeaders.java @@ -26,6 +26,11 @@ public interface HttpHeaders { */ String PROTOCOL_VERSION = "MCP-Protocol-Version"; + /** + * The name or URI of the resource/tool/prompt being accessed. + */ + String MCP_NAME = "Mcp-Name"; + /** * The HTTP Content-Length header. * @see Date: Tue, 2 Jun 2026 22:08:16 -0400 Subject: [PATCH 2/4] feat(mcp-core): support Mcp-Name header for Streamable HTTP (SEP-2243) Add MCP_NAME header constant and inject it in the streamable HTTP client transport (HttpClientStreamableHttpTransport) --- .../HttpClientStreamableHttpTransport.java | 71 +++++++- ...vletStreamableServerTransportProvider.java | 152 +++++++++++++++++- .../spec/HttpHeaders.java | 10 ++ mcp-json-jackson2/pom.xml | 2 + mcp-json-jackson3/pom.xml | 2 + ...ientStreamableHttpTransportHeaderTest.java | 70 ++++++++ ...HttpClientStreamableHttpTransportTest.java | 97 +++++++++++ 7 files changed, 398 insertions(+), 6 deletions(-) create mode 100644 mcp-test/src/test/java/io/modelcontextprotocol/client/transport/HttpClientStreamableHttpTransportHeaderTest.java diff --git a/mcp-core/src/main/java/io/modelcontextprotocol/client/transport/HttpClientStreamableHttpTransport.java b/mcp-core/src/main/java/io/modelcontextprotocol/client/transport/HttpClientStreamableHttpTransport.java index 8e0e06cd4..a642e0113 100644 --- a/mcp-core/src/main/java/io/modelcontextprotocol/client/transport/HttpClientStreamableHttpTransport.java +++ b/mcp-core/src/main/java/io/modelcontextprotocol/client/transport/HttpClientStreamableHttpTransport.java @@ -483,14 +483,35 @@ public Mono sendMessage(McpSchema.JSONRPCMessage sentMessage) { transportSession.sessionId().get()); } + // Extract method and params for Mcp-Name header + String method = null; + Object params = null; + if (sentMessage instanceof McpSchema.JSONRPCRequest request) { + method = request.method(); + params = request.params(); + } + else if (sentMessage instanceof McpSchema.JSONRPCNotification notification) { + method = notification.method(); + params = notification.params(); + } + var builder = requestBuilder.uri(uri) .header(HttpHeaders.ACCEPT, APPLICATION_JSON + ", " + TEXT_EVENT_STREAM) .header(HttpHeaders.CONTENT_TYPE, APPLICATION_JSON_UTF8) .header(HttpHeaders.CACHE_CONTROL, "no-cache") - .header(HttpHeaders.PROTOCOL_VERSION, - ctx.getOrDefault(McpAsyncClient.NEGOTIATED_PROTOCOL_VERSION, - this.latestSupportedProtocolVersion)) - .POST(HttpRequest.BodyPublishers.ofString(jsonBody)); + .header(HttpHeaders.PROTOCOL_VERSION, ctx.getOrDefault(McpAsyncClient.NEGOTIATED_PROTOCOL_VERSION, + this.latestSupportedProtocolVersion)); + + // Add MCP-specific headers if applicable + if (method != null) { + builder = builder.header(HttpHeaders.MCP_METHOD, method); + String name = extractNameFromParams(method, params); + if (name != null) { + builder = builder.header(HttpHeaders.MCP_NAME, name); + } + } + + builder = builder.POST(HttpRequest.BodyPublishers.ofString(jsonBody)); var transportContext = ctx.getOrDefault(McpTransportContext.KEY, McpTransportContext.EMPTY); return Mono .from(this.httpRequestCustomizer.customize(builder, "POST", uri, jsonBody, transportContext)); @@ -675,6 +696,48 @@ public T unmarshalFrom(Object data, TypeRef typeRef) { return this.jsonMapper.convertValue(data, typeRef); } + /** + * Extracts the name/URI from the request params based on the method type. + * @param method the MCP method name + * @param params the request parameters + * @return the name/URI if applicable for the method, or null otherwise + */ + private String extractNameFromParams(String method, Object params) { + if (params == null) { + return null; + } + + try { + switch (method) { + case McpSchema.METHOD_TOOLS_CALL -> { + McpSchema.CallToolRequest request = this.jsonMapper.convertValue(params, + new TypeRef() { + }); + return request.name(); + } + case McpSchema.METHOD_RESOURCES_READ -> { + McpSchema.ReadResourceRequest request = this.jsonMapper.convertValue(params, + new TypeRef() { + }); + return request.uri(); + } + case McpSchema.METHOD_PROMPT_GET -> { + McpSchema.GetPromptRequest request = this.jsonMapper.convertValue(params, + new TypeRef() { + }); + return request.name(); + } + default -> { + return null; + } + } + } + catch (Exception e) { + logger.debug("Failed to extract name from params for method {}: {}", method, e.getMessage()); + return null; + } + } + /** * Builder for {@link HttpClientStreamableHttpTransport}. */ diff --git a/mcp-core/src/main/java/io/modelcontextprotocol/server/transport/HttpServletStreamableServerTransportProvider.java b/mcp-core/src/main/java/io/modelcontextprotocol/server/transport/HttpServletStreamableServerTransportProvider.java index d956a8726..23b9ab8bc 100644 --- a/mcp-core/src/main/java/io/modelcontextprotocol/server/transport/HttpServletStreamableServerTransportProvider.java +++ b/mcp-core/src/main/java/io/modelcontextprotocol/server/transport/HttpServletStreamableServerTransportProvider.java @@ -126,6 +126,10 @@ public class HttpServletStreamableServerTransportProvider extends HttpServlet */ private final ServerTransportSecurityValidator securityValidator; + private final boolean requireMcpNameHeader; + + private final boolean requireMcpMethodHeader; + /** * Constructs a new HttpServletStreamableServerTransportProvider instance. * @param jsonMapper The JsonMapper to use for JSON serialization/deserialization of @@ -141,7 +145,8 @@ public class HttpServletStreamableServerTransportProvider extends HttpServlet */ private HttpServletStreamableServerTransportProvider(McpJsonMapper jsonMapper, String mcpEndpoint, boolean disallowDelete, McpTransportContextExtractor contextExtractor, - Duration keepAliveInterval, ServerTransportSecurityValidator securityValidator) { + Duration keepAliveInterval, ServerTransportSecurityValidator securityValidator, + boolean requireMcpNameHeader, boolean requireMcpMethodHeader) { Assert.notNull(jsonMapper, "JsonMapper must not be null"); Assert.notNull(mcpEndpoint, "MCP endpoint must not be null"); Assert.notNull(contextExtractor, "Context extractor must not be null"); @@ -152,6 +157,8 @@ private HttpServletStreamableServerTransportProvider(McpJsonMapper jsonMapper, S this.disallowDelete = disallowDelete; this.contextExtractor = contextExtractor; this.securityValidator = securityValidator; + this.requireMcpNameHeader = requireMcpNameHeader; + this.requireMcpMethodHeader = requireMcpMethodHeader; if (keepAliveInterval != null) { @@ -390,6 +397,101 @@ public void onStartAsync(jakarta.servlet.AsyncEvent event) throws IOException { * @throws ServletException If a servlet-specific error occurs * @throws IOException If an I/O error occurs */ + private String extractNameFromParams(String method, Object params) { + if (params == null) { + return null; + } + + try { + switch (method) { + case McpSchema.METHOD_TOOLS_CALL -> { + McpSchema.CallToolRequest request = jsonMapper.convertValue(params, + new TypeRef() { + }); + return request.name(); + } + case McpSchema.METHOD_RESOURCES_READ -> { + McpSchema.ReadResourceRequest request = jsonMapper.convertValue(params, + new TypeRef() { + }); + return request.uri(); + } + case McpSchema.METHOD_PROMPT_GET -> { + McpSchema.GetPromptRequest request = jsonMapper.convertValue(params, + new TypeRef() { + }); + return request.name(); + } + default -> { + return null; + } + } + } + catch (Exception e) { + logger.debug("Failed to extract name from params for method {}: {}", method, e.getMessage()); + return null; + } + } + + private boolean validateMcpMethodHeader(HttpServletRequest request, HttpServletResponse response, String method) + throws IOException { + if (!this.requireMcpMethodHeader) { + return true; + } + + String headerMethod = request.getHeader(HttpHeaders.MCP_METHOD); + if (headerMethod == null || headerMethod.isBlank()) { + this.responseError(response, HttpServletResponse.SC_BAD_REQUEST, + McpError.builder(McpSchema.ErrorCodes.INVALID_REQUEST) + .message("Mcp-Method header required for method " + method) + .build()); + return false; + } + + if (!method.equals(headerMethod)) { + this.responseError(response, HttpServletResponse.SC_BAD_REQUEST, + McpError.builder(McpSchema.ErrorCodes.INVALID_REQUEST) + .message("Mcp-Method header mismatch: expected '" + method + "' but was '" + headerMethod + "'") + .build()); + return false; + } + + return true; + } + + private boolean validateMcpNameHeader(HttpServletRequest request, HttpServletResponse response, String method, + Object params) throws IOException { + if (!this.requireMcpNameHeader) { + return true; + } + + String expectedName = extractNameFromParams(method, params); + String headerName = request.getHeader(HttpHeaders.MCP_NAME); + if (headerName == null || headerName.isBlank()) { + this.responseError(response, HttpServletResponse.SC_BAD_REQUEST, + McpError.builder(McpSchema.ErrorCodes.INVALID_REQUEST) + .message("Mcp-Name header required for method " + method) + .build()); + return false; + } + + if (expectedName == null || !headerName.equals(expectedName)) { + this.responseError(response, HttpServletResponse.SC_BAD_REQUEST, + McpError.builder(McpSchema.ErrorCodes.INVALID_REQUEST) + .message("Mcp-Name header mismatch: expected '" + expectedName + "' but was '" + headerName + + "'") + .build()); + return false; + } + + return true; + } + + private boolean isNameHeaderMethod(String method) { + return McpSchema.METHOD_TOOLS_CALL.equals(method) || McpSchema.METHOD_RESOURCES_READ.equals(method) + || McpSchema.METHOD_PROMPT_GET.equals(method); + } + @Override protected void doPost(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException { @@ -436,6 +538,23 @@ protected void doPost(HttpServletRequest request, HttpServletResponse response) McpSchema.JSONRPCMessage message = McpSchema.deserializeJsonRpcMessage(jsonMapper, body.toString()); + if (message instanceof McpSchema.JSONRPCRequest jsonrpcRequest) { + String method = jsonrpcRequest.method(); + if (!validateMcpMethodHeader(request, response, method)) { + return; + } + if (isNameHeaderMethod(method) + && !validateMcpNameHeader(request, response, method, jsonrpcRequest.params())) { + return; + } + } + else if (message instanceof McpSchema.JSONRPCNotification jsonrpcNotification) { + String method = jsonrpcNotification.method(); + if (!validateMcpMethodHeader(request, response, method)) { + return; + } + } + // Handle initialization request if (message instanceof McpSchema.JSONRPCRequest jsonrpcRequest && jsonrpcRequest.method().equals(McpSchema.METHOD_INITIALIZE)) { @@ -830,6 +949,10 @@ public static class Builder { private boolean disallowDelete = false; + private boolean requireMcpNameHeader = false; + + private boolean requireMcpMethodHeader = false; + private McpTransportContextExtractor contextExtractor = ( serverRequest) -> McpTransportContext.EMPTY; @@ -908,6 +1031,30 @@ public Builder securityValidator(ServerTransportSecurityValidator securityValida return this; } + /** + * Sets whether the server should reject requests that do not include the + * {@code Mcp-Name} header for methods that may include it. + * @param requireMcpNameHeader true to reject missing headers, false to accept + * requests from older clients by default + * @return this builder instance + */ + public Builder requireMcpNameHeader(boolean requireMcpNameHeader) { + this.requireMcpNameHeader = requireMcpNameHeader; + return this; + } + + /** + * Sets whether the server should reject requests that do not include the + * {@code Mcp-Method} header for requests and notifications. + * @param requireMcpMethodHeader true to reject missing or mismatched method + * headers, false to accept requests from older clients by default + * @return this builder instance + */ + public Builder requireMcpMethodHeader(boolean requireMcpMethodHeader) { + this.requireMcpMethodHeader = requireMcpMethodHeader; + return this; + } + /** * Builds a new instance of {@link HttpServletStreamableServerTransportProvider} * with the configured settings. @@ -918,7 +1065,8 @@ public HttpServletStreamableServerTransportProvider build() { Assert.notNull(this.mcpEndpoint, "MCP endpoint must be set"); return new HttpServletStreamableServerTransportProvider( jsonMapper == null ? McpJsonDefaults.getMapper() : jsonMapper, mcpEndpoint, disallowDelete, - contextExtractor, keepAliveInterval, securityValidator); + contextExtractor, keepAliveInterval, securityValidator, requireMcpNameHeader, + requireMcpMethodHeader); } } diff --git a/mcp-core/src/main/java/io/modelcontextprotocol/spec/HttpHeaders.java b/mcp-core/src/main/java/io/modelcontextprotocol/spec/HttpHeaders.java index 6afc2c119..59cf9eba5 100644 --- a/mcp-core/src/main/java/io/modelcontextprotocol/spec/HttpHeaders.java +++ b/mcp-core/src/main/java/io/modelcontextprotocol/spec/HttpHeaders.java @@ -26,6 +26,16 @@ public interface HttpHeaders { */ String PROTOCOL_VERSION = "MCP-Protocol-Version"; + /** + * The name or URI of the resource/tool/prompt being accessed. + */ + String MCP_NAME = "Mcp-Name"; + + /** + * The MCP method name for the current request or notification. + */ + String MCP_METHOD = "Mcp-Method"; + /** * The HTTP Content-Length header. * @see { + try (exchange) { + if (!"POST".equals(exchange.getRequestMethod())) { + exchange.sendResponseHeaders(405, -1); + return; + } + + String methodHeader = exchange.getRequestHeaders().getFirst("Mcp-Method"); + byte[] requestBody = exchange.getRequestBody().readAllBytes(); + String body = new String(requestBody, StandardCharsets.UTF_8); + + if (!"initialize".equals(methodHeader) || !body.contains("\"method\":\"initialize\"")) { + exchange.sendResponseHeaders(400, 0); + return; + } + + exchange.sendResponseHeaders(202, -1); + } + }); + server.start(); + + int port = server.getAddress().getPort(); + var transport = HttpClientStreamableHttpTransport.builder("http://localhost:" + port) + .endpoint("/mcp") + .build(); + try { + var initializeRequest = McpSchema.InitializeRequest + .builder(ProtocolVersions.MCP_2025_11_25, + McpSchema.ClientCapabilities.builder().roots(true).build(), + McpSchema.Implementation.builder("MCP Client", "0.3.1").build()) + .build(); + var testMessage = new McpSchema.JSONRPCRequest(McpSchema.METHOD_INITIALIZE, "test-id", + initializeRequest); + + StepVerifier.create(transport.sendMessage(testMessage)).verifyComplete(); + } + finally { + StepVerifier.create(transport.closeGracefully()).verifyComplete(); + } + } + finally { + server.stop(0); + } + } + +} diff --git a/mcp-test/src/test/java/io/modelcontextprotocol/client/transport/HttpClientStreamableHttpTransportTest.java b/mcp-test/src/test/java/io/modelcontextprotocol/client/transport/HttpClientStreamableHttpTransportTest.java index c3e85814c..10c9837f0 100644 --- a/mcp-test/src/test/java/io/modelcontextprotocol/client/transport/HttpClientStreamableHttpTransportTest.java +++ b/mcp-test/src/test/java/io/modelcontextprotocol/client/transport/HttpClientStreamableHttpTransportTest.java @@ -4,13 +4,17 @@ package io.modelcontextprotocol.client.transport; +import com.sun.net.httpserver.HttpServer; import io.modelcontextprotocol.client.transport.customizer.McpAsyncHttpClientRequestCustomizer; import io.modelcontextprotocol.client.transport.customizer.McpSyncHttpClientRequestCustomizer; import io.modelcontextprotocol.common.McpTransportContext; import io.modelcontextprotocol.spec.McpSchema; import io.modelcontextprotocol.spec.ProtocolVersions; +import java.io.IOException; +import java.net.InetSocketAddress; import java.net.URI; import java.net.URISyntaxException; +import java.nio.charset.StandardCharsets; import java.util.Map; import java.util.function.Consumer; import org.junit.jupiter.api.AfterAll; @@ -161,4 +165,97 @@ void testCloseInitialized() { .verify(); } + @Test + void testMcpMethodHeaderIsAddedForInitializeRequest() throws IOException { + HttpServer server = HttpServer.create(new InetSocketAddress(0), 0); + try { + server.createContext("/mcp", exchange -> { + try (exchange) { + if (!"POST".equals(exchange.getRequestMethod())) { + exchange.sendResponseHeaders(405, -1); + return; + } + + String methodHeader = exchange.getRequestHeaders().getFirst("Mcp-Method"); + byte[] requestBody = exchange.getRequestBody().readAllBytes(); + String body = new String(requestBody, StandardCharsets.UTF_8); + + if (!"initialize".equals(methodHeader) || !body.contains("\"method\":\"initialize\"")) { + exchange.sendResponseHeaders(400, 0); + return; + } + + exchange.sendResponseHeaders(202, -1); + } + }); + server.start(); + + int port = server.getAddress().getPort(); + var transport = HttpClientStreamableHttpTransport.builder("http://localhost:" + port) + .endpoint("/mcp") + .build(); + try { + var initializeRequest = McpSchema.InitializeRequest + .builder(ProtocolVersions.MCP_2025_11_25, + McpSchema.ClientCapabilities.builder().roots(true).build(), + McpSchema.Implementation.builder("MCP Client", "0.3.1").build()) + .build(); + var testMessage = new McpSchema.JSONRPCRequest(McpSchema.METHOD_INITIALIZE, "test-id", + initializeRequest); + + StepVerifier.create(transport.sendMessage(testMessage)).verifyComplete(); + } + finally { + StepVerifier.create(transport.closeGracefully()).verifyComplete(); + } + } + finally { + server.stop(0); + } + } + + @Test + void testMcpNameHeaderIsAddedForToolsCall() throws IOException { + HttpServer server = HttpServer.create(new InetSocketAddress(0), 0); + try { + server.createContext("/mcp", exchange -> { + try (exchange) { + if (!"POST".equals(exchange.getRequestMethod())) { + exchange.sendResponseHeaders(405, -1); + return; + } + + String nameHeader = exchange.getRequestHeaders().getFirst("Mcp-Name"); + byte[] requestBody = exchange.getRequestBody().readAllBytes(); + String body = new String(requestBody, StandardCharsets.UTF_8); + + if (!"test-tool".equals(nameHeader) || !body.contains("\"method\":\"tools/call\"")) { + exchange.sendResponseHeaders(400, 0); + return; + } + + exchange.sendResponseHeaders(202, -1); + } + }); + server.start(); + + int port = server.getAddress().getPort(); + var transport = HttpClientStreamableHttpTransport.builder("http://localhost:" + port) + .endpoint("/mcp") + .build(); + try { + var callToolRequest = McpSchema.CallToolRequest.builder("test-tool").build(); + var testMessage = new McpSchema.JSONRPCRequest(McpSchema.METHOD_TOOLS_CALL, "test-id", callToolRequest); + + StepVerifier.create(transport.sendMessage(testMessage)).verifyComplete(); + } + finally { + StepVerifier.create(transport.closeGracefully()).verifyComplete(); + } + } + finally { + server.stop(0); + } + } + } From bb9b410abc2ed2d2ec13948f661a563039bd0bfe Mon Sep 17 00:00:00 2001 From: cooleditphoto Date: Sun, 19 Jul 2026 19:27:47 -0400 Subject: [PATCH 3/4] clean up client side test --- .../HttpClientStreamableHttpTransport.java | 14 +--- .../spec/HttpHeaders.java | 3 +- ...ientStreamableHttpTransportHeaderTest.java | 70 ------------------- ...HttpClientStreamableHttpTransportTest.java | 46 +++++++++++- 4 files changed, 47 insertions(+), 86 deletions(-) delete mode 100644 mcp-test/src/test/java/io/modelcontextprotocol/client/transport/HttpClientStreamableHttpTransportHeaderTest.java diff --git a/mcp-core/src/main/java/io/modelcontextprotocol/client/transport/HttpClientStreamableHttpTransport.java b/mcp-core/src/main/java/io/modelcontextprotocol/client/transport/HttpClientStreamableHttpTransport.java index c844367b8..2b8c15496 100644 --- a/mcp-core/src/main/java/io/modelcontextprotocol/client/transport/HttpClientStreamableHttpTransport.java +++ b/mcp-core/src/main/java/io/modelcontextprotocol/client/transport/HttpClientStreamableHttpTransport.java @@ -503,19 +503,7 @@ public Mono sendMessage(McpSchema.JSONRPCMessage sentMessage) { transportSession.sessionId().get()); } - // Extract method and params for Mcp-Name header - String method = null; - Object params = null; - if (sentMessage instanceof McpSchema.JSONRPCRequest request) { - method = request.method(); - params = request.params(); - } - else if (sentMessage instanceof McpSchema.JSONRPCNotification notification) { - method = notification.method(); - params = notification.params(); - } - - // Extract method and params for Mcp-Name header + // Extract method and params for MCP headers. String method = null; Object params = null; if (sentMessage instanceof McpSchema.JSONRPCRequest request) { diff --git a/mcp-core/src/main/java/io/modelcontextprotocol/spec/HttpHeaders.java b/mcp-core/src/main/java/io/modelcontextprotocol/spec/HttpHeaders.java index dfd471216..fa697bb23 100644 --- a/mcp-core/src/main/java/io/modelcontextprotocol/spec/HttpHeaders.java +++ b/mcp-core/src/main/java/io/modelcontextprotocol/spec/HttpHeaders.java @@ -14,8 +14,7 @@ public interface HttpHeaders { /** * Identifies individual MCP sessions. */ - String MCP_SESSION_ID = " - "; + String MCP_SESSION_ID = "mcp-session-id"; /** * Identifies events within an SSE Stream. diff --git a/mcp-test/src/test/java/io/modelcontextprotocol/client/transport/HttpClientStreamableHttpTransportHeaderTest.java b/mcp-test/src/test/java/io/modelcontextprotocol/client/transport/HttpClientStreamableHttpTransportHeaderTest.java deleted file mode 100644 index 5635d8691..000000000 --- a/mcp-test/src/test/java/io/modelcontextprotocol/client/transport/HttpClientStreamableHttpTransportHeaderTest.java +++ /dev/null @@ -1,70 +0,0 @@ -/* - * Copyright 2024-2026 the original author or authors. - */ - -package io.modelcontextprotocol.client.transport; - -import java.io.IOException; -import java.net.InetSocketAddress; -import java.nio.charset.StandardCharsets; - -import org.junit.jupiter.api.Test; - -import com.sun.net.httpserver.HttpServer; - -import io.modelcontextprotocol.spec.McpSchema; -import io.modelcontextprotocol.spec.ProtocolVersions; -import reactor.test.StepVerifier; - -class HttpClientStreamableHttpTransportHeaderTest { - - @Test - void sendsMcpMethodHeaderForInitializeRequests() throws IOException { - HttpServer server = HttpServer.create(new InetSocketAddress(0), 0); - try { - server.createContext("/mcp", exchange -> { - try (exchange) { - if (!"POST".equals(exchange.getRequestMethod())) { - exchange.sendResponseHeaders(405, -1); - return; - } - - String methodHeader = exchange.getRequestHeaders().getFirst("Mcp-Method"); - byte[] requestBody = exchange.getRequestBody().readAllBytes(); - String body = new String(requestBody, StandardCharsets.UTF_8); - - if (!"initialize".equals(methodHeader) || !body.contains("\"method\":\"initialize\"")) { - exchange.sendResponseHeaders(400, 0); - return; - } - - exchange.sendResponseHeaders(202, -1); - } - }); - server.start(); - - int port = server.getAddress().getPort(); - var transport = HttpClientStreamableHttpTransport.builder("http://localhost:" + port) - .endpoint("/mcp") - .build(); - try { - var initializeRequest = McpSchema.InitializeRequest - .builder(ProtocolVersions.MCP_2025_11_25, - McpSchema.ClientCapabilities.builder().roots(true).build(), - McpSchema.Implementation.builder("MCP Client", "0.3.1").build()) - .build(); - var testMessage = new McpSchema.JSONRPCRequest(McpSchema.METHOD_INITIALIZE, "test-id", - initializeRequest); - - StepVerifier.create(transport.sendMessage(testMessage)).verifyComplete(); - } - finally { - StepVerifier.create(transport.closeGracefully()).verifyComplete(); - } - } - finally { - server.stop(0); - } - } - -} diff --git a/mcp-test/src/test/java/io/modelcontextprotocol/client/transport/HttpClientStreamableHttpTransportTest.java b/mcp-test/src/test/java/io/modelcontextprotocol/client/transport/HttpClientStreamableHttpTransportTest.java index 2d3917362..601567781 100644 --- a/mcp-test/src/test/java/io/modelcontextprotocol/client/transport/HttpClientStreamableHttpTransportTest.java +++ b/mcp-test/src/test/java/io/modelcontextprotocol/client/transport/HttpClientStreamableHttpTransportTest.java @@ -171,7 +171,7 @@ void testCloseInitialized() { } @Test - void testMcpMethodHeaderIsAddedForInitializeRequest() throws IOException { + void sendsMcpMethodHeaderForInitializeRequests() throws IOException { HttpServer server = HttpServer.create(new InetSocketAddress(0), 0); try { server.createContext("/mcp", exchange -> { @@ -219,6 +219,50 @@ void testMcpMethodHeaderIsAddedForInitializeRequest() throws IOException { } } + @Test + void sendsMcpMethodHeaderForNotifications() throws IOException { + HttpServer server = HttpServer.create(new InetSocketAddress(0), 0); + try { + server.createContext("/mcp", exchange -> { + try (exchange) { + if (!"POST".equals(exchange.getRequestMethod())) { + exchange.sendResponseHeaders(405, -1); + return; + } + + String methodHeader = exchange.getRequestHeaders().getFirst("Mcp-Method"); + byte[] requestBody = exchange.getRequestBody().readAllBytes(); + String body = new String(requestBody, StandardCharsets.UTF_8); + + if (!McpSchema.METHOD_NOTIFICATION_INITIALIZED.equals(methodHeader) + || !body.contains("\"method\":\"notifications/initialized\"")) { + exchange.sendResponseHeaders(400, 0); + return; + } + + exchange.sendResponseHeaders(202, -1); + } + }); + server.start(); + + int port = server.getAddress().getPort(); + var transport = HttpClientStreamableHttpTransport.builder("http://localhost:" + port) + .endpoint("/mcp") + .build(); + try { + var testMessage = new McpSchema.JSONRPCNotification(McpSchema.METHOD_NOTIFICATION_INITIALIZED); + + StepVerifier.create(transport.sendMessage(testMessage)).verifyComplete(); + } + finally { + StepVerifier.create(transport.closeGracefully()).verifyComplete(); + } + } + finally { + server.stop(0); + } + } + @Test void testMcpNameHeaderIsAddedForToolsCall() throws IOException { HttpServer server = HttpServer.create(new InetSocketAddress(0), 0); From 15de3971ac0d9199c854e3ec6cf73448ae9ab8d0 Mon Sep 17 00:00:00 2001 From: cooleditphoto Date: Sun, 19 Jul 2026 21:16:19 -0400 Subject: [PATCH 4/4] add integration tests --- ...rverTransportSecurityIntegrationTests.java | 84 +++++++++++++++++++ 1 file changed, 84 insertions(+) diff --git a/mcp-test/src/test/java/io/modelcontextprotocol/server/transport/ServerTransportSecurityIntegrationTests.java b/mcp-test/src/test/java/io/modelcontextprotocol/server/transport/ServerTransportSecurityIntegrationTests.java index c1dcc7c19..3a3c6f15a 100644 --- a/mcp-test/src/test/java/io/modelcontextprotocol/server/transport/ServerTransportSecurityIntegrationTests.java +++ b/mcp-test/src/test/java/io/modelcontextprotocol/server/transport/ServerTransportSecurityIntegrationTests.java @@ -6,6 +6,7 @@ import java.net.URI; import java.net.http.HttpRequest; +import java.nio.charset.StandardCharsets; import java.time.Duration; import java.util.stream.Stream; @@ -17,6 +18,7 @@ import io.modelcontextprotocol.common.McpTransportContext; import io.modelcontextprotocol.json.McpJsonDefaults; import io.modelcontextprotocol.server.McpServer; +import io.modelcontextprotocol.spec.HttpHeaders; import io.modelcontextprotocol.spec.McpSchema; import jakarta.servlet.http.HttpServlet; import org.apache.catalina.LifecycleException; @@ -26,6 +28,8 @@ import org.junit.jupiter.api.AfterEach; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; +import org.springframework.mock.web.MockHttpServletRequest; +import org.springframework.mock.web.MockHttpServletResponse; import org.junit.jupiter.params.BeforeParameterizedClassInvocation; import org.junit.jupiter.params.Parameter; import org.junit.jupiter.params.ParameterizedClass; @@ -129,6 +133,86 @@ void hostAllowed() { assertThat(tools.tools()).isEmpty(); } + @Test + void rejectsInitializeRequestWithoutMcpMethodHeaderWhenRequired() throws Exception { + var provider = HttpServletStreamableServerTransportProvider.builder() + .mcpEndpoint("/mcp") + .requireMcpMethodHeader(true) + .build(); + MockHttpServletRequest request = new MockHttpServletRequest("POST", "/mcp"); + request.setContent( + "{\"jsonrpc\":\"2.0\",\"id\":\"1\",\"method\":\"initialize\",\"params\":{\"protocolVersion\":\"2025-11-25\",\"capabilities\":{},\"clientInfo\":{\"name\":\"test-client\",\"version\":\"1.0.0\"}}}" + .getBytes(StandardCharsets.UTF_8)); + request.addHeader("Accept", "application/json, text/event-stream"); + MockHttpServletResponse response = new MockHttpServletResponse(); + + provider.doPost(request, response); + + assertThat(response.getStatus()).isEqualTo(400); + assertThat(response.getContentAsString()).contains("Mcp-Method header required"); + } + + @Test + void rejectsNotificationWithoutMcpMethodHeaderWhenRequired() throws Exception { + var provider = HttpServletStreamableServerTransportProvider.builder() + .mcpEndpoint("/mcp") + .requireMcpMethodHeader(true) + .build(); + MockHttpServletRequest request = new MockHttpServletRequest("POST", "/mcp"); + request.setContent( + "{\"jsonrpc\":\"2.0\",\"method\":\"notifications/initialized\"}".getBytes(StandardCharsets.UTF_8)); + request.addHeader("Accept", "application/json, text/event-stream"); + MockHttpServletResponse response = new MockHttpServletResponse(); + + provider.doPost(request, response); + + assertThat(response.getStatus()).isEqualTo(400); + assertThat(response.getContentAsString()).contains("Mcp-Method header required"); + } + + @Test + void rejectsToolCallRequestWithoutMcpNameHeaderWhenRequired() throws Exception { + var provider = HttpServletStreamableServerTransportProvider.builder() + .mcpEndpoint("/mcp") + .requireMcpMethodHeader(true) + .requireMcpNameHeader(true) + .build(); + MockHttpServletRequest request = new MockHttpServletRequest("POST", "/mcp"); + request.setContent( + "{\"jsonrpc\":\"2.0\",\"id\":\"2\",\"method\":\"tools/call\",\"params\":{\"name\":\"echo\",\"arguments\":{\"message\":\"hello\"}}}" + .getBytes(StandardCharsets.UTF_8)); + request.addHeader(HttpHeaders.MCP_METHOD, McpSchema.METHOD_TOOLS_CALL); + request.addHeader("Accept", "application/json, text/event-stream"); + MockHttpServletResponse response = new MockHttpServletResponse(); + + provider.doPost(request, response); + + assertThat(response.getStatus()).isEqualTo(400); + assertThat(response.getContentAsString()).contains("Mcp-Name header required"); + } + + @Test + void rejectsToolCallRequestWithMismatchedMcpNameHeaderWhenRequired() throws Exception { + var provider = HttpServletStreamableServerTransportProvider.builder() + .mcpEndpoint("/mcp") + .requireMcpMethodHeader(true) + .requireMcpNameHeader(true) + .build(); + MockHttpServletRequest request = new MockHttpServletRequest("POST", "/mcp"); + request.setContent( + "{\"jsonrpc\":\"2.0\",\"id\":\"2\",\"method\":\"tools/call\",\"params\":{\"name\":\"echo\",\"arguments\":{\"message\":\"hello\"}}}" + .getBytes(StandardCharsets.UTF_8)); + request.addHeader(HttpHeaders.MCP_METHOD, McpSchema.METHOD_TOOLS_CALL); + request.addHeader(HttpHeaders.MCP_NAME, "wrong-name"); + request.addHeader("Accept", "application/json, text/event-stream"); + MockHttpServletResponse response = new MockHttpServletResponse(); + + provider.doPost(request, response); + + assertThat(response.getStatus()).isEqualTo(400); + assertThat(response.getContentAsString()).contains("Mcp-Name header mismatch"); + } + @Test void connectHostNotAllowed() { requestCustomizer.setHostHeader(DISALLOWED_HOST);