Add ILoggerFactory to StreamableHttpServerTransport (#1213)

Co-authored-by: copilot-swe-agent[bot] <198982749+Copilot@users.noreply.github.com>
Co-authored-by: stephentoub <2642209+stephentoub@users.noreply.github.com>
Co-authored-by: Stephen Toub <stoub@microsoft.com>
This commit is contained in:
Copilot 2026-01-30 17:45:39 -05:00 • committed by GitHub
commit 5084a79068
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
3 changed files with 33 additions and 6 deletions

View file

@ -238,7 +238,7 @@ internal sealed class StreamableHttpHandler(
if (!HttpServerTransportOptions.Stateless)
{
sessionId = MakeNewSessionId();
transport = new()
transport = new(loggerFactory)
{
SessionId = sessionId,
FlowExecutionContextFromRequests = !HttpServerTransportOptions.PerSessionExecutionContext,
@ -252,7 +252,7 @@ internal sealed class StreamableHttpHandler(
// If in the future we support resuming stateless requests, we should populate
// the event stream store and retry interval here as well.
sessionId = "";
transport = new()
transport = new(loggerFactory)
{
Stateless = true,
};

View file

@ -1,3 +1,4 @@
using Microsoft.Extensions.Logging;
using ModelContextProtocol.Protocol;
using System.Diagnostics;
using System.Net.ServerSentEvents;
@ -10,7 +11,11 @@ namespace ModelContextProtocol.Server;
/// Handles processing the request/response body pairs for the Streamable HTTP transport.
/// This is typically used via <see cref="JsonRpcMessageContext.RelatedTransport"/>.
/// </summary>
internal sealed class StreamableHttpPostTransport(StreamableHttpServerTransport parentTransport, Stream responseStream, CancellationToken sessionCancellationToken) : ITransport
internal sealed partial class StreamableHttpPostTransport(
StreamableHttpServerTransport parentTransport,
Stream responseStream,
CancellationToken sessionCancellationToken,
ILogger logger) : ITransport
{
private readonly SemaphoreSlim _messageLock = new(1, 1);
private readonly TaskCompletionSource<bool> _httpResponseTcs = new(TaskCreationOptions.RunContinuationsAsynchronously);
@ -199,7 +204,14 @@ internal sealed class StreamableHttpPostTransport(StreamableHttpServerTransport
{
using var _ = await _messageLock.LockAsync().ConfigureAwait(false);
await _storeSseWriter!.DisposeAsync().ConfigureAwait(false);
try
{
await _storeSseWriter!.DisposeAsync().ConfigureAwait(false);
}
catch (Exception ex)
{
LogStoreStreamDisposalFailed(ex);
}
}
}
}
@ -222,4 +234,7 @@ internal sealed class StreamableHttpPostTransport(StreamableHttpServerTransport
// Don't dispose the event stream writer here, as we may continue to write to the event store
// after disposal if there are pending messages.
}
[LoggerMessage(Level = LogLevel.Warning, Message = "Failed to dispose SSE event stream writer.")]
private partial void LogStoreStreamDisposalFailed(Exception exception);
}

View file

@ -1,3 +1,5 @@
using Microsoft.Extensions.Logging;
using Microsoft.Extensions.Logging.Abstractions;
using ModelContextProtocol.Protocol;
using System.Diagnostics;
using System.Diagnostics.CodeAnalysis;
@ -21,7 +23,7 @@ namespace ModelContextProtocol.Server;
/// such as when streaming completion results or providing progress updates during long-running operations.
/// </para>
/// </remarks>
public sealed class StreamableHttpServerTransport : ITransport
public sealed partial class StreamableHttpServerTransport : ITransport
{
/// <summary>
/// The stream ID used for unsolicited messages sent via the standalone GET SSE stream.
@ -35,6 +37,7 @@ public sealed class StreamableHttpServerTransport : ITransport
});
private readonly CancellationTokenSource _transportDisposedCts = new();
private readonly SemaphoreSlim _unsolicitedMessageLock = new(1, 1);
private readonly ILogger _logger;
private SseEventWriter? _httpSseWriter;
private ISseEventStreamWriter? _storeSseWriter;
@ -42,6 +45,15 @@ public sealed class StreamableHttpServerTransport : ITransport
private bool _getHttpRequestStarted;
private bool _getHttpResponseCompleted;
/// <summary>
/// Initializes a new instance of the <see cref="StreamableHttpServerTransport"/> class.
/// </summary>
/// <param name="loggerFactory">Optional logger factory used for logging employed by the transport.</param>
public StreamableHttpServerTransport(ILoggerFactory? loggerFactory = null)
{
_logger = loggerFactory?.CreateLogger<StreamableHttpServerTransport>() ?? NullLogger<StreamableHttpServerTransport>.Instance;
}
/// <inheritdoc/>
public string? SessionId { get; init; }
@ -161,7 +173,7 @@ public sealed class StreamableHttpServerTransport : ITransport
Throw.IfNull(message);
Throw.IfNull(responseStream);
var postTransport = new StreamableHttpPostTransport(this, responseStream, _transportDisposedCts.Token);
var postTransport = new StreamableHttpPostTransport(this, responseStream, _transportDisposedCts.Token, _logger);
using var postCts = CancellationTokenSource.CreateLinkedTokenSource(_transportDisposedCts.Token, cancellationToken);
await using (postTransport.ConfigureAwait(false))
{