using Microsoft.Extensions.Logging;
using ModelContextProtocol.Authentication;
using ModelContextProtocol.Protocol;
namespace ModelContextProtocol.Client;
///
/// Provides an over HTTP using the Server-Sent Events (SSE) or Streamable HTTP protocol.
///
///
/// This transport connects to an MCP server over HTTP using SSE or Streamable HTTP,
/// allowing for real-time server-to-client communication with standard HTTP requests.
/// Unlike the , this transport connects to an existing server
/// rather than launching a new process.
///
public sealed class HttpClientTransport : IClientTransport, IAsyncDisposable
{
private readonly HttpClientTransportOptions _options;
private readonly McpHttpClient _mcpHttpClient;
private readonly ILoggerFactory? _loggerFactory;
private readonly HttpClient? _ownedHttpClient;
///
/// Initializes a new instance of the class.
///
/// The configuration options for the transport.
/// The logger factory for creating loggers used for diagnostic output during transport operations.
public HttpClientTransport(HttpClientTransportOptions transportOptions, ILoggerFactory? loggerFactory = null)
: this(transportOptions, new HttpClient(), loggerFactory, ownsHttpClient: true)
{
}
///
/// Initializes a new instance of the class with a provided HTTP client.
///
/// The configuration options for the transport.
/// The HTTP client instance used for requests.
/// The logger factory for creating loggers used for diagnostic output during transport operations.
///
/// to dispose of when the transport is disposed;
/// if the caller is retaining ownership of the 's lifetime.
///
/// or is .
public HttpClientTransport(HttpClientTransportOptions transportOptions, HttpClient httpClient, ILoggerFactory? loggerFactory = null, bool ownsHttpClient = false)
{
Throw.IfNull(transportOptions);
Throw.IfNull(httpClient);
_options = transportOptions;
_loggerFactory = loggerFactory;
Name = transportOptions.Name ?? transportOptions.Endpoint.ToString();
if (transportOptions.OAuth is { } clientOAuthOptions)
{
var oAuthProvider = new ClientOAuthProvider(_options.Endpoint, clientOAuthOptions, httpClient, loggerFactory);
_mcpHttpClient = new AuthenticatingMcpHttpClient(httpClient, oAuthProvider);
}
else
{
_mcpHttpClient = new(httpClient);
}
if (ownsHttpClient)
{
_ownedHttpClient = httpClient;
}
}
///
public string Name { get; }
///
public async Task ConnectAsync(CancellationToken cancellationToken = default)
{
if (_options.KnownSessionId is not null && _options.TransportMode == HttpTransportMode.Sse)
{
throw new InvalidOperationException("SSE transport does not support resuming an existing session.");
}
return _options.TransportMode switch
{
HttpTransportMode.AutoDetect => new AutoDetectingClientSessionTransport(Name, _options, _mcpHttpClient, _loggerFactory),
HttpTransportMode.StreamableHttp => new StreamableHttpClientSessionTransport(Name, _options, _mcpHttpClient, messageChannel: null, _loggerFactory),
HttpTransportMode.Sse => await ConnectSseTransportAsync(cancellationToken).ConfigureAwait(false),
_ => throw new InvalidOperationException($"Unsupported transport mode: {_options.TransportMode}"),
};
}
private async Task ConnectSseTransportAsync(CancellationToken cancellationToken)
{
var sessionTransport = new SseClientSessionTransport(Name, _options, _mcpHttpClient, messageChannel: null, _loggerFactory);
try
{
await sessionTransport.ConnectAsync(cancellationToken).ConfigureAwait(false);
return sessionTransport;
}
catch
{
await sessionTransport.DisposeAsync().ConfigureAwait(false);
throw;
}
}
///
public ValueTask DisposeAsync()
{
_ownedHttpClient?.Dispose();
return default;
}
}