@@ -125,6 +125,14 @@ private Mono<McpTransportResponse> handle(McpTransportContext transportContext,
125125 private Mono <McpTransportResponse > dispatch (McpTransportContext transportContext , JSONRPCRequest request ,
126126 boolean blocking ) {
127127 Object id = request .id ();
128+ if (McpSchema .METHOD_INITIALIZE .equals (request .method ()) && !hasModernVersionClaim (request .params ())) {
129+ // Legacy client: tries to initialize and doesn't pass version header. We
130+ // reject early to return the correct error and not a header-validation error.
131+ // If there is a modern version header, it's an invalid INITIALIZE request
132+ // from a modern client: let it fall through, it will be rejected with
133+ // METHOD_NOT_FOUND
134+ return rejectLegacyInitialize (id , request .params ());
135+ }
128136 if (request .params () == null ) {
129137 return invalidParams (id , "params is required" );
130138 }
@@ -308,6 +316,40 @@ private static JSONRPCResponse bug(McpRequestContext ctx, String problem) {
308316 return JSONRPCResponse .error (ctx .requestId (), new JSONRPCError (ErrorCodes .INTERNAL_ERROR , "Internal error" ));
309317 }
310318
319+ /**
320+ * Answers the {@code initialize} handshake of a client on an initialization-based
321+ * (legacy) protocol version. The server broadcasts to name its supported version(s)
322+ * in the error response.
323+ * <p>
324+ * This runs before any {@code params}/{@code _meta} validation: a legacy
325+ * {@code initialize} carries neither, and would otherwise be answered with an error
326+ * about the missing {@code _meta} instead.
327+ *
328+ * @see <a href=
329+ * "https://modelcontextprotocol.io/specification/2026-07-28/basic/versioning#protocol-version-negotiation">Protocol
330+ * Version Negotiation</a>
331+ */
332+ private Mono <McpTransportResponse > rejectLegacyInitialize (Object id , Object params ) {
333+ // The legacy handshake names the client's version in params.protocolVersion. When
334+ // it is missing, "requested" is left out of the data rather than made up.
335+ String requested = params instanceof Map <?, ?> paramsMap
336+ && paramsMap .get ("protocolVersion" ) instanceof String version ? version : null ;
337+ String message = "Unsupported protocol version" + (requested != null ? " " + requested : "[undefined]" )
338+ + ": this server does not support the initialize handshake; supported protocol versions: "
339+ + String .join (", " , this .supportedVersions );
340+ return error (id , new JSONRPCError (ErrorCodes .UNSUPPORTED_PROTOCOL_VERSION , message ,
341+ new UnsupportedProtocolVersionData (this .supportedVersions , requested )));
342+ }
343+
344+ /**
345+ * Whether {@code params._meta} names a protocol version, i.e. the request claims to
346+ * follow a per-request-metadata revision rather than the legacy handshake.
347+ */
348+ private static boolean hasModernVersionClaim (Object params ) {
349+ return params instanceof Map <?, ?> paramsMap && paramsMap .get ("_meta" ) instanceof Map <?, ?> meta
350+ && meta .get (MetaKeys .PROTOCOL_VERSION ) instanceof String ;
351+ }
352+
311353 private static Mono <McpTransportResponse > error (Object id , JSONRPCError error ) {
312354 return Mono .just (McpTransportResponse .error (JSONRPCResponse .error (id , error )));
313355 }
0 commit comments