Skip to content

Commit 9e92bc9

Browse files
authored
Remove InputOutputStreams and clean up StdioServerTransport ctors (#71)
- There doesn't appear to be a meaningful scenario for injecting different readers/writers than stdin/stdout, and having InputOutputStreams complicates the API. - Logically folks talk about "input and output", "readers and writers", etc., and "stdio" is "standard input and output", so having input specified first is less likely to be a source of bugs. - Logger should be optional, not required, to simplify the common and getting-started cases.
1 parent 9c500b4 commit 9e92bc9

File tree

4 files changed

+108
-65
lines changed

4 files changed

+108
-65
lines changed

src/mcpdotnet/Configuration/McpServerServiceCollectionExtension.cs

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -39,7 +39,6 @@ public static IMcpServerBuilder AddMcpServer(this IServiceCollection services, M
3939
services.AddSingleton<IMcpServerFactory, McpServerFactory>();
4040
services.AddHostedService<McpServerHostedService>();
4141
services.AddOptions();
42-
services.AddSingleton(InputOutputStreams.Default);
4342

4443
services.AddSingleton<IMcpServer>(sp => sp.GetRequiredService<IMcpServerFactory>().CreateServer());
4544

src/mcpdotnet/Protocol/Transport/InputOutputStreams.cs

Lines changed: 0 additions & 26 deletions
This file was deleted.

src/mcpdotnet/Protocol/Transport/StdioServerTransport.cs

Lines changed: 85 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -1,15 +1,18 @@
11
using System.Text.Json;
2+
using McpDotNet.Configuration;
23
using McpDotNet.Logging;
34
using McpDotNet.Protocol.Messages;
45
using McpDotNet.Server;
56
using McpDotNet.Utils.Json;
67
using Microsoft.Extensions.Logging;
78
using Microsoft.Extensions.Logging.Abstractions;
89

10+
#pragma warning disable CA2208 // Instantiate argument exceptions correctly
11+
912
namespace McpDotNet.Protocol.Transport;
1013

1114
/// <summary>
12-
/// Implements the MCP transport protocol over standard input/output streams.
15+
/// Provides an implementation of the MCP transport protocol over standard input/output streams.
1316
/// </summary>
1417
public sealed class StdioServerTransport : TransportBase, IServerTransport
1518
{
@@ -24,47 +27,90 @@ public sealed class StdioServerTransport : TransportBase, IServerTransport
2427
private string EndpointName => $"Server (stdio) ({_serverName})";
2528

2629
/// <summary>
27-
/// Initializes a new instance of the StdioServerTransport class.
30+
/// Initializes a new instance of the <see cref="StdioServerTransport"/> class, using
31+
/// <see cref="Console.In"/> and <see cref="Console.Out"/> for input and output streams.
2832
/// </summary>
2933
/// <param name="serverOptions">The server options.</param>
30-
/// <param name="loggerFactory">A logger factory for creating loggers.</param>
31-
/// <param name="inputOutputStreams">Container for the input and output streams</param>
32-
public StdioServerTransport(McpServerOptions serverOptions, ILoggerFactory? loggerFactory, InputOutputStreams inputOutputStreams)
33-
: this(
34-
serverOptions is not null ? serverOptions.ServerInfo.Name : throw new ArgumentNullException(nameof(serverOptions)),
35-
loggerFactory,
36-
inputOutputStreams is not null ? inputOutputStreams.Output : throw new ArgumentNullException(nameof(inputOutputStreams)),
37-
inputOutputStreams.Input
38-
)
34+
/// <param name="loggerFactory">Optional logger factory used for logging employed by the transport.</param>
35+
/// <exception cref="ArgumentNullException"><paramref name="serverOptions"/> is <see langword="null"/> or contains a null name.</exception>
36+
/// <remarks>
37+
/// <para>
38+
/// By default, no logging is performed. If a <paramref name="loggerFactory"/> is supplied, it must not log
39+
/// to <see cref="Console.Out"/>, as that will interfere with the transport's output.
40+
/// </para>
41+
/// </remarks>
42+
public StdioServerTransport(McpServerOptions serverOptions, ILoggerFactory? loggerFactory = null)
43+
: this(GetServerName(serverOptions), loggerFactory)
3944
{
4045
}
4146

4247
/// <summary>
43-
/// Initializes a new instance of the StdioServerTransport class.
48+
/// Initializes a new instance of the <see cref="StdioServerTransport"/> class, using
49+
/// <see cref="Console.In"/> and <see cref="Console.Out"/> for input and output streams.
4450
/// </summary>
4551
/// <param name="serverName">The name of the server.</param>
46-
/// <param name="loggerFactory">A logger factory for creating loggers.</param>
47-
public StdioServerTransport(string serverName, ILoggerFactory? loggerFactory)
48-
: this(serverName, loggerFactory, Console.Out, Console.In)
52+
/// <param name="loggerFactory">Optional logger factory used for logging employed by the transport.</param>
53+
/// <exception cref="ArgumentNullException"><paramref name="serverName"/> is <see langword="null"/>.</exception>
54+
/// <remarks>
55+
/// <para>
56+
/// By default, no logging is performed. If a <paramref name="loggerFactory"/> is supplied, it must not log
57+
/// to <see cref="Console.Out"/>, as that will interfere with the transport's output.
58+
/// </para>
59+
/// </remarks>
60+
public StdioServerTransport(string serverName, ILoggerFactory? loggerFactory = null)
61+
: this(serverName ?? throw new ArgumentNullException(nameof(serverName)), Console.In, Console.Out, loggerFactory)
4962
{
63+
}
5064

65+
/// <summary>
66+
/// Initializes a new instance of the <see cref="StdioServerTransport"/> class using the specified
67+
/// <paramref name="input"/> and <paramref name="output"/> streams.
68+
/// </summary>
69+
/// <param name="serverOptions">The server options.</param>
70+
/// <param name="input">The reader, from which all input is read.</param>
71+
/// <param name="output">The writer, to which all output is written.</param>
72+
/// <param name="loggerFactory">Optional logger factory used for logging employed by the transport.</param>
73+
/// <exception cref="ArgumentNullException"><paramref name="serverOptions"/> is <see langword="null"/> or contains a null name.</exception>
74+
/// <exception cref="ArgumentNullException"><paramref name="input"/> is <see langword="null"/>.</exception>
75+
/// <exception cref="ArgumentNullException"><paramref name="output"/> is <see langword="null"/>.</exception>
76+
/// <remarks>
77+
/// <para>
78+
/// By default, no logging is performed. If a <paramref name="loggerFactory"/> is supplied, it must not log
79+
/// to <see cref="Console.Out"/>, as that will interfere with the transport's output.
80+
/// </para>
81+
/// </remarks>
82+
public StdioServerTransport(McpServerOptions serverOptions, TextReader input, TextWriter output, ILoggerFactory? loggerFactory = null)
83+
: this(GetServerName(serverOptions), input, output, loggerFactory)
84+
{
5185
}
5286

5387
/// <summary>
54-
/// Initializes a new instance of the StdioServerTransport class.
88+
/// Initializes a new instance of the <see cref="StdioServerTransport"/> class using the specified
89+
/// <paramref name="input"/> and <paramref name="output"/> streams.
5590
/// </summary>
5691
/// <param name="serverName">The name of the server.</param>
57-
/// <param name="loggerFactory">A logger factory for creating loggers.</param>
58-
/// <param name="output">The output stream writer</param>
59-
/// <param name="input">The input stream reader</param>
60-
public StdioServerTransport(string serverName, ILoggerFactory? loggerFactory, TextWriter output, TextReader input)
92+
/// <param name="input">The reader, from which all input is read.</param>
93+
/// <param name="output">The writer, to which all output is written.</param>
94+
/// <param name="loggerFactory">Optional logger factory used for logging employed by the transport.</param>
95+
/// <exception cref="ArgumentNullException"><paramref name="serverName"/> is <see langword="null"/>.</exception>
96+
/// <exception cref="ArgumentNullException"><paramref name="input"/> is <see langword="null"/>.</exception>
97+
/// <exception cref="ArgumentNullException"><paramref name="output"/> is <see langword="null"/>.</exception>
98+
/// <remarks>
99+
/// <para>
100+
/// By default, no logging is performed. If a <paramref name="loggerFactory"/> is supplied, it must not log
101+
/// to <see cref="Console.Out"/>, as that will interfere with the transport's output.
102+
/// </para>
103+
/// </remarks>
104+
public StdioServerTransport(string serverName, TextReader input, TextWriter output, ILoggerFactory? loggerFactory = null)
61105
: base(loggerFactory)
62106
{
63-
_serverName = serverName;
64-
_logger = loggerFactory is not null ? loggerFactory.CreateLogger<StdioClientTransport>() : NullLogger.Instance;
65-
_jsonOptions = JsonSerializerOptionsExtensions.DefaultOptions;
107+
_serverName = serverName ?? throw new ArgumentNullException(nameof(serverName));
66108
_input = input ?? throw new ArgumentNullException(nameof(input));
67109
_output = output ?? throw new ArgumentNullException(nameof(output));
110+
111+
_logger = loggerFactory is not null ? loggerFactory.CreateLogger<StdioClientTransport>() : NullLogger.Instance;
112+
113+
_jsonOptions = JsonSerializerOptionsExtensions.DefaultOptions;
68114
}
69115

70116
/// <inheritdoc/>
@@ -224,4 +270,20 @@ private async Task CleanupAsync(CancellationToken cancellationToken)
224270
SetConnected(false);
225271
_logger.TransportCleanedUp(EndpointName);
226272
}
273+
274+
/// <summary>Validates the <paramref name="serverOptions"/> and extracts from it the server name to use.</summary>
275+
private static string GetServerName(McpServerOptions serverOptions)
276+
{
277+
if (serverOptions is null)
278+
{
279+
throw new ArgumentNullException(nameof(serverOptions));
280+
}
281+
282+
if (serverOptions.ServerInfo is null)
283+
{
284+
throw new ArgumentNullException($"{nameof(serverOptions)}.{nameof(serverOptions.ServerInfo)}");
285+
}
286+
287+
return serverOptions.ServerInfo.Name;
288+
}
227289
}

tests/mcpdotnet.Tests/Transport/StdioServerTransportTests.cs

Lines changed: 23 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -31,7 +31,7 @@ public StdioServerTransportTests()
3131
public async Task Constructor_Should_Initialize_With_Valid_Parameters()
3232
{
3333
// Act
34-
await using var transport = new StdioServerTransport(_serverOptions, NullLoggerFactory.Instance, InputOutputStreams.Default);
34+
await using var transport = new StdioServerTransport(_serverOptions);
3535

3636
// Assert
3737
Assert.NotNull(transport);
@@ -40,21 +40,27 @@ public async Task Constructor_Should_Initialize_With_Valid_Parameters()
4040
[Fact]
4141
public void Constructor_Throws_For_Null_Options()
4242
{
43-
var exception = Assert.Throws<ArgumentNullException>(() => new StdioServerTransport(serverOptions: null!, NullLoggerFactory.Instance, InputOutputStreams.Default));
44-
Assert.Equal("serverOptions", exception.ParamName);
43+
Assert.Throws<ArgumentNullException>("serverName", () => new StdioServerTransport((string)null!));
44+
45+
Assert.Throws<ArgumentNullException>("serverOptions", () => new StdioServerTransport((McpServerOptions)null!));
46+
Assert.Throws<ArgumentNullException>("serverOptions.ServerInfo", () => new StdioServerTransport(new McpServerOptions() { ServerInfo = null! }));
47+
Assert.Throws<ArgumentNullException>("serverName", () => new StdioServerTransport(new McpServerOptions() { ServerInfo = new() { Name = null!, Version = "" } }));
4548
}
4649

4750
[Fact]
4851
public void Constructor_Throws_For_Null_Streams()
4952
{
50-
var exception = Assert.Throws<ArgumentNullException>(() => new StdioServerTransport(_serverOptions, NullLoggerFactory.Instance, null!));
51-
Assert.Equal("inputOutputStreams", exception.ParamName);
53+
Assert.Throws<ArgumentNullException>("input", () => new StdioServerTransport(_serverOptions, null!, Console.Out));
54+
Assert.Throws<ArgumentNullException>("input", () => new StdioServerTransport("name", null!, Console.Out));
55+
56+
Assert.Throws<ArgumentNullException>("output", () => new StdioServerTransport("name", Console.In, null!));
57+
Assert.Throws<ArgumentNullException>("output", () => new StdioServerTransport(_serverOptions, Console.In, null!));
5258
}
5359

5460
[Fact]
5561
public async Task StartListeningAsync_Should_Set_Connected_State()
5662
{
57-
await using var transport = new StdioServerTransport(_serverOptions, NullLoggerFactory.Instance, InputOutputStreams.Default);
63+
await using var transport = new StdioServerTransport(_serverOptions);
5864

5965
await transport.StartListeningAsync();
6066

@@ -64,16 +70,18 @@ public async Task StartListeningAsync_Should_Set_Connected_State()
6470
[Fact]
6571
public async Task SendMessageAsync_Should_Send_Message()
6672
{
67-
var streams = new InputOutputStreams { Output = new StringWriter(), Input = new StringReader("") };
68-
await using var transport = new StdioServerTransport(_serverOptions, NullLoggerFactory.Instance, streams);
73+
var input = new StringReader("");
74+
var output = new StringWriter();
75+
76+
await using var transport = new StdioServerTransport(_serverOptions, input, output, NullLoggerFactory.Instance);
6977
await transport.StartListeningAsync();
7078

7179
var message = new JsonRpcRequest { Method = "test", Id = RequestId.FromNumber(44) };
7280

7381

7482
await transport.SendMessageAsync(message);
7583

76-
var result = streams.Output.ToString()?.Trim();
84+
var result = output.ToString()?.Trim();
7785
var expected = JsonSerializer.Serialize(message, JsonSerializerOptionsExtensions.DefaultOptions);
7886

7987
Assert.Equal(expected, result);
@@ -82,7 +90,7 @@ public async Task SendMessageAsync_Should_Send_Message()
8290
[Fact]
8391
public async Task SendMessageAsync_Throws_Exception_If_Not_Connected()
8492
{
85-
await using var transport = new StdioServerTransport(_serverOptions, NullLoggerFactory.Instance, InputOutputStreams.Default);
93+
await using var transport = new StdioServerTransport(_serverOptions);
8694

8795
var message = new JsonRpcRequest { Method = "test" };
8896

@@ -92,7 +100,7 @@ public async Task SendMessageAsync_Throws_Exception_If_Not_Connected()
92100
[Fact]
93101
public async Task DisposeAsync_Should_Dispose_Resources()
94102
{
95-
await using var transport = new StdioServerTransport(_serverOptions, NullLoggerFactory.Instance, InputOutputStreams.Default);
103+
await using var transport = new StdioServerTransport(_serverOptions);
96104

97105
await transport.DisposeAsync();
98106

@@ -105,10 +113,10 @@ public async Task ReadMessagesAsync_Should_Read_Messages()
105113
var message = new JsonRpcRequest { Method = "test", Id = RequestId.FromNumber(44) };
106114
var json = JsonSerializer.Serialize(message, JsonSerializerOptionsExtensions.DefaultOptions);
107115

108-
using var sr = new StringReader(json);
116+
var input = new StringReader(json);
117+
var output = new StringWriter();
109118

110-
var streams = new InputOutputStreams { Output = new StringWriter(), Input = new StringReader(json) };
111-
await using var transport = new StdioServerTransport(_serverOptions, NullLoggerFactory.Instance, streams);
119+
await using var transport = new StdioServerTransport(_serverOptions, input, output);
112120
await transport.StartListeningAsync();
113121

114122
var canRead = await transport.MessageReader.WaitToReadAsync();
@@ -123,7 +131,7 @@ public async Task ReadMessagesAsync_Should_Read_Messages()
123131
[Fact]
124132
public async Task CleanupAsync_Should_Cleanup_Resources()
125133
{
126-
var transport = new StdioServerTransport(_serverOptions, NullLoggerFactory.Instance, InputOutputStreams.Default);
134+
var transport = new StdioServerTransport(_serverOptions);
127135
await transport.StartListeningAsync();
128136

129137
await transport.DisposeAsync();

0 commit comments

Comments
 (0)