Fix support for sending server notifications with request-specific McpServer instances used outside their original scope. (#832)

* Fix support for sending server notifications with request-specific McpServer instances used outside their original scope.

* Address feedback.

* Update src/ModelContextProtocol.Core/Server/SseWriter.cs
This commit is contained in:
Eirik Tsarpalis 2025-10-06 18:50:33 +03:00 • committed by GitHub
commit 93834d1aec
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
4 changed files with 49 additions and 6 deletions

View file

@ -66,6 +66,7 @@ public sealed class SseResponseStreamTransport(Stream sseResponseStream, string?
public async Task SendMessageAsync(JsonRpcMessage message, CancellationToken cancellationToken = default)
{
Throw.IfNull(message);
// If the underlying writer has been disposed, just drop the message.
await _sseWriter.SendMessageAsync(message, cancellationToken).ConfigureAwait(false);
}

View file

@ -47,7 +47,7 @@ internal sealed class SseWriter(string? messageEndpoint = null, BoundedChannelOp
return _writeTask;
}
public async Task SendMessageAsync(JsonRpcMessage message, CancellationToken cancellationToken = default)
public async Task<bool> SendMessageAsync(JsonRpcMessage message, CancellationToken cancellationToken = default)
{
Throw.IfNull(message);
@ -55,14 +55,14 @@ internal sealed class SseWriter(string? messageEndpoint = null, BoundedChannelOp
if (_disposed)
{
// Don't throw an ODE, because this is disposed internally when the transport disconnects due to an abort
// or sending all the responses for the a give given Streamable HTTP POST request, so the user might not be at fault.
// There's precedence for no-oping here similar to writing to the response body of an aborted request in ASP.NET Core.
return;
// Don't throw ObjectDisposedException here; just return false to indicate the message wasn't sent.
// The calling transport can determine what to do in this case (drop the message, or fall back to another transport).
return false;
}
// Emit redundant "event: message" lines for better compatibility with other SDKs.
await _messages.Writer.WriteAsync(new SseItem<JsonRpcMessage?>(message, SseParser.EventTypeDefault), cancellationToken).ConfigureAwait(false);
return true;
}
public async ValueTask DisposeAsync()

View file

@ -72,7 +72,13 @@ internal sealed class StreamableHttpPostTransport(StreamableHttpServerTransport
throw new InvalidOperationException("Server to client requests are not supported in stateless mode.");
}
await _sseWriter.SendMessageAsync(message, cancellationToken).ConfigureAwait(false);
bool isAccepted = await _sseWriter.SendMessageAsync(message, cancellationToken).ConfigureAwait(false);
if (!isAccepted)
{
// The underlying writer didn't accept the message because the underlying request has completed.
// Rather than drop the message, fall back to sending it via the parent transport.
await parentTransport.SendMessageAsync(message, cancellationToken).ConfigureAwait(false);
}
}
public async ValueTask DisposeAsync()

View file

@ -131,6 +131,7 @@ public sealed class StreamableHttpServerTransport : ITransport
throw new InvalidOperationException("Unsolicited server to client messages are not supported in stateless mode.");
}
// If the underlying writer has been disposed, just drop the message.
await _sseWriter.SendMessageAsync(message, cancellationToken).ConfigureAwait(false);
}