143 lines
No EOL
5.6 KiB
C#
143 lines
No EOL
5.6 KiB
C#
using Microsoft.Extensions.Logging;
|
|
using Microsoft.Extensions.Logging.Abstractions;
|
|
using ModelContextProtocol.Protocol;
|
|
using System.Net;
|
|
using System.Threading.Channels;
|
|
|
|
namespace ModelContextProtocol.Client;
|
|
|
|
/// <summary>
|
|
/// A transport that automatically detects whether to use Streamable HTTP or SSE transport
|
|
/// by trying Streamable HTTP first and falling back to SSE if that fails.
|
|
/// </summary>
|
|
internal sealed partial class AutoDetectingClientSessionTransport : ITransport
|
|
{
|
|
private readonly SseClientTransportOptions _options;
|
|
private readonly HttpClient _httpClient;
|
|
private readonly ILoggerFactory? _loggerFactory;
|
|
private readonly ILogger _logger;
|
|
private readonly string _name;
|
|
private readonly Channel<JsonRpcMessage> _messageChannel;
|
|
|
|
public AutoDetectingClientSessionTransport(SseClientTransportOptions transportOptions, HttpClient httpClient, ILoggerFactory? loggerFactory, string endpointName)
|
|
{
|
|
Throw.IfNull(transportOptions);
|
|
Throw.IfNull(httpClient);
|
|
|
|
_options = transportOptions;
|
|
_httpClient = httpClient;
|
|
_loggerFactory = loggerFactory;
|
|
_logger = (ILogger?)loggerFactory?.CreateLogger<AutoDetectingClientSessionTransport>() ?? NullLogger.Instance;
|
|
_name = endpointName;
|
|
|
|
// Same as TransportBase.cs.
|
|
_messageChannel = Channel.CreateUnbounded<JsonRpcMessage>(new UnboundedChannelOptions
|
|
{
|
|
SingleReader = true,
|
|
SingleWriter = false,
|
|
});
|
|
}
|
|
|
|
/// <summary>
|
|
/// Returns the active transport (either StreamableHttp or SSE)
|
|
/// </summary>
|
|
internal ITransport? ActiveTransport { get; private set; }
|
|
|
|
public ChannelReader<JsonRpcMessage> MessageReader => _messageChannel.Reader;
|
|
|
|
/// <inheritdoc/>
|
|
public Task SendMessageAsync(JsonRpcMessage message, CancellationToken cancellationToken = default)
|
|
{
|
|
if (ActiveTransport is null)
|
|
{
|
|
return InitializeAsync(message, cancellationToken);
|
|
}
|
|
|
|
return ActiveTransport.SendMessageAsync(message, cancellationToken);
|
|
}
|
|
|
|
private async Task InitializeAsync(JsonRpcMessage message, CancellationToken cancellationToken)
|
|
{
|
|
// Try StreamableHttp first
|
|
var streamableHttpTransport = new StreamableHttpClientSessionTransport(_name, _options, _httpClient, _messageChannel, _loggerFactory);
|
|
|
|
try
|
|
{
|
|
LogAttemptingStreamableHttp(_name);
|
|
using var response = await streamableHttpTransport.SendHttpRequestAsync(message, cancellationToken).ConfigureAwait(false);
|
|
|
|
if (response.IsSuccessStatusCode)
|
|
{
|
|
LogUsingStreamableHttp(_name);
|
|
ActiveTransport = streamableHttpTransport;
|
|
}
|
|
else
|
|
{
|
|
// If the status code is not success, fall back to SSE
|
|
LogStreamableHttpFailed(_name, response.StatusCode);
|
|
|
|
await streamableHttpTransport.DisposeAsync().ConfigureAwait(false);
|
|
await InitializeSseTransportAsync(message, cancellationToken).ConfigureAwait(false);
|
|
}
|
|
}
|
|
catch
|
|
{
|
|
// If nothing threw inside the try block, we've either set streamableHttpTransport as the
|
|
// ActiveTransport, or else we will have disposed it in the !IsSuccessStatusCode else block.
|
|
await streamableHttpTransport.DisposeAsync().ConfigureAwait(false);
|
|
throw;
|
|
}
|
|
}
|
|
|
|
private async Task InitializeSseTransportAsync(JsonRpcMessage message, CancellationToken cancellationToken)
|
|
{
|
|
var sseTransport = new SseClientSessionTransport(_name, _options, _httpClient, _messageChannel, _loggerFactory);
|
|
|
|
try
|
|
{
|
|
LogAttemptingSSE(_name);
|
|
await sseTransport.ConnectAsync(cancellationToken).ConfigureAwait(false);
|
|
await sseTransport.SendMessageAsync(message, cancellationToken).ConfigureAwait(false);
|
|
|
|
LogUsingSSE(_name);
|
|
ActiveTransport = sseTransport;
|
|
}
|
|
catch
|
|
{
|
|
await sseTransport.DisposeAsync().ConfigureAwait(false);
|
|
throw;
|
|
}
|
|
}
|
|
|
|
public async ValueTask DisposeAsync()
|
|
{
|
|
try
|
|
{
|
|
if (ActiveTransport is not null)
|
|
{
|
|
await ActiveTransport.DisposeAsync().ConfigureAwait(false);
|
|
}
|
|
}
|
|
finally
|
|
{
|
|
// In the majority of cases, either the Streamable HTTP transport or SSE transport has completed the channel by now.
|
|
// However, this may not be the case if HttpClient throws during the initial request due to misconfiguration.
|
|
_messageChannel.Writer.TryComplete();
|
|
}
|
|
}
|
|
|
|
[LoggerMessage(Level = LogLevel.Debug, Message = "{EndpointName} attempting to connect using Streamable HTTP transport.")]
|
|
private partial void LogAttemptingStreamableHttp(string endpointName);
|
|
|
|
[LoggerMessage(Level = LogLevel.Information, Message = "{EndpointName} streamable HTTP transport failed with status code {StatusCode}, falling back to SSE transport.")]
|
|
private partial void LogStreamableHttpFailed(string endpointName, HttpStatusCode statusCode);
|
|
|
|
[LoggerMessage(Level = LogLevel.Information, Message = "{EndpointName} using Streamable HTTP transport.")]
|
|
private partial void LogUsingStreamableHttp(string endpointName);
|
|
|
|
[LoggerMessage(Level = LogLevel.Debug, Message = "{EndpointName} attempting to connect using SSE transport.")]
|
|
private partial void LogAttemptingSSE(string endpointName);
|
|
|
|
[LoggerMessage(Level = LogLevel.Information, Message = "{EndpointName} using SSE transport.")]
|
|
private partial void LogUsingSSE(string endpointName);
|
|
} |