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
3 changes: 3 additions & 0 deletions docs/client.md
Original file line number Diff line number Diff line change
Expand Up @@ -165,6 +165,9 @@ McpTransport transport = new StdioClientTransport(params, McpJsonDefaults.getMap
- Configurable connect timeout
- Custom HTTP request customization
- Multiple protocol version negotiation
- SEP-2243 header mirroring: every POST carries an `Mcp-Method` header, and requests
targeting a tool, prompt, or resource also carry `Mcp-Name` (the name or URI), so
servers can validate the headers against the body without parsing it.

=== "Streamable WebClient (external)"

Expand Down
5 changes: 5 additions & 0 deletions docs/server.md
Original file line number Diff line number Diff line change
Expand Up @@ -167,6 +167,11 @@ Key features:
- Configurable keep-alive intervals
- Security validation support
- Graceful shutdown support
- SEP-2243 validation: the servlet transport rejects requests whose present
`Mcp-Method` / `Mcp-Name` headers do not mirror the request body, and rejects
unsupported `MCP-Protocol-Version` values. Missing headers are tolerated so legacy
clients keep working. Call `.requireMcpHeaders(true)` on the builder to also
reject requests that omit them.

=== "Streamable HTTP WebFlux (external)"

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -543,11 +543,32 @@ public Mono<Void> sendMessage(McpSchema.JSONRPCMessage sentMessage) {
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.CACHE_CONTROL, "no-cache");
// Per the Streamable HTTP transport spec, the MCP-Protocol-Version header
// is required on all requests after initialization completes. The
// initialize request itself carries no negotiated version yet -- the
// client's supported versions are conveyed in the request body for
// server-side negotiation -- so the header must not be sent.
if (!(sentMessage instanceof McpSchema.JSONRPCRequest jsonrpcMessage
&& McpSchema.METHOD_INITIALIZE.equals(jsonrpcMessage.method()))) {
builder = builder.header(HttpHeaders.PROTOCOL_VERSION, ctx
.getOrDefault(McpAsyncClient.NEGOTIATED_PROTOCOL_VERSION, this.latestSupportedProtocolVersion));
}
// Per SEP-2243, mirror the JSON-RPC method and, where applicable, the
// target name/URI in dedicated headers so the server can validate
// them without parsing the body.
if (sentMessage instanceof McpSchema.JSONRPCRequest jsonrpcRequest) {
builder = builder.header(HttpHeaders.MCP_METHOD, jsonrpcRequest.method());
String name = extractNameFromParams(jsonrpcRequest.method(), jsonrpcRequest.params());
if (name != null) {
builder = builder.header(HttpHeaders.MCP_NAME, HttpHeaders.encodeHeaderValue(name));
}
}
else if (sentMessage instanceof McpSchema.JSONRPCNotification jsonrpcNotification) {
builder = builder.header(HttpHeaders.MCP_METHOD, jsonrpcNotification.method());
}

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 @@ -740,6 +761,45 @@ public <T> T unmarshalFrom(Object data, TypeRef<T> typeRef) {
return this.jsonMapper.convertValue(data, typeRef);
}

/**
* Extracts the name or URI of the tool, prompt, or resource referenced by a request,
* used to populate the SEP-2243 {@code Mcp-Name} header.
* @param method the JSON-RPC method of the request
* @param params the request parameters
* @return the target name or URI when the method references one, otherwise
* {@code null}
*/
private String extractNameFromParams(String method, Object params) {
if (params == null) {
return null;
}

try {
return switch (method) {
case McpSchema.METHOD_TOOLS_CALL ->
this.jsonMapper.convertValue(params, new TypeRef<McpSchema.CallToolRequest>() {
}).name();
case McpSchema.METHOD_PROMPT_GET ->
this.jsonMapper.convertValue(params, new TypeRef<McpSchema.GetPromptRequest>() {
}).name();
case McpSchema.METHOD_RESOURCES_READ ->
this.jsonMapper.convertValue(params, new TypeRef<McpSchema.ReadResourceRequest>() {
}).uri();
case McpSchema.METHOD_RESOURCES_SUBSCRIBE ->
this.jsonMapper.convertValue(params, new TypeRef<McpSchema.SubscribeRequest>() {
}).uri();
case McpSchema.METHOD_RESOURCES_UNSUBSCRIBE ->
this.jsonMapper.convertValue(params, new TypeRef<McpSchema.UnsubscribeRequest>() {
}).uri();
default -> 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 @@ -69,6 +69,12 @@ public class HttpServletStatelessServerTransport extends HttpServlet implements
*/
private final ServerHttpHeaderValidator httpHeaderValidator;

/**
* Validator for the SEP-2243 {@code MCP-Protocol-Version}, {@code Mcp-Method}, and
* {@code Mcp-Name} header checks that need the parsed JSON-RPC body.
*/
private final Sep2243RequestValidator sep2243Validator;

/**
* Maximum size, in bytes, of a single request body accepted by this transport.
*/
Expand All @@ -84,11 +90,13 @@ public class HttpServletStatelessServerTransport extends HttpServlet implements
* @param httpHeaderValidator The HTTP header validator for validating HTTP requests.
* @param requestMaxSize The maximum size, in bytes, of a single request body. Must be
* positive.
* @param requireMcpHeaders Whether a POST lacking the SEP-2243 {@code Mcp-Method} /
* {@code Mcp-Name} headers is rejected instead of tolerated.
* @throws IllegalArgumentException if any parameter is null
*/
private HttpServletStatelessServerTransport(McpJsonMapper jsonMapper, String mcpEndpoint,
McpTransportContextExtractor<HttpServletRequest> contextExtractor,
ServerHttpHeaderValidator httpHeaderValidator, int requestMaxSize) {
ServerHttpHeaderValidator httpHeaderValidator, int requestMaxSize, boolean requireMcpHeaders) {
Assert.notNull(jsonMapper, "jsonMapper must not be null");
Assert.notNull(mcpEndpoint, "mcpEndpoint must not be null");
Assert.notNull(contextExtractor, "contextExtractor must not be null");
Expand All @@ -100,6 +108,7 @@ private HttpServletStatelessServerTransport(McpJsonMapper jsonMapper, String mcp
this.contextExtractor = contextExtractor;
this.httpHeaderValidator = httpHeaderValidator;
this.requestMaxSize = requestMaxSize;
this.sep2243Validator = new Sep2243RequestValidator(jsonMapper, this::protocolVersions, requireMcpHeaders);
}

@Override
Expand Down Expand Up @@ -184,6 +193,24 @@ protected void doPost(HttpServletRequest request, HttpServletResponse response)

McpSchema.JSONRPCMessage message = McpSchema.deserializeJsonRpcMessage(jsonMapper, body);

// The MCP-Protocol-Version header can only be strictly validated once a
// version has been negotiated; 'initialize' requests are exempt.
HttpServletHeaderAccessor headerAccessor = new HttpServletHeaderAccessor(request);
McpError protocolVersionError = this.sep2243Validator.validateProtocolVersion(headerAccessor,
Sep2243RequestValidator.isInitializeRequest(message));
if (protocolVersionError != null) {
this.responseError(response, HttpServletResponse.SC_BAD_REQUEST, protocolVersionError);
return;
}

// Per SEP-2243, reject header/body mismatches (missing headers are tolerated
// by default so legacy clients keep working).
McpError mirroringError = this.sep2243Validator.validateMirroringHeaders(headerAccessor, message);
if (mirroringError != null) {
this.responseError(response, HttpServletResponse.SC_BAD_REQUEST, mirroringError);
return;
}

if (message instanceof McpSchema.JSONRPCRequest jsonrpcRequest) {
try {
McpSchema.JSONRPCResponse jsonrpcResponse = this.mcpHandler
Expand Down Expand Up @@ -302,6 +329,8 @@ public static class Builder {

private int requestMaxSize = DEFAULT_REQUEST_MAX_SIZE;

private boolean requireMcpHeaders = false;

private Builder() {
// used by a static method
}
Expand Down Expand Up @@ -387,6 +416,22 @@ public Builder maxRequestSize(int requestMaxSize) {
return this;
}

/**
* Opt-in strict mode for SEP-2243. When enabled, a POST whose JSON-RPC request or
* notification carries no {@code Mcp-Method} header, or targets a tool, prompt or
* resource without an {@code Mcp-Name} header, is rejected with HTTP 400 and
* error code {@code HEADER_MISMATCH} (-32020). Disabled by default so that
* clients that do not send these headers keep working. A present header that
* mismatches the body is always rejected regardless of this setting.
* @param requireMcpHeaders whether to require the SEP-2243 {@code Mcp-Method} /
* {@code Mcp-Name} headers
* @return this builder instance
*/
public Builder requireMcpHeaders(boolean requireMcpHeaders) {
this.requireMcpHeaders = requireMcpHeaders;
return this;
}

/**
* Builds a new instance of {@link HttpServletStatelessServerTransport} with the
* configured settings.
Expand All @@ -397,7 +442,7 @@ public HttpServletStatelessServerTransport build() {
Assert.notNull(mcpEndpoint, "Message endpoint must be set");
return new HttpServletStatelessServerTransport(
jsonMapper == null ? McpJsonDefaults.getMapper() : jsonMapper, mcpEndpoint, contextExtractor,
httpHeaderValidator, requestMaxSize);
httpHeaderValidator, requestMaxSize, requireMcpHeaders);
}

}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -132,6 +132,12 @@ public class HttpServletStreamableServerTransportProvider extends HttpServlet
*/
private final ServerHttpHeaderValidator httpHeaderValidator;

/**
* Validator for the SEP-2243 {@code MCP-Protocol-Version}, {@code Mcp-Method}, and
* {@code Mcp-Name} header checks that need the parsed JSON-RPC body.
*/
private final Sep2243RequestValidator sep2243Validator;

/**
* Constructs a new HttpServletStreamableServerTransportProvider instance.
* @param jsonMapper The JsonMapper to use for JSON serialization/deserialization of
Expand All @@ -145,11 +151,14 @@ public class HttpServletStreamableServerTransportProvider extends HttpServlet
* @param httpHeaderValidator The HTTP header validator for validating HTTP requests.
* @param requestMaxSize The maximum size, in bytes, of a single request body. Must be
* positive.
* @param requireMcpHeaders Whether a POST lacking the SEP-2243 {@code Mcp-Method} /
* {@code Mcp-Name} headers is rejected instead of tolerated.
* @throws IllegalArgumentException if any parameter is null
*/
private HttpServletStreamableServerTransportProvider(McpJsonMapper jsonMapper, String mcpEndpoint,
boolean disallowDelete, McpTransportContextExtractor<HttpServletRequest> contextExtractor,
Duration keepAliveInterval, ServerHttpHeaderValidator httpHeaderValidator, int requestMaxSize) {
Duration keepAliveInterval, ServerHttpHeaderValidator httpHeaderValidator, int requestMaxSize,
boolean requireMcpHeaders) {
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 @@ -162,6 +171,7 @@ private HttpServletStreamableServerTransportProvider(McpJsonMapper jsonMapper, S
this.contextExtractor = contextExtractor;
this.httpHeaderValidator = httpHeaderValidator;
this.requestMaxSize = requestMaxSize;
this.sep2243Validator = new Sep2243RequestValidator(jsonMapper, this::protocolVersions, requireMcpHeaders);

if (keepAliveInterval != null) {

Expand Down Expand Up @@ -273,6 +283,13 @@ protected void doGet(HttpServletRequest request, HttpServletResponse response)
return;
}

McpError protocolVersionError = this.sep2243Validator
.validateProtocolVersion(new HttpServletHeaderAccessor(request), false);
if (protocolVersionError != null) {
this.responseError(response, HttpServletResponse.SC_BAD_REQUEST, protocolVersionError);
return;
}

try {
this.httpHeaderValidator.validate(new HttpServletHeaderAccessor(request));
}
Expand Down Expand Up @@ -448,6 +465,24 @@ protected void doPost(HttpServletRequest request, HttpServletResponse response)

McpSchema.JSONRPCMessage message = McpSchema.deserializeJsonRpcMessage(jsonMapper, body);

// The MCP-Protocol-Version header can only be strictly validated once a
// version has been negotiated; 'initialize' requests are exempt.
HttpServletHeaderAccessor headerAccessor = new HttpServletHeaderAccessor(request);
McpError protocolVersionError = this.sep2243Validator.validateProtocolVersion(headerAccessor,
Sep2243RequestValidator.isInitializeRequest(message));
if (protocolVersionError != null) {
this.responseError(response, HttpServletResponse.SC_BAD_REQUEST, protocolVersionError);
return;
}

// Per SEP-2243, reject header/body mismatches (missing headers are tolerated
// by default so legacy clients keep working).
McpError mirroringError = this.sep2243Validator.validateMirroringHeaders(headerAccessor, message);
if (mirroringError != null) {
this.responseError(response, HttpServletResponse.SC_BAD_REQUEST, mirroringError);
return;
}

// Handle initialization request
if (message instanceof McpSchema.JSONRPCRequest jsonrpcRequest
&& jsonrpcRequest.method().equals(McpSchema.METHOD_INITIALIZE)) {
Expand Down Expand Up @@ -601,6 +636,13 @@ protected void doDelete(HttpServletRequest request, HttpServletResponse response
return;
}

McpError protocolVersionError = this.sep2243Validator
.validateProtocolVersion(new HttpServletHeaderAccessor(request), false);
if (protocolVersionError != null) {
this.responseError(response, HttpServletResponse.SC_BAD_REQUEST, protocolVersionError);
return;
}

try {
this.httpHeaderValidator.validate(new HttpServletHeaderAccessor(request));
}
Expand Down Expand Up @@ -859,6 +901,8 @@ public static class Builder {

private int requestMaxSize = DEFAULT_REQUEST_MAX_SIZE;

private boolean requireMcpHeaders = false;

/**
* Sets the JsonMapper to use for JSON serialization/deserialization of MCP
* messages.
Expand Down Expand Up @@ -958,6 +1002,22 @@ public Builder maxRequestSize(int requestMaxSize) {
return this;
}

/**
* Opt-in strict mode for SEP-2243. When enabled, a POST whose JSON-RPC request or
* notification carries no {@code Mcp-Method} header, or targets a tool, prompt or
* resource without an {@code Mcp-Name} header, is rejected with HTTP 400 and
* error code {@code HEADER_MISMATCH} (-32020). Disabled by default so that
* clients that do not send these headers keep working. A present header that
* mismatches the body is always rejected regardless of this setting.
* @param requireMcpHeaders whether to require the SEP-2243 {@code Mcp-Method} /
* {@code Mcp-Name} headers
* @return this builder instance
*/
public Builder requireMcpHeaders(boolean requireMcpHeaders) {
this.requireMcpHeaders = requireMcpHeaders;
return this;
}

/**
* Builds a new instance of {@link HttpServletStreamableServerTransportProvider}
* with the configured settings.
Expand All @@ -968,7 +1028,7 @@ public HttpServletStreamableServerTransportProvider build() {
Assert.notNull(this.mcpEndpoint, "MCP endpoint must be set");
return new HttpServletStreamableServerTransportProvider(
jsonMapper == null ? McpJsonDefaults.getMapper() : jsonMapper, mcpEndpoint, disallowDelete,
contextExtractor, keepAliveInterval, httpHeaderValidator, requestMaxSize);
contextExtractor, keepAliveInterval, httpHeaderValidator, requestMaxSize, requireMcpHeaders);
}

}
Expand Down
Loading
Loading