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; } }