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