using Microsoft.Extensions.Logging;
using ModelContextProtocol.Protocol;
namespace ModelContextProtocol.Server;
///
/// Provides an implemented via "stdio" (standard input/output).
///
public sealed class StdioServerTransport : StreamServerTransport
{
///
/// Initializes a new instance of the class.
///
/// The server options.
/// Optional logger factory used for logging employed by the transport.
/// is or contains a null name.
public StdioServerTransport(McpServerOptions serverOptions, ILoggerFactory? loggerFactory = null)
: this(GetServerName(serverOptions), loggerFactory: loggerFactory)
{
}
///
/// Initializes a new instance of the class.
///
/// The name of the server.
/// Optional logger factory used for logging employed by the transport.
/// is .
public StdioServerTransport(string serverName, ILoggerFactory? loggerFactory = null)
: base(new CancellableStdinStream(Console.OpenStandardInput()),
new BufferedStream(Console.OpenStandardOutput()),
serverName ?? throw new ArgumentNullException(nameof(serverName)),
loggerFactory)
{
}
private static string GetServerName(McpServerOptions serverOptions)
{
Throw.IfNull(serverOptions);
return serverOptions.ServerInfo?.Name ?? McpServer.DefaultImplementation.Name;
}
// Neither WindowsConsoleStream nor UnixConsoleStream respect CancellationTokens or cancel any I/O on Dispose.
// WindowsConsoleStream will return an EOS on Ctrl-C, but that is not the only reason the shutdownToken may fire.
private sealed class CancellableStdinStream(Stream stdinStream) : Stream
{
public override bool CanRead => true;
public override bool CanSeek => false;
public override bool CanWrite => false;
public override Task ReadAsync(byte[] buffer, int offset, int count, CancellationToken cancellationToken)
=> stdinStream.ReadAsync(buffer, offset, count, cancellationToken).WaitAsync(cancellationToken);
#if NET
public override ValueTask ReadAsync(Memory buffer, CancellationToken cancellationToken = default)
{
ValueTask vt = stdinStream.ReadAsync(buffer, cancellationToken);
return vt.IsCompletedSuccessfully ? vt : new(vt.AsTask().WaitAsync(cancellationToken));
}
#endif
// The McpServer shouldn't call flush on the stdin Stream, but it doesn't need to throw just in case.
public override void Flush() { }
public override long Length => throw new NotSupportedException();
public override long Position { get => throw new NotSupportedException(); set => throw new NotSupportedException(); }
public override int Read(byte[] buffer, int offset, int count) => throw new NotSupportedException();
public override long Seek(long offset, SeekOrigin origin) => throw new NotSupportedException();
public override void SetLength(long value) => throw new NotSupportedException();
public override void Write(byte[] buffer, int offset, int count) => throw new NotSupportedException();
}
}