1818
1919namespace ModelContextProtocol . AspNetCore ;
2020
21- internal sealed class StreamableHttpHandler (
21+ internal sealed partial class StreamableHttpHandler (
2222 IOptions < McpServerOptions > mcpServerOptionsSnapshot ,
2323 IOptionsFactory < McpServerOptions > mcpServerOptionsFactory ,
2424 IOptions < HttpServerTransportOptions > httpServerTransportOptions ,
@@ -28,6 +28,20 @@ internal sealed class StreamableHttpHandler(
2828{
2929 private const string McpSessionIdHeaderName = "Mcp-Session-Id" ;
3030 private static readonly JsonTypeInfo < JsonRpcError > s_errorTypeInfo = GetRequiredJsonTypeInfo < JsonRpcError > ( ) ;
31+
32+ private readonly ILogger _logger = loggerFactory . CreateLogger < StreamableHttpHandler > ( ) ;
33+
34+ // Headers that are safe and relevant to log for MCP over HTTP
35+ private static readonly HashSet < string > SafeHeadersToLog = new ( StringComparer . OrdinalIgnoreCase )
36+ {
37+ "Accept" ,
38+ "Content-Type" ,
39+ "Content-Length" ,
40+ "User-Agent" ,
41+ McpSessionIdHeaderName ,
42+ "X-Request-ID" ,
43+ "X-Correlation-ID"
44+ } ;
3145
3246 public ConcurrentDictionary < string , HttpMcpSession < StreamableHttpServerTransport > > Sessions { get ; } = new ( StringComparer . Ordinal ) ;
3347
@@ -37,6 +51,9 @@ internal sealed class StreamableHttpHandler(
3751
3852 public async Task HandlePostRequestAsync ( HttpContext context )
3953 {
54+ // Log request headers for debugging
55+ LogHttpRequestHeadersIfEnabled ( context ) ;
56+
4057 // The Streamable HTTP spec mandates the client MUST accept both application/json and text/event-stream.
4158 // ASP.NET Core Minimal APIs mostly try to stay out of the business of response content negotiation,
4259 // so we have to do this manually. The spec doesn't mandate that servers MUST reject these requests,
@@ -83,6 +100,9 @@ await WriteJsonRpcErrorAsync(context,
83100
84101 public async Task HandleGetRequestAsync ( HttpContext context )
85102 {
103+ // Log request headers for debugging
104+ LogHttpRequestHeadersIfEnabled ( context ) ;
105+
86106 if ( ! context . Request . GetTypedHeaders ( ) . Accept . Any ( MatchesTextEventStreamMediaType ) )
87107 {
88108 await WriteJsonRpcErrorAsync ( context ,
@@ -118,6 +138,9 @@ await WriteJsonRpcErrorAsync(context,
118138
119139 public async Task HandleDeleteRequestAsync ( HttpContext context )
120140 {
141+ // Log request headers for debugging
142+ LogHttpRequestHeadersIfEnabled ( context ) ;
143+
121144 var sessionId = context . Request . Headers [ McpSessionIdHeaderName ] . ToString ( ) ;
122145 if ( Sessions . TryRemove ( sessionId , out var session ) )
123146 {
@@ -336,6 +359,27 @@ private static bool MatchesApplicationJsonMediaType(MediaTypeHeaderValue acceptH
336359 private static bool MatchesTextEventStreamMediaType ( MediaTypeHeaderValue acceptHeaderValue )
337360 => acceptHeaderValue . MatchesMediaType ( "text/event-stream" ) ;
338361
362+ private void LogHttpRequestHeadersIfEnabled ( HttpContext context )
363+ {
364+ if ( _logger . IsEnabled ( LogLevel . Trace ) )
365+ {
366+ var safeHeaders = new Dictionary < string , string > ( ) ;
367+
368+ foreach ( var header in context . Request . Headers )
369+ {
370+ if ( SafeHeadersToLog . Contains ( header . Key ) )
371+ {
372+ safeHeaders [ header . Key ] = header . Value . ToString ( ) ;
373+ }
374+ }
375+
376+ LogHttpRequestHeaders ( context . Request . Method , context . Request . Path , safeHeaders ) ;
377+ }
378+ }
379+
380+ [ LoggerMessage ( Level = LogLevel . Trace , Message = "HTTP {Method} {Path} - Headers: {Headers}" ) ]
381+ private partial void LogHttpRequestHeaders ( string method , string path , Dictionary < string , string > headers ) ;
382+
339383 private sealed class HttpDuplexPipe ( HttpContext context ) : IDuplexPipe
340384 {
341385 public PipeReader Input => context . Request . BodyReader ;
0 commit comments