Skip to content

Commit 4216490

Browse files
Copilothalter73
andcommitted
Add tests validating \n line delimiter for stream transports
Validate that both StreamServerTransport and StreamClientSessionTransport use \n (not \r\n) as the message delimiter on all platforms, and that both correctly accept \n-delimited messages on the read side. Co-authored-by: halter73 <54385+halter73@users.noreply.github.com>
1 parent 072fa67 commit 4216490

2 files changed

Lines changed: 108 additions & 0 deletions

File tree

tests/ModelContextProtocol.Tests/Transport/StdioServerTransportTests.cs

Lines changed: 48 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -227,6 +227,54 @@ public async Task SendMessageAsync_Should_Log_At_Trace_Level()
227227
Assert.Contains(traceLogMessages, x => x.Message.Contains("\"method\":\"test\"") && x.Message.Contains("\"id\":44"));
228228
}
229229

230+
[Fact]
231+
public async Task SendMessageAsync_Should_Use_LF_Not_CRLF()
232+
{
233+
using var output = new MemoryStream();
234+
235+
await using var transport = new StreamServerTransport(
236+
new Pipe().Reader.AsStream(),
237+
output,
238+
loggerFactory: LoggerFactory);
239+
240+
var message = new JsonRpcRequest { Method = "test", Id = new RequestId(44) };
241+
242+
await transport.SendMessageAsync(message, TestContext.Current.CancellationToken);
243+
244+
byte[] bytes = output.ToArray();
245+
246+
// The output should end with exactly \n (0x0A), not \r\n (0x0D 0x0A).
247+
Assert.True(bytes.Length > 1, "Output should contain message data");
248+
Assert.Equal((byte)'\n', bytes[^1]);
249+
Assert.NotEqual((byte)'\r', bytes[^2]);
250+
}
251+
252+
[Fact]
253+
public async Task ReadMessagesAsync_Should_Accept_LF_Delimited_Messages()
254+
{
255+
var message = new JsonRpcRequest { Method = "test", Id = new RequestId(44) };
256+
var json = JsonSerializer.Serialize(message, McpJsonUtilities.DefaultOptions);
257+
258+
Pipe pipe = new();
259+
using var input = pipe.Reader.AsStream();
260+
261+
await using var transport = new StreamServerTransport(
262+
input,
263+
Stream.Null,
264+
loggerFactory: LoggerFactory);
265+
266+
// Write the message with \n line ending (not \r\n)
267+
await pipe.Writer.WriteAsync(Encoding.UTF8.GetBytes($"{json}\n"), TestContext.Current.CancellationToken);
268+
269+
var canRead = await transport.MessageReader.WaitToReadAsync(TestContext.Current.CancellationToken);
270+
271+
Assert.True(canRead, "Should be able to read a \\n-delimited message");
272+
Assert.True(transport.MessageReader.TryPeek(out var readMessage));
273+
Assert.NotNull(readMessage);
274+
Assert.IsType<JsonRpcRequest>(readMessage);
275+
Assert.Equal("44", ((JsonRpcRequest)readMessage).Id.ToString());
276+
}
277+
230278
[Fact]
231279
public async Task ReadMessagesAsync_Should_Log_Received_At_Trace_Level()
232280
{
Lines changed: 60 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,60 @@
1+
using ModelContextProtocol.Protocol;
2+
using ModelContextProtocol.Tests.Utils;
3+
using System.IO.Pipelines;
4+
using System.Text;
5+
using System.Text.Json;
6+
7+
namespace ModelContextProtocol.Tests.Transport;
8+
9+
public class StreamClientTransportTests(ITestOutputHelper testOutputHelper) : LoggedTest(testOutputHelper)
10+
{
11+
[Fact]
12+
public async Task SendMessageAsync_Should_Use_LF_Not_CRLF()
13+
{
14+
using var serverInput = new MemoryStream();
15+
Pipe serverOutputPipe = new();
16+
17+
var transport = new StreamClientTransport(serverInput, serverOutputPipe.Reader.AsStream(), LoggerFactory);
18+
await using var sessionTransport = await transport.ConnectAsync(TestContext.Current.CancellationToken);
19+
20+
var message = new JsonRpcRequest { Method = "test", Id = new RequestId(44) };
21+
22+
await sessionTransport.SendMessageAsync(message, TestContext.Current.CancellationToken);
23+
24+
byte[] bytes = serverInput.ToArray();
25+
26+
// The output should end with exactly \n (0x0A), not \r\n (0x0D 0x0A).
27+
Assert.True(bytes.Length > 1, "Output should contain message data");
28+
Assert.Equal((byte)'\n', bytes[^1]);
29+
Assert.NotEqual((byte)'\r', bytes[^2]);
30+
31+
// Also verify the JSON content is valid
32+
var json = Encoding.UTF8.GetString(bytes).TrimEnd('\n');
33+
var expected = JsonSerializer.Serialize(message, McpJsonUtilities.DefaultOptions);
34+
Assert.Equal(expected, json);
35+
}
36+
37+
[Fact]
38+
public async Task ReadMessagesAsync_Should_Accept_LF_Delimited_Messages()
39+
{
40+
Pipe serverInputPipe = new();
41+
Pipe serverOutputPipe = new();
42+
43+
var transport = new StreamClientTransport(serverInputPipe.Writer.AsStream(), serverOutputPipe.Reader.AsStream(), LoggerFactory);
44+
await using var sessionTransport = await transport.ConnectAsync(TestContext.Current.CancellationToken);
45+
46+
var message = new JsonRpcRequest { Method = "test", Id = new RequestId(44) };
47+
var json = JsonSerializer.Serialize(message, McpJsonUtilities.DefaultOptions);
48+
49+
// Write a \n-delimited message to the server's output (which the client reads)
50+
await serverOutputPipe.Writer.WriteAsync(Encoding.UTF8.GetBytes($"{json}\n"), TestContext.Current.CancellationToken);
51+
52+
var canRead = await sessionTransport.MessageReader.WaitToReadAsync(TestContext.Current.CancellationToken);
53+
54+
Assert.True(canRead, "Should be able to read a \\n-delimited message");
55+
Assert.True(sessionTransport.MessageReader.TryPeek(out var readMessage));
56+
Assert.NotNull(readMessage);
57+
Assert.IsType<JsonRpcRequest>(readMessage);
58+
Assert.Equal("44", ((JsonRpcRequest)readMessage).Id.ToString());
59+
}
60+
}

0 commit comments

Comments
 (0)