1414
1515import io .modelcontextprotocol .json .McpJsonDefaults ;
1616import io .modelcontextprotocol .json .McpJsonMapper ;
17+ import io .modelcontextprotocol .json .TypeRef ;
1718
1819import io .modelcontextprotocol .common .McpTransportContext ;
1920import io .modelcontextprotocol .server .McpStatelessServerHandler ;
2021import io .modelcontextprotocol .server .McpTransportContextExtractor ;
22+ import io .modelcontextprotocol .spec .HttpHeaders ;
2123import io .modelcontextprotocol .spec .McpError ;
2224import io .modelcontextprotocol .spec .McpSchema ;
2325import io .modelcontextprotocol .spec .McpStatelessServerTransport ;
@@ -161,6 +163,10 @@ protected void doPost(HttpServletRequest request, HttpServletResponse response)
161163 return ;
162164 }
163165
166+ if (!validateProtocolVersion (request , response )) {
167+ return ;
168+ }
169+
164170 try {
165171 Map <String , List <String >> headers = HttpServletRequestUtils .extractHeaders (request );
166172 this .securityValidator .validateHeaders (headers );
@@ -186,6 +192,12 @@ protected void doPost(HttpServletRequest request, HttpServletResponse response)
186192
187193 McpSchema .JSONRPCMessage message = McpSchema .deserializeJsonRpcMessage (jsonMapper , body );
188194
195+ // Per SEP-2243, reject header/body mismatches (missing headers are tolerated
196+ // so legacy clients keep working).
197+ if (!validateMcpHeaders (request , response , message )) {
198+ return ;
199+ }
200+
189201 if (message instanceof McpSchema .JSONRPCRequest jsonrpcRequest ) {
190202 try {
191203 McpSchema .JSONRPCResponse jsonrpcResponse = this .mcpHandler
@@ -266,6 +278,118 @@ private void responseError(HttpServletResponse response, int httpCode, McpError
266278 writer .flush ();
267279 }
268280
281+ /**
282+ * Validates the {@code MCP-Protocol-Version} header against the protocol versions
283+ * supported by this transport. A missing header is allowed and falls back to the
284+ * negotiated protocol version, while a header carrying an unsupported version is
285+ * rejected with a 400 Bad Request.
286+ * @param request the HTTP servlet request
287+ * @param response the HTTP servlet response
288+ * @return true if the header is missing or contains a supported version, false if a
289+ * 400 error response has been written
290+ * @throws IOException if an I/O error occurs
291+ */
292+ private boolean validateProtocolVersion (HttpServletRequest request , HttpServletResponse response )
293+ throws IOException {
294+ String protocolVersion = request .getHeader (HttpHeaders .PROTOCOL_VERSION );
295+ if (protocolVersion == null || this .protocolVersions ().contains (protocolVersion )) {
296+ return true ;
297+ }
298+ this .responseError (response , HttpServletResponse .SC_BAD_REQUEST ,
299+ McpError .builder (McpSchema .ErrorCodes .METHOD_NOT_FOUND )
300+ .message ("Unsupported protocol version (supported versions: "
301+ + String .join (", " , this .protocolVersions ()) + ")" )
302+ .build ());
303+ return false ;
304+ }
305+
306+ /**
307+ * Validates the SEP-2243 {@code Mcp-Method} and {@code Mcp-Name} request headers
308+ * against the deserialized message body. Missing headers are permitted for backwards
309+ * compatibility with legacy clients, but any header that is supplied must match the
310+ * corresponding payload attribute. Mismatches are rejected with a 400 Bad Request.
311+ * @param request the incoming servlet request
312+ * @param response the servlet response used to write an error payload if validation
313+ * fails
314+ * @param message the parsed JSON-RPC message
315+ * @return {@code true} if validation passed, {@code false} if a 400 response was
316+ * written
317+ * @throws IOException if writing the error response fails
318+ */
319+ private boolean validateMcpHeaders (HttpServletRequest request , HttpServletResponse response ,
320+ McpSchema .JSONRPCMessage message ) throws IOException {
321+ String method = message instanceof McpSchema .JSONRPCRequest req ? req .method ()
322+ : message instanceof McpSchema .JSONRPCNotification notif ? notif .method () : null ;
323+
324+ if (method == null ) {
325+ return true ;
326+ }
327+
328+ String methodHeader = request .getHeader (HttpHeaders .MCP_METHOD );
329+ if (methodHeader != null && !methodHeader .isBlank () && !method .equals (methodHeader )) {
330+ this .responseError (response , HttpServletResponse .SC_BAD_REQUEST ,
331+ McpError .builder (McpSchema .ErrorCodes .INVALID_REQUEST )
332+ .message ("Mcp-Method header mismatch: expected '" + method + "' but was '" + methodHeader + "'" )
333+ .build ());
334+ return false ;
335+ }
336+
337+ Object params = message instanceof McpSchema .JSONRPCRequest req ? req .params ()
338+ : message instanceof McpSchema .JSONRPCNotification notif ? notif .params () : null ;
339+ String name = extractNameFromParams (method , params );
340+ if (name != null ) {
341+ String nameHeader = request .getHeader (HttpHeaders .MCP_NAME );
342+ if (nameHeader != null && !nameHeader .isBlank () && !name .equals (nameHeader )) {
343+ this .responseError (response , HttpServletResponse .SC_BAD_REQUEST ,
344+ McpError .builder (McpSchema .ErrorCodes .INVALID_REQUEST )
345+ .message ("Mcp-Name header mismatch: expected '" + name + "' but was '" + nameHeader + "'" )
346+ .build ());
347+ return false ;
348+ }
349+ }
350+
351+ return true ;
352+ }
353+
354+ /**
355+ * Extracts the name or URI of the tool, prompt, or resource referenced by a request,
356+ * as used to validate the SEP-2243 {@code Mcp-Name} header.
357+ * @param method the JSON-RPC method of the request
358+ * @param params the request parameters
359+ * @return the target name or URI when the method references one, otherwise
360+ * {@code null}
361+ */
362+ private String extractNameFromParams (String method , Object params ) {
363+ if (params == null ) {
364+ return null ;
365+ }
366+
367+ try {
368+ return switch (method ) {
369+ case McpSchema .METHOD_TOOLS_CALL ->
370+ this .jsonMapper .convertValue (params , new TypeRef <McpSchema .CallToolRequest >() {
371+ }).name ();
372+ case McpSchema .METHOD_PROMPT_GET ->
373+ this .jsonMapper .convertValue (params , new TypeRef <McpSchema .GetPromptRequest >() {
374+ }).name ();
375+ case McpSchema .METHOD_RESOURCES_READ ->
376+ this .jsonMapper .convertValue (params , new TypeRef <McpSchema .ReadResourceRequest >() {
377+ }).uri ();
378+ case McpSchema .METHOD_RESOURCES_SUBSCRIBE ->
379+ this .jsonMapper .convertValue (params , new TypeRef <McpSchema .SubscribeRequest >() {
380+ }).uri ();
381+ case McpSchema .METHOD_RESOURCES_UNSUBSCRIBE ->
382+ this .jsonMapper .convertValue (params , new TypeRef <McpSchema .UnsubscribeRequest >() {
383+ }).uri ();
384+ default -> null ;
385+ };
386+ }
387+ catch (Exception e ) {
388+ logger .debug ("Failed to extract name from params for method {}: {}" , method , e .getMessage ());
389+ return null ;
390+ }
391+ }
392+
269393 /**
270394 * Cleans up resources when the servlet is being destroyed.
271395 * <p>
0 commit comments