@@ -23,15 +23,34 @@ internal sealed class StreamableHttpHandler(
2323 ILoggerFactory loggerFactory )
2424{
2525 private const string McpSessionIdHeaderName = "Mcp-Session-Id" ;
26+ private const string McpProtocolVersionHeaderName = "MCP-Protocol-Version" ;
2627 private const string LastEventIdHeaderName = "Last-Event-ID" ;
2728
29+ /// <summary>
30+ /// All protocol versions supported by this implementation.
31+ /// Keep in sync with McpSessionHandler.SupportedProtocolVersions in ModelContextProtocol.Core.
32+ /// </summary>
33+ private static readonly HashSet < string > s_supportedProtocolVersions =
34+ [
35+ "2024-11-05" ,
36+ "2025-03-26" ,
37+ "2025-06-18" ,
38+ "2025-11-25" ,
39+ ] ;
40+
2841 private static readonly JsonTypeInfo < JsonRpcMessage > s_messageTypeInfo = GetRequiredJsonTypeInfo < JsonRpcMessage > ( ) ;
2942 private static readonly JsonTypeInfo < JsonRpcError > s_errorTypeInfo = GetRequiredJsonTypeInfo < JsonRpcError > ( ) ;
3043
3144 public HttpServerTransportOptions HttpServerTransportOptions => httpServerTransportOptions . Value ;
3245
3346 public async Task HandlePostRequestAsync ( HttpContext context )
3447 {
48+ if ( ! ValidateProtocolVersionHeader ( context , out var errorMessage ) )
49+ {
50+ await WriteJsonRpcErrorAsync ( context , errorMessage ! , StatusCodes . Status400BadRequest ) ;
51+ return ;
52+ }
53+
3554 // The Streamable HTTP spec mandates the client MUST accept both application/json and text/event-stream.
3655 // ASP.NET Core Minimal APIs mostly try to stay out of the business of response content negotiation,
3756 // so we have to do this manually. The spec doesn't mandate that servers MUST reject these requests,
@@ -74,6 +93,12 @@ await WriteJsonRpcErrorAsync(context,
7493
7594 public async Task HandleGetRequestAsync ( HttpContext context )
7695 {
96+ if ( ! ValidateProtocolVersionHeader ( context , out var errorMessage ) )
97+ {
98+ await WriteJsonRpcErrorAsync ( context , errorMessage ! , StatusCodes . Status400BadRequest ) ;
99+ return ;
100+ }
101+
77102 if ( ! context . Request . GetTypedHeaders ( ) . Accept . Any ( MatchesTextEventStreamMediaType ) )
78103 {
79104 await WriteJsonRpcErrorAsync ( context ,
@@ -171,6 +196,12 @@ private static async Task HandleResumePostResponseStreamAsync(HttpContext contex
171196
172197 public async Task HandleDeleteRequestAsync ( HttpContext context )
173198 {
199+ if ( ! ValidateProtocolVersionHeader ( context , out var errorMessage ) )
200+ {
201+ await WriteJsonRpcErrorAsync ( context , errorMessage ! , StatusCodes . Status400BadRequest ) ;
202+ return ;
203+ }
204+
174205 var sessionId = context . Request . Headers [ McpSessionIdHeaderName ] . ToString ( ) ;
175206 if ( sessionManager . TryRemove ( sessionId , out var session ) )
176207 {
@@ -391,6 +422,24 @@ internal static Task RunSessionAsync(HttpContext httpContext, McpServer session,
391422
392423 internal static JsonTypeInfo < T > GetRequiredJsonTypeInfo < T > ( ) => ( JsonTypeInfo < T > ) McpJsonUtilities . DefaultOptions . GetTypeInfo ( typeof ( T ) ) ;
393424
425+ /// <summary>
426+ /// Validates the MCP-Protocol-Version header if present. A missing header is allowed for backwards compatibility,
427+ /// but an invalid or unsupported value must be rejected with 400 Bad Request per the MCP spec.
428+ /// </summary>
429+ private static bool ValidateProtocolVersionHeader ( HttpContext context , out string ? errorMessage )
430+ {
431+ var protocolVersionHeader = context . Request . Headers [ McpProtocolVersionHeaderName ] . ToString ( ) ;
432+ if ( ! string . IsNullOrEmpty ( protocolVersionHeader ) &&
433+ ! s_supportedProtocolVersions . Contains ( protocolVersionHeader ) )
434+ {
435+ errorMessage = $ "Bad Request: The MCP-Protocol-Version header value '{ protocolVersionHeader } ' is not supported.";
436+ return false ;
437+ }
438+
439+ errorMessage = null ;
440+ return true ;
441+ }
442+
394443 private static bool MatchesApplicationJsonMediaType ( MediaTypeHeaderValue acceptHeaderValue )
395444 => acceptHeaderValue . MatchesMediaType ( "application/json" ) ;
396445
0 commit comments