Add opt-out for Streamable HTTP standalone GET stream (#1699)
Co-authored-by: Jeff Handley <jeffhandley@users.noreply.github.com> Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
This commit is contained in:
parent
2ca96f548b
commit
6af05d0bf2
2 changed files with 210 additions and 3 deletions
|
|
@ -94,6 +94,24 @@ public sealed class HttpClientTransportOptions
|
|||
/// </remarks>
|
||||
public string? KnownSessionId { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets a value indicating whether the Streamable HTTP transport opens the standalone GET SSE stream
|
||||
/// used to receive unsolicited server-to-client messages.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// <para>
|
||||
/// This defaults to <see langword="true"/>, matching the Streamable HTTP behavior of opening a standalone GET
|
||||
/// SSE stream after initialization so the server can push unsolicited server-to-client messages.
|
||||
/// </para>
|
||||
/// <para>
|
||||
/// Set this to <see langword="false"/> for servers where the client does not need unsolicited server-to-client
|
||||
/// messages, or when a long-lived standalone GET would block other requests under a constrained
|
||||
/// <see cref="HttpClient"/> connection pool. Direct responses and streaming responses to client POST requests
|
||||
/// still work; only the standalone GET stream is skipped.
|
||||
/// </para>
|
||||
/// </remarks>
|
||||
public bool EnableStandaloneGetStream { get; set; } = true;
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets a value indicating whether this transport endpoint is responsible for ending the session on dispose.
|
||||
/// </summary>
|
||||
|
|
|
|||
|
|
@ -26,6 +26,7 @@ internal sealed partial class StreamableHttpClientSessionTransport : TransportBa
|
|||
|
||||
private string? _negotiatedProtocolVersion;
|
||||
private Task? _getReceiveTask;
|
||||
private bool _streamableHttpAdopted;
|
||||
private volatile ClientTransportClosedException? _disconnectError;
|
||||
|
||||
private readonly SemaphoreSlim _disposeLock = new(1, 1);
|
||||
|
|
@ -54,7 +55,8 @@ internal sealed partial class StreamableHttpClientSessionTransport : TransportBa
|
|||
if (_options.KnownSessionId is { } knownSessionId)
|
||||
{
|
||||
SessionId = knownSessionId;
|
||||
_getReceiveTask = ReceiveUnsolicitedMessagesAsync();
|
||||
_streamableHttpAdopted = true;
|
||||
StartUnsolicitedMessageStreamIfEnabled();
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -228,7 +230,8 @@ internal sealed partial class StreamableHttpClientSessionTransport : TransportBa
|
|||
var initializeResult = JsonSerializer.Deserialize(initResponse.Result, McpJsonUtilities.JsonContext.Default.InitializeResult);
|
||||
_negotiatedProtocolVersion = initializeResult?.ProtocolVersion;
|
||||
|
||||
_getReceiveTask ??= ReceiveUnsolicitedMessagesAsync();
|
||||
_streamableHttpAdopted = true;
|
||||
StartUnsolicitedMessageStreamIfEnabled();
|
||||
}
|
||||
else if (rpcRequest.Method == RequestMethods.ServerDiscover && rpcResponseOrError is JsonRpcResponse)
|
||||
{
|
||||
|
|
@ -241,6 +244,14 @@ internal sealed partial class StreamableHttpClientSessionTransport : TransportBa
|
|||
return response;
|
||||
}
|
||||
|
||||
private void StartUnsolicitedMessageStreamIfEnabled()
|
||||
{
|
||||
if (_options.EnableStandaloneGetStream)
|
||||
{
|
||||
_getReceiveTask ??= ReceiveUnsolicitedMessagesAsync();
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Reads the protocol version from a request's <c>_meta/io.modelcontextprotocol/protocolVersion</c> field,
|
||||
/// Introduced by the 2026-07-28 protocol revision (SEP-2575). Returns <see langword="null"/> for messages that
|
||||
|
|
@ -298,7 +309,7 @@ internal sealed partial class StreamableHttpClientSessionTransport : TransportBa
|
|||
{
|
||||
// If we're auto-detecting the transport and failed to connect, leave the message Channel open for the SSE transport.
|
||||
// This class isn't directly exposed to public callers, so we don't have to worry about changing the _state in this case.
|
||||
if (_options.TransportMode is not HttpTransportMode.AutoDetect || _getReceiveTask is not null)
|
||||
if (_options.TransportMode is not HttpTransportMode.AutoDetect || _streamableHttpAdopted)
|
||||
{
|
||||
// _disconnectError is set when the server returns 404 indicating session expiry.
|
||||
// When null, this is a graceful client-initiated closure (no error).
|
||||
|
|
|
|||
Loading…
Reference in a new issue