using Microsoft.Extensions.Logging; using Microsoft.Extensions.Logging.Abstractions; using ModelContextProtocol.Protocol; using System.Text; using System.Text.Json; namespace ModelContextProtocol.Server; /// /// Provides an implemented using a pair of input and output streams. /// /// /// The class implements bidirectional JSON-RPC messaging over arbitrary /// streams, allowing MCP communication with clients through various I/O channels such as network sockets, /// memory streams, or pipes. /// public class StreamServerTransport : TransportBase { private static readonly byte[] s_newlineBytes = "\n"u8.ToArray(); private readonly ILogger _logger; private readonly TextReader _inputReader; private readonly Stream _outputStream; private readonly SemaphoreSlim _sendLock = new(1, 1); private readonly CancellationTokenSource _shutdownCts = new(); private readonly Task _readLoopCompleted; private int _disposed = 0; /// /// Initializes a new instance of the class with explicit input/output streams. /// /// The input to use as standard input. /// The output to use as standard output. /// Optional name of the server, used for diagnostic purposes, like logging. /// Optional logger factory used for logging employed by the transport. /// is . /// is . public StreamServerTransport(Stream inputStream, Stream outputStream, string? serverName = null, ILoggerFactory? loggerFactory = null) : base(serverName is not null ? $"Server (stream) ({serverName})" : "Server (stream)", loggerFactory) { Throw.IfNull(inputStream); Throw.IfNull(outputStream); _logger = loggerFactory?.CreateLogger(GetType()) ?? NullLogger.Instance; #if NET _inputReader = new StreamReader(inputStream, Encoding.UTF8); #else _inputReader = new CancellableStreamReader(inputStream, Encoding.UTF8); #endif _outputStream = outputStream; SetConnected(); _readLoopCompleted = Task.Run(ReadMessagesAsync, _shutdownCts.Token); } /// public override async Task SendMessageAsync(JsonRpcMessage message, CancellationToken cancellationToken = default) { if (!IsConnected) { return; } using var _ = await _sendLock.LockAsync(cancellationToken).ConfigureAwait(false); string id = "(no id)"; if (message is JsonRpcMessageWithId messageWithId) { id = messageWithId.Id.ToString(); } try { await JsonSerializer.SerializeAsync(_outputStream, message, McpJsonUtilities.DefaultOptions.GetTypeInfo(typeof(JsonRpcMessage)), cancellationToken).ConfigureAwait(false); await _outputStream.WriteAsync(s_newlineBytes, cancellationToken).ConfigureAwait(false); await _outputStream.FlushAsync(cancellationToken).ConfigureAwait(false); } catch (Exception ex) { LogTransportSendFailed(Name, id, ex); throw new IOException("Failed to send message.", ex); } } private async Task ReadMessagesAsync() { CancellationToken shutdownToken = _shutdownCts.Token; Exception? error = null; try { LogTransportEnteringReadMessagesLoop(Name); while (!shutdownToken.IsCancellationRequested) { var line = await _inputReader.ReadLineAsync(shutdownToken).ConfigureAwait(false); if (string.IsNullOrWhiteSpace(line)) { if (line is null) { LogTransportEndOfStream(Name); break; } continue; } LogTransportReceivedMessageSensitive(Name, line); try { if (JsonSerializer.Deserialize(line, McpJsonUtilities.DefaultOptions.GetTypeInfo(typeof(JsonRpcMessage))) is JsonRpcMessage message) { await WriteMessageAsync(message, shutdownToken).ConfigureAwait(false); } else { LogTransportMessageParseUnexpectedTypeSensitive(Name, line); } } catch (JsonException ex) { if (Logger.IsEnabled(LogLevel.Trace)) { LogTransportMessageParseFailedSensitive(Name, line, ex); } else { LogTransportMessageParseFailed(Name, ex); } // Continue reading even if we fail to parse a message } } } catch (OperationCanceledException) { LogTransportReadMessagesCancelled(Name); } catch (Exception ex) { LogTransportReadMessagesFailed(Name, ex); error = ex; } finally { SetDisconnected(error); } } /// public override async ValueTask DisposeAsync() { if (Interlocked.Exchange(ref _disposed, 1) != 0) { return; } try { LogTransportShuttingDown(Name); // Signal to the stdin reading loop to stop. await _shutdownCts.CancelAsync().ConfigureAwait(false); _shutdownCts.Dispose(); // Dispose of stdin/out. Cancellation may not be able to wake up operations // synchronously blocked in a syscall; we need to forcefully close the handle / file descriptor. _inputReader?.Dispose(); _outputStream?.Dispose(); // Make sure the work has quiesced. try { await _readLoopCompleted.ConfigureAwait(false); } catch (OperationCanceledException) { } catch (Exception ex) { LogTransportCleanupReadTaskFailed(Name, ex); } } finally { SetDisconnected(); LogTransportShutDown(Name); } GC.SuppressFinalize(this); } }