1313
1414import io .modelcontextprotocol .json .McpJsonDefaults ;
1515import io .modelcontextprotocol .json .McpJsonMapper ;
16- import io .modelcontextprotocol .json .TypeRef ;
1716
1817import io .modelcontextprotocol .common .McpTransportContext ;
1918import io .modelcontextprotocol .server .McpStatelessServerHandler ;
2019import io .modelcontextprotocol .server .McpTransportContextExtractor ;
21- import io .modelcontextprotocol .spec .HttpHeaders ;
2220import io .modelcontextprotocol .spec .McpError ;
2321import io .modelcontextprotocol .spec .McpSchema ;
2422import io .modelcontextprotocol .spec .McpStatelessServerTransport ;
@@ -71,6 +69,12 @@ public class HttpServletStatelessServerTransport extends HttpServlet implements
7169 */
7270 private final ServerHttpHeaderValidator httpHeaderValidator ;
7371
72+ /**
73+ * Validator for the SEP-2243 {@code MCP-Protocol-Version}, {@code Mcp-Method}, and
74+ * {@code Mcp-Name} header checks that need the parsed JSON-RPC body.
75+ */
76+ private final Sep2243RequestValidator sep2243Validator ;
77+
7478 /**
7579 * Maximum size, in bytes, of a single request body accepted by this transport.
7680 */
@@ -86,11 +90,13 @@ public class HttpServletStatelessServerTransport extends HttpServlet implements
8690 * @param httpHeaderValidator The HTTP header validator for validating HTTP requests.
8791 * @param requestMaxSize The maximum size, in bytes, of a single request body. Must be
8892 * positive.
93+ * @param requireMcpHeaders Whether a POST lacking the SEP-2243 {@code Mcp-Method} /
94+ * {@code Mcp-Name} headers is rejected instead of tolerated.
8995 * @throws IllegalArgumentException if any parameter is null
9096 */
9197 private HttpServletStatelessServerTransport (McpJsonMapper jsonMapper , String mcpEndpoint ,
9298 McpTransportContextExtractor <HttpServletRequest > contextExtractor ,
93- ServerHttpHeaderValidator httpHeaderValidator , int requestMaxSize ) {
99+ ServerHttpHeaderValidator httpHeaderValidator , int requestMaxSize , boolean requireMcpHeaders ) {
94100 Assert .notNull (jsonMapper , "jsonMapper must not be null" );
95101 Assert .notNull (mcpEndpoint , "mcpEndpoint must not be null" );
96102 Assert .notNull (contextExtractor , "contextExtractor must not be null" );
@@ -102,6 +108,7 @@ private HttpServletStatelessServerTransport(McpJsonMapper jsonMapper, String mcp
102108 this .contextExtractor = contextExtractor ;
103109 this .httpHeaderValidator = httpHeaderValidator ;
104110 this .requestMaxSize = requestMaxSize ;
111+ this .sep2243Validator = new Sep2243RequestValidator (jsonMapper , this ::protocolVersions , requireMcpHeaders );
105112 }
106113
107114 @ Override
@@ -187,18 +194,20 @@ protected void doPost(HttpServletRequest request, HttpServletResponse response)
187194 McpSchema .JSONRPCMessage message = McpSchema .deserializeJsonRpcMessage (jsonMapper , body );
188195
189196 // The MCP-Protocol-Version header can only be strictly validated once a
190- // version has been negotiated; during 'initialize' the client advertises its
191- // versions in the request body and any header value is resolved by regular
192- // version negotiation instead of being rejected.
193- boolean initializationRequest = message instanceof McpSchema . JSONRPCRequest initRequestCheck
194- && McpSchema . METHOD_INITIALIZE . equals ( initRequestCheck . method ());
195- if (! initializationRequest && ! validateProtocolVersion ( request , response )) {
197+ // version has been negotiated; 'initialize' requests are exempt.
198+ HttpServletHeaderAccessor headerAccessor = new HttpServletHeaderAccessor ( request );
199+ McpError protocolVersionError = this . sep2243Validator . validateProtocolVersion ( headerAccessor ,
200+ Sep2243RequestValidator . isInitializeRequest ( message ));
201+ if ( protocolVersionError != null ) {
202+ this . responseError ( response , HttpServletResponse . SC_BAD_REQUEST , protocolVersionError );
196203 return ;
197204 }
198205
199206 // Per SEP-2243, reject header/body mismatches (missing headers are tolerated
200- // so legacy clients keep working).
201- if (!validateMcpHeaders (request , response , message )) {
207+ // by default so legacy clients keep working).
208+ McpError mirroringError = this .sep2243Validator .validateMirroringHeaders (headerAccessor , message );
209+ if (mirroringError != null ) {
210+ this .responseError (response , HttpServletResponse .SC_BAD_REQUEST , mirroringError );
202211 return ;
203212 }
204213
@@ -282,124 +291,6 @@ private void responseError(HttpServletResponse response, int httpCode, McpError
282291 writer .flush ();
283292 }
284293
285- /**
286- * Validates the {@code MCP-Protocol-Version} header against the protocol versions
287- * supported by this transport. A missing header is allowed and falls back to the
288- * negotiated protocol version, while a header carrying an unsupported version is
289- * rejected with a 400 Bad Request. Initialize requests are exempt: no version has
290- * been negotiated yet, so any header value carried on them is resolved through
291- * regular body-based version negotiation.
292- * @param request the HTTP servlet request
293- * @param response the HTTP servlet response
294- * @return true if the header is missing or contains a supported version, false if a
295- * 400 error response has been written
296- * @throws IOException if an I/O error occurs
297- */
298- private boolean validateProtocolVersion (HttpServletRequest request , HttpServletResponse response )
299- throws IOException {
300- String protocolVersion = request .getHeader (HttpHeaders .PROTOCOL_VERSION );
301- if (protocolVersion == null || this .protocolVersions ().contains (protocolVersion )) {
302- return true ;
303- }
304- this .responseError (response , HttpServletResponse .SC_BAD_REQUEST ,
305- McpError .builder (McpSchema .ErrorCodes .METHOD_NOT_FOUND )
306- .message ("Unsupported protocol version (supported versions: "
307- + String .join (", " , this .protocolVersions ()) + ")" )
308- .build ());
309- return false ;
310- }
311-
312- /**
313- * Validates the SEP-2243 {@code Mcp-Method} and {@code Mcp-Name} request headers
314- * against the deserialized message body. Missing headers are permitted for backwards
315- * compatibility with legacy clients, but any header that is supplied must match the
316- * corresponding payload attribute. Mismatches are rejected with a 400 Bad Request.
317- * @param request the incoming servlet request
318- * @param response the servlet response used to write an error payload if validation
319- * fails
320- * @param message the parsed JSON-RPC message
321- * @return {@code true} if validation passed, {@code false} if a 400 response was
322- * written
323- * @throws IOException if writing the error response fails
324- */
325- private boolean validateMcpHeaders (HttpServletRequest request , HttpServletResponse response ,
326- McpSchema .JSONRPCMessage message ) throws IOException {
327- String method = message instanceof McpSchema .JSONRPCRequest req ? req .method ()
328- : message instanceof McpSchema .JSONRPCNotification notif ? notif .method () : null ;
329-
330- if (method == null ) {
331- return true ;
332- }
333-
334- String methodHeader = request .getHeader (HttpHeaders .MCP_METHOD );
335- if (methodHeader != null && !methodHeader .isBlank () && !method .equals (methodHeader )) {
336- this .responseError (response , HttpServletResponse .SC_BAD_REQUEST ,
337- McpError .builder (McpSchema .ErrorCodes .HEADER_MISMATCH )
338- .message ("Mcp-Method header mismatch: expected '" + method + "' but was '" + methodHeader + "'" )
339- .build ());
340- return false ;
341- }
342-
343- Object params = message instanceof McpSchema .JSONRPCRequest req ? req .params ()
344- : message instanceof McpSchema .JSONRPCNotification notif ? notif .params () : null ;
345- String name = extractNameFromParams (method , params );
346- if (name != null ) {
347- String nameHeader = request .getHeader (HttpHeaders .MCP_NAME );
348- if (nameHeader != null && !nameHeader .isBlank ()) {
349- String decodedName = HttpHeaders .decodeHeaderValue (nameHeader );
350- if (!name .equals (decodedName )) {
351- this .responseError (response , HttpServletResponse .SC_BAD_REQUEST ,
352- McpError .builder (McpSchema .ErrorCodes .HEADER_MISMATCH )
353- .message ("Mcp-Name header mismatch: expected '" + name + "' but was '" + nameHeader
354- + "'" )
355- .build ());
356- return false ;
357- }
358- }
359- }
360-
361- return true ;
362- }
363-
364- /**
365- * Extracts the name or URI of the tool, prompt, or resource referenced by a request,
366- * as used to validate the SEP-2243 {@code Mcp-Name} header.
367- * @param method the JSON-RPC method of the request
368- * @param params the request parameters
369- * @return the target name or URI when the method references one, otherwise
370- * {@code null}
371- */
372- private String extractNameFromParams (String method , Object params ) {
373- if (params == null ) {
374- return null ;
375- }
376-
377- try {
378- return switch (method ) {
379- case McpSchema .METHOD_TOOLS_CALL ->
380- this .jsonMapper .convertValue (params , new TypeRef <McpSchema .CallToolRequest >() {
381- }).name ();
382- case McpSchema .METHOD_PROMPT_GET ->
383- this .jsonMapper .convertValue (params , new TypeRef <McpSchema .GetPromptRequest >() {
384- }).name ();
385- case McpSchema .METHOD_RESOURCES_READ ->
386- this .jsonMapper .convertValue (params , new TypeRef <McpSchema .ReadResourceRequest >() {
387- }).uri ();
388- case McpSchema .METHOD_RESOURCES_SUBSCRIBE ->
389- this .jsonMapper .convertValue (params , new TypeRef <McpSchema .SubscribeRequest >() {
390- }).uri ();
391- case McpSchema .METHOD_RESOURCES_UNSUBSCRIBE ->
392- this .jsonMapper .convertValue (params , new TypeRef <McpSchema .UnsubscribeRequest >() {
393- }).uri ();
394- default -> null ;
395- };
396- }
397- catch (Exception e ) {
398- logger .debug ("Failed to extract name from params for method {}: {}" , method , e .getMessage ());
399- return null ;
400- }
401- }
402-
403294 /**
404295 * Cleans up resources when the servlet is being destroyed.
405296 * <p>
@@ -438,6 +329,8 @@ public static class Builder {
438329
439330 private int requestMaxSize = DEFAULT_REQUEST_MAX_SIZE ;
440331
332+ private boolean requireMcpHeaders = false ;
333+
441334 private Builder () {
442335 // used by a static method
443336 }
@@ -523,6 +416,22 @@ public Builder maxRequestSize(int requestMaxSize) {
523416 return this ;
524417 }
525418
419+ /**
420+ * Opt-in strict mode for SEP-2243. When enabled, a POST whose JSON-RPC request or
421+ * notification carries no {@code Mcp-Method} header, or targets a tool, prompt or
422+ * resource without an {@code Mcp-Name} header, is rejected with HTTP 400 and
423+ * error code {@code HEADER_MISMATCH} (-32020). Disabled by default so that
424+ * clients that do not send these headers keep working. A present header that
425+ * mismatches the body is always rejected regardless of this setting.
426+ * @param requireMcpHeaders whether to require the SEP-2243 {@code Mcp-Method} /
427+ * {@code Mcp-Name} headers
428+ * @return this builder instance
429+ */
430+ public Builder requireMcpHeaders (boolean requireMcpHeaders ) {
431+ this .requireMcpHeaders = requireMcpHeaders ;
432+ return this ;
433+ }
434+
526435 /**
527436 * Builds a new instance of {@link HttpServletStatelessServerTransport} with the
528437 * configured settings.
@@ -533,7 +442,7 @@ public HttpServletStatelessServerTransport build() {
533442 Assert .notNull (mcpEndpoint , "Message endpoint must be set" );
534443 return new HttpServletStatelessServerTransport (
535444 jsonMapper == null ? McpJsonDefaults .getMapper () : jsonMapper , mcpEndpoint , contextExtractor ,
536- httpHeaderValidator , requestMaxSize );
445+ httpHeaderValidator , requestMaxSize , requireMcpHeaders );
537446 }
538447
539448 }
0 commit comments