Skip to content
Open
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 @@ -503,14 +503,47 @@ public Mono<Void> 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
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));
Expand Down Expand Up @@ -699,6 +732,48 @@ public <T> T unmarshalFrom(Object data, TypeRef<T> 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<McpSchema.CallToolRequest>() {
});
return request.name();
}
case McpSchema.METHOD_RESOURCES_READ -> {
McpSchema.ReadResourceRequest request = this.jsonMapper.convertValue(params,
new TypeRef<McpSchema.ReadResourceRequest>() {
});
return request.uri();
}
case McpSchema.METHOD_PROMPT_GET -> {
McpSchema.GetPromptRequest request = this.jsonMapper.convertValue(params,
new TypeRef<McpSchema.GetPromptRequest>() {
});
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}.
*/
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -125,6 +125,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
Expand All @@ -140,7 +144,8 @@ public class HttpServletStreamableServerTransportProvider extends HttpServlet
*/
private HttpServletStreamableServerTransportProvider(McpJsonMapper jsonMapper, String mcpEndpoint,
boolean disallowDelete, McpTransportContextExtractor<HttpServletRequest> 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");
Expand All @@ -151,6 +156,8 @@ private HttpServletStreamableServerTransportProvider(McpJsonMapper jsonMapper, S
this.disallowDelete = disallowDelete;
this.contextExtractor = contextExtractor;
this.securityValidator = securityValidator;
this.requireMcpNameHeader = requireMcpNameHeader;
this.requireMcpMethodHeader = requireMcpMethodHeader;

if (keepAliveInterval != null) {

Expand Down Expand Up @@ -383,6 +390,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<McpSchema.CallToolRequest>() {
});
return request.name();
}
case McpSchema.METHOD_RESOURCES_READ -> {
McpSchema.ReadResourceRequest request = jsonMapper.convertValue(params,
new TypeRef<McpSchema.ReadResourceRequest>() {
});
return request.uri();
}
case McpSchema.METHOD_PROMPT_GET -> {
McpSchema.GetPromptRequest request = jsonMapper.convertValue(params,
new TypeRef<McpSchema.GetPromptRequest>() {
});
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 {
Expand Down Expand Up @@ -429,6 +531,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)) {
Expand Down Expand Up @@ -823,6 +942,10 @@ public static class Builder {

private boolean disallowDelete = false;

private boolean requireMcpNameHeader = false;

private boolean requireMcpMethodHeader = false;

private McpTransportContextExtractor<HttpServletRequest> contextExtractor = (
serverRequest) -> McpTransportContext.EMPTY;

Expand Down Expand Up @@ -901,6 +1024,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.
Expand All @@ -911,7 +1058,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);
}

}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,8 @@ public interface HttpHeaders {
/**
* Identifies individual MCP sessions.
*/
String MCP_SESSION_ID = "Mcp-Session-Id";
String MCP_SESSION_ID = "
";

/**
* Identifies events within an SSE Stream.
Expand All @@ -26,6 +27,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 <a href=
Expand Down
Loading