csharp-sdk/src/mcpdotnet/Protocol/Transport/TransportBase.cs
Stephen Toub 08b0a04dab
Overhaul McpClientFactory/McpServerFactory (#87)
Handlers are now specified before connecting the client/server. Otherwise, race conditions exist. Handlers move into being a part of the McpClientOptions/McpServerOptions, so that they're provided to the factories. Where relevant, the handlers are specified as part of the capability descriptors, so that they go hand in hand.

The factories are no longer stateful. Instead of allocating a factory and then calling a create method on it, you just call a static factory method.
2025-03-16 12:08:52 -04:00

78 lines
No EOL
2.5 KiB
C#

using System.Threading.Channels;
using Microsoft.Extensions.Logging;
using McpDotNet.Logging;
using McpDotNet.Protocol.Messages;
using Microsoft.Extensions.Logging.Abstractions;
namespace McpDotNet.Protocol.Transport;
/// <summary>
/// Base class for implementing MCP transports with common functionality.
/// </summary>
public abstract class TransportBase : ITransport
{
private readonly Channel<IJsonRpcMessage> _messageChannel;
private readonly ILogger _logger;
private bool _isConnected;
/// <summary>
/// Initializes a new instance of the <see cref="TransportBase"/> class.
/// </summary>
protected TransportBase(ILoggerFactory? loggerFactory)
{
// Unbounded channel to prevent blocking on writes
_messageChannel = Channel.CreateUnbounded<IJsonRpcMessage>(new UnboundedChannelOptions
{
SingleReader = true,
SingleWriter = true,
});
_logger = (ILogger?)loggerFactory?.CreateLogger<TransportBase>() ?? NullLogger.Instance;
}
/// <inheritdoc/>
public bool IsConnected => _isConnected;
/// <inheritdoc/>
public ChannelReader<IJsonRpcMessage> MessageReader => _messageChannel.Reader;
/// <inheritdoc/>
public abstract Task SendMessageAsync(IJsonRpcMessage message, CancellationToken cancellationToken = default);
/// <inheritdoc/>
public abstract ValueTask DisposeAsync();
/// <summary>
/// Writes a message to the message channel.
/// </summary>
/// <param name="message">The message to write.</param>
/// <param name="cancellationToken">Token to cancel the operation.</param>
protected async Task WriteMessageAsync(IJsonRpcMessage message, CancellationToken cancellationToken = default)
{
if (!_isConnected)
{
throw new McpTransportException("Transport is not connected");
}
_logger.TransportWritingMessageToChannel(message);
await _messageChannel.Writer.WriteAsync(message, cancellationToken).ConfigureAwait(false);
_logger.TransportMessageWrittenToChannel();
}
/// <summary>
/// Sets the connected state of the transport.
/// </summary>
/// <param name="isConnected">Whether the transport is connected.</param>
protected void SetConnected(bool isConnected)
{
if (_isConnected == isConnected)
{
return;
}
_isConnected = isConnected;
if (!isConnected)
{
_messageChannel.Writer.Complete();
}
}
}