using System.Text;
using System.Buffers;
using System.Net.ServerSentEvents;
using System.Text.Json;
using System.Threading.Channels;
using ModelContextProtocol.Protocol.Messages;
using ModelContextProtocol.Utils.Json;
namespace ModelContextProtocol.Protocol.Transport;
///
/// Implements the MCP SSE server transport protocol using the SSE response .
///
/// The stream to write the SSE response body to.
/// The endpoint to send JSON-RPC messages to. Defaults to "/message".
public sealed class SseResponseStreamTransport(Stream sseResponseStream, string messageEndpoint = "/message") : ITransport
{
private readonly Channel _incomingChannel = CreateBoundedChannel();
private readonly Channel> _outgoingSseChannel = CreateBoundedChannel>();
private Task? _sseWriteTask;
private Utf8JsonWriter? _jsonWriter;
///
public bool IsConnected { get; private set; }
///
/// Starts the transport and writes the JSON-RPC messages sent via
/// to the SSE response stream until cancelled or disposed.
///
/// A token to cancel writing to the SSE response stream.
/// A task representing the send loop that writes JSON-RPC messages to the SSE response stream.
public Task RunAsync(CancellationToken cancellationToken)
{
// The very first SSE event isn't really an IJsonRpcMessage, but there's no API to write a single item of a different type,
// so we fib and special-case the "endpoint" event type in the formatter.
if (!_outgoingSseChannel.Writer.TryWrite(new SseItem(null, "endpoint")))
{
throw new InvalidOperationException($"You must call ${nameof(RunAsync)} before calling ${nameof(SendMessageAsync)}.");
}
IsConnected = true;
var sseItems = _outgoingSseChannel.Reader.ReadAllAsync(cancellationToken);
return _sseWriteTask = SseFormatter.WriteAsync(sseItems, sseResponseStream, WriteJsonRpcMessageToBuffer, cancellationToken);
}
private void WriteJsonRpcMessageToBuffer(SseItem item, IBufferWriter writer)
{
if (item.EventType == "endpoint")
{
writer.Write(Encoding.UTF8.GetBytes(messageEndpoint));
return;
}
JsonSerializer.Serialize(GetUtf8JsonWriter(writer), item.Data, McpJsonUtilities.JsonContext.Default.IJsonRpcMessage);
}
///
public ChannelReader MessageReader => _incomingChannel.Reader;
///
public ValueTask DisposeAsync()
{
IsConnected = false;
_incomingChannel.Writer.TryComplete();
_outgoingSseChannel.Writer.TryComplete();
return new ValueTask(_sseWriteTask ?? Task.CompletedTask);
}
///
public async Task SendMessageAsync(IJsonRpcMessage message, CancellationToken cancellationToken = default)
{
if (!IsConnected)
{
throw new InvalidOperationException($"Transport is not connected. Make sure to call {nameof(RunAsync)} first.");
}
// Emit redundant "event: message" lines for better compatibility with other SDKs.
await _outgoingSseChannel.Writer.WriteAsync(new SseItem(message, SseParser.EventTypeDefault), cancellationToken);
}
///
/// Handles incoming JSON-RPC messages received on the /message endpoint.
///
/// The JSON-RPC message received.
/// A token to cancel the operation.
/// A task representing the potentially asynchronous operation to buffer or process the JSON-RPC message.
/// Thrown when there is an attempt to process a message before calling .
public async Task OnMessageReceivedAsync(IJsonRpcMessage message, CancellationToken cancellationToken)
{
if (!IsConnected)
{
throw new InvalidOperationException($"Transport is not connected. Make sure to call {nameof(RunAsync)} first.");
}
await _incomingChannel.Writer.WriteAsync(message, cancellationToken);
}
private static Channel CreateBoundedChannel(int capacity = 1) =>
Channel.CreateBounded(new BoundedChannelOptions(capacity)
{
SingleReader = true,
SingleWriter = false,
});
private Utf8JsonWriter GetUtf8JsonWriter(IBufferWriter writer)
{
if (_jsonWriter is null)
{
_jsonWriter = new Utf8JsonWriter(writer);
}
else
{
_jsonWriter.Reset(writer);
}
return _jsonWriter;
}
}