@@ -106,13 +106,18 @@ await WriteJsonRpcErrorAsync(context,
106106 return ;
107107 }
108108
109+ // Once the body has been parsed into a request with a readable id, every JSON-RPC error response
110+ // for this request MUST echo that id (base protocol responses section; SEP-2243 error format).
111+ // Notifications carry no id, so this stays default (null), which is correct.
112+ var requestId = message is JsonRpcRequest jsonRpcRequest ? jsonRpcRequest . Id : default ;
113+
109114 if ( ! ValidateMcpHeaders ( context , message , mcpServerOptionsSnapshot . Value . ToolCollection , out var errorMessage ) )
110115 {
111- await WriteJsonRpcErrorAsync ( context , errorMessage , StatusCodes . Status400BadRequest , ( int ) McpErrorCode . HeaderMismatch ) ;
116+ await WriteJsonRpcErrorAsync ( context , errorMessage , StatusCodes . Status400BadRequest , ( int ) McpErrorCode . HeaderMismatch , requestId ) ;
112117 return ;
113118 }
114119
115- var session = await GetOrCreateSessionAsync ( context , message ) ;
120+ var session = await GetOrCreateSessionAsync ( context , message , requestId ) ;
116121 if ( session is null )
117122 {
118123 return ;
@@ -286,15 +291,15 @@ await WriteJsonRpcErrorAsync(context,
286291 }
287292 }
288293
289- private async ValueTask < StreamableHttpSession ? > GetSessionAsync ( HttpContext context , string sessionId )
294+ private async ValueTask < StreamableHttpSession ? > GetSessionAsync ( HttpContext context , string sessionId , RequestId requestId = default )
290295 {
291296 if ( string . IsNullOrEmpty ( sessionId ) )
292297 {
293298 await WriteJsonRpcErrorAsync ( context ,
294299 "Bad Request: Mcp-Session-Id header is required for GET and DELETE requests when the server is using sessions. " +
295300 "If your server doesn't need sessions, enable stateless mode by setting HttpServerTransportOptions.Stateless = true. " +
296301 "See https://csharp.sdk.modelcontextprotocol.io/concepts/stateless/stateless.html for more details." ,
297- StatusCodes . Status400BadRequest ) ;
302+ StatusCodes . Status400BadRequest , requestId : requestId ) ;
298303 return null ;
299304 }
300305
@@ -309,7 +314,7 @@ await WriteJsonRpcErrorAsync(context,
309314 // One of the few other usages I found was from some Ethereum JSON-RPC documentation and this
310315 // JSON-RPC library from Microsoft called StreamJsonRpc where it's called JsonRpcErrorCode.NoMarshaledObjectFound
311316 // https://learn.microsoft.com/dotnet/api/streamjsonrpc.protocol.jsonrpcerrorcode?view=streamjsonrpc-2.9#fields
312- await WriteJsonRpcErrorAsync ( context , "Session not found" , StatusCodes . Status404NotFound , - 32001 ) ;
317+ await WriteJsonRpcErrorAsync ( context , "Session not found" , StatusCodes . Status404NotFound , - 32001 , requestId ) ;
313318 return null ;
314319 }
315320 }
@@ -318,7 +323,7 @@ await WriteJsonRpcErrorAsync(context,
318323 {
319324 await WriteJsonRpcErrorAsync ( context ,
320325 "Forbidden: The currently authenticated user does not match the user who initiated the session." ,
321- StatusCodes . Status403Forbidden ) ;
326+ StatusCodes . Status403Forbidden , requestId : requestId ) ;
322327 return null ;
323328 }
324329
@@ -367,7 +372,7 @@ await WriteJsonRpcErrorAsync(context,
367372 }
368373 }
369374
370- private async ValueTask < StreamableHttpSession ? > GetOrCreateSessionAsync ( HttpContext context , JsonRpcMessage message )
375+ private async ValueTask < StreamableHttpSession ? > GetOrCreateSessionAsync ( HttpContext context , JsonRpcMessage message , RequestId requestId = default )
371376 {
372377 var sessionId = context . Request . Headers [ McpSessionIdHeaderName ] . ToString ( ) ;
373378
@@ -380,7 +385,7 @@ await WriteJsonRpcErrorAsync(context,
380385 // A request carrying an Mcp-Session-Id is non-conformant under the 2026-07-28 revision (SEP-2567).
381386 await WriteJsonRpcErrorAsync ( context ,
382387 "Bad Request: Mcp-Session-Id is not supported by the 2026-07-28 and later protocol revisions (SEP-2567)." ,
383- StatusCodes . Status400BadRequest ) ;
388+ StatusCodes . Status400BadRequest , requestId : requestId ) ;
384389 return null ;
385390 }
386391
@@ -389,7 +394,7 @@ await WriteJsonRpcErrorAsync(context,
389394 // The author explicitly opted into sessions (Stateless = false), which the 2026-07-28
390395 // revision cannot provide. Refuse it so a dual-era client falls back to the legacy
391396 // initialize handshake and gets the session it asked for (SEP-2575 fallback semantics).
392- await WriteUnsupportedProtocolVersionErrorAsync ( context ) ;
397+ await WriteUnsupportedProtocolVersionErrorAsync ( context , requestId ) ;
393398 return null ;
394399 }
395400
@@ -408,7 +413,7 @@ await WriteJsonRpcErrorAsync(context,
408413 "Bad Request: A new session can only be created by an initialize request. Include a valid Mcp-Session-Id header for non-initialize requests, " +
409414 "or enable stateless mode by setting HttpServerTransportOptions.Stateless = true if your server doesn't need sessions. " +
410415 "See https://csharp.sdk.modelcontextprotocol.io/concepts/stateless/stateless.html for more details." ,
411- StatusCodes . Status400BadRequest ) ;
416+ StatusCodes . Status400BadRequest , requestId : requestId ) ;
412417 return null ;
413418 }
414419
@@ -418,12 +423,12 @@ await WriteJsonRpcErrorAsync(context,
418423 {
419424 // In stateless mode, we should not be getting existing sessions via sessionId
420425 // This path should not be reached in stateless mode
421- await WriteJsonRpcErrorAsync ( context , "Bad Request: The Mcp-Session-Id header is not supported in stateless mode" , StatusCodes . Status400BadRequest ) ;
426+ await WriteJsonRpcErrorAsync ( context , "Bad Request: The Mcp-Session-Id header is not supported in stateless mode" , StatusCodes . Status400BadRequest , requestId : requestId ) ;
422427 return null ;
423428 }
424429 else
425430 {
426- return await GetSessionAsync ( context , sessionId ) ;
431+ return await GetSessionAsync ( context , sessionId , requestId ) ;
427432 }
428433 }
429434
@@ -568,10 +573,11 @@ await WriteJsonRpcErrorAsync(context,
568573 return eventStreamReader ;
569574 }
570575
571- private static Task WriteJsonRpcErrorAsync ( HttpContext context , string errorMessage , int statusCode , int errorCode = - 32000 )
576+ private static Task WriteJsonRpcErrorAsync ( HttpContext context , string errorMessage , int statusCode , int errorCode = - 32000 , RequestId requestId = default )
572577 {
573578 var jsonRpcError = new JsonRpcError
574579 {
580+ Id = requestId ,
575581 Error = new ( )
576582 {
577583 Code = errorCode ,
@@ -691,7 +697,7 @@ private static bool ValidateProtocolVersionHeader(HttpContext context, [NotNullW
691697 /// that excludes 2026-07-28 and later. A dual-era client then falls back to the legacy initialize handshake
692698 /// (SEP-2575).
693699 /// </summary>
694- private static Task WriteUnsupportedProtocolVersionErrorAsync ( HttpContext context )
700+ private static Task WriteUnsupportedProtocolVersionErrorAsync ( HttpContext context , RequestId requestId = default )
695701 {
696702 var requestedProtocolVersion = context . Request . Headers [ McpProtocolVersionHeaderName ] . ToString ( ) ;
697703 var errorDetail = new JsonRpcErrorDetail
@@ -708,7 +714,7 @@ private static Task WriteUnsupportedProtocolVersionErrorAsync(HttpContext contex
708714 GetRequiredJsonTypeInfo < UnsupportedProtocolVersionErrorData > ( ) ) ,
709715 } ;
710716
711- return WriteJsonRpcErrorDetailAsync ( context , errorDetail , StatusCodes . Status400BadRequest ) ;
717+ return WriteJsonRpcErrorDetailAsync ( context , errorDetail , StatusCodes . Status400BadRequest , requestId ) ;
712718 }
713719
714720 /// <summary>
@@ -1150,9 +1156,9 @@ private static SafeIntegerParse ParseSafeInteger(string text, out long value)
11501156 return SafeIntegerParse . NotNumeric ;
11511157 }
11521158
1153- private static Task WriteJsonRpcErrorDetailAsync ( HttpContext context , JsonRpcErrorDetail detail , int statusCode )
1159+ private static Task WriteJsonRpcErrorDetailAsync ( HttpContext context , JsonRpcErrorDetail detail , int statusCode , RequestId requestId = default )
11541160 {
1155- var jsonRpcError = new JsonRpcError { Error = detail } ;
1161+ var jsonRpcError = new JsonRpcError { Id = requestId , Error = detail } ;
11561162 return Results . Json ( jsonRpcError , s_errorTypeInfo , statusCode : statusCode ) . ExecuteAsync ( context ) ;
11571163 }
11581164
0 commit comments