From ed475c96b8d8529728d7a94d7c21a0aa006a7c2e Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Sun, 15 Feb 2026 12:19:06 +0000 Subject: [PATCH 01/10] Initial plan From 0a62ad790fa309779c2509d6522d546ca4a41d57 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Sun, 15 Feb 2026 12:27:44 +0000 Subject: [PATCH 02/10] Avoid intermediate strings in MCP transport serialization - StreamServerTransport.SendMessageAsync: Use SerializeToUtf8Bytes instead of Serialize + Encoding.UTF8.GetBytes - StreamClientSessionTransport.SendMessageAsync: Refactor to write UTF-8 bytes directly to the stream instead of going through TextWriter with a string - McpHttpClient (netstandard2.0 path): Use SerializeToUtf8Bytes + ByteArrayContent instead of Serialize + StringContent - ClientOAuthProvider: Use SerializeToUtf8Bytes + ByteArrayContent instead of Serialize + StringContent Co-authored-by: stephentoub <2642209+stephentoub@users.noreply.github.com> --- .../Authentication/ClientOAuthProvider.cs | 5 +- .../Client/McpHttpClient.cs | 10 +-- .../Client/StreamClientSessionTransport.cs | 75 ++++++------------- .../Server/StreamServerTransport.cs | 6 +- 4 files changed, 33 insertions(+), 63 deletions(-) diff --git a/src/ModelContextProtocol.Core/Authentication/ClientOAuthProvider.cs b/src/ModelContextProtocol.Core/Authentication/ClientOAuthProvider.cs index 5b6aa8618..25b61ed44 100644 --- a/src/ModelContextProtocol.Core/Authentication/ClientOAuthProvider.cs +++ b/src/ModelContextProtocol.Core/Authentication/ClientOAuthProvider.cs @@ -618,8 +618,9 @@ private async Task PerformDynamicClientRegistrationAsync( Scope = GetScopeParameter(protectedResourceMetadata), }; - var requestJson = JsonSerializer.Serialize(registrationRequest, McpJsonUtilities.JsonContext.Default.DynamicClientRegistrationRequest); - using var requestContent = new StringContent(requestJson, Encoding.UTF8, "application/json"); + var requestBytes = JsonSerializer.SerializeToUtf8Bytes(registrationRequest, McpJsonUtilities.JsonContext.Default.DynamicClientRegistrationRequest); + using var requestContent = new ByteArrayContent(requestBytes); + requestContent.Headers.ContentType = new MediaTypeHeaderValue("application/json") { CharSet = "utf-8" }; using var request = new HttpRequestMessage(HttpMethod.Post, authServerMetadata.RegistrationEndpoint) { diff --git a/src/ModelContextProtocol.Core/Client/McpHttpClient.cs b/src/ModelContextProtocol.Core/Client/McpHttpClient.cs index 77ca78fb4..52c481bea 100644 --- a/src/ModelContextProtocol.Core/Client/McpHttpClient.cs +++ b/src/ModelContextProtocol.Core/Client/McpHttpClient.cs @@ -4,7 +4,6 @@ #if NET using System.Net.Http.Json; #else -using System.Text; using System.Text.Json; #endif @@ -32,11 +31,10 @@ internal virtual async Task SendAsync(HttpRequestMessage re #if NET return JsonContent.Create(message, McpJsonUtilities.JsonContext.Default.JsonRpcMessage); #else - return new StringContent( - JsonSerializer.Serialize(message, McpJsonUtilities.JsonContext.Default.JsonRpcMessage), - Encoding.UTF8, - "application/json" - ); + var bytes = JsonSerializer.SerializeToUtf8Bytes(message, McpJsonUtilities.JsonContext.Default.JsonRpcMessage); + var content = new ByteArrayContent(bytes); + content.Headers.ContentType = new System.Net.Http.Headers.MediaTypeHeaderValue("application/json") { CharSet = "utf-8" }; + return content; #endif } } diff --git a/src/ModelContextProtocol.Core/Client/StreamClientSessionTransport.cs b/src/ModelContextProtocol.Core/Client/StreamClientSessionTransport.cs index d582abe31..19306349f 100644 --- a/src/ModelContextProtocol.Core/Client/StreamClientSessionTransport.cs +++ b/src/ModelContextProtocol.Core/Client/StreamClientSessionTransport.cs @@ -8,10 +8,12 @@ namespace ModelContextProtocol.Client; /// Provides the client side of a stream-based session transport. internal class StreamClientSessionTransport : TransportBase { + private static readonly byte[] s_newlineBytes = "\n"u8.ToArray(); + internal static UTF8Encoding NoBomUtf8Encoding { get; } = new(encoderShouldEmitUTF8Identifier: false); private readonly TextReader _serverOutput; - private readonly TextWriter _serverInput; + private readonly Stream _serverInputStream; private readonly SemaphoreSlim _sendLock = new(1, 1); private CancellationTokenSource? _shutdownCts = new(); private Task? _readTask; @@ -20,12 +22,13 @@ internal class StreamClientSessionTransport : TransportBase /// Initializes a new instance of the class. /// /// - /// The text writer connected to the server's input stream. - /// Messages written to this writer will be sent to the server. + /// The server's input stream. Messages written to this stream will be sent to the server. /// /// - /// The text reader connected to the server's output stream. - /// Messages read from this reader will be received from the server. + /// The server's output stream. Messages read from this stream will be received from the server. + /// + /// + /// The encoding used for reading and writing messages from the input and output streams. Defaults to UTF-8 without BOM if null. /// /// /// A name that identifies this transport endpoint in logs. @@ -37,12 +40,18 @@ internal class StreamClientSessionTransport : TransportBase /// This constructor starts a background task to read messages from the server output stream. /// The transport will be marked as connected once initialized. /// - public StreamClientSessionTransport( - TextWriter serverInput, TextReader serverOutput, string endpointName, ILoggerFactory? loggerFactory) + public StreamClientSessionTransport(Stream serverInput, Stream serverOutput, Encoding? encoding, string endpointName, ILoggerFactory? loggerFactory) : base(endpointName, loggerFactory) { - _serverOutput = serverOutput; - _serverInput = serverInput; + Throw.IfNull(serverInput); + Throw.IfNull(serverOutput); + + _serverInputStream = serverInput; +#if NET + _serverOutput = new StreamReader(serverOutput, encoding ?? NoBomUtf8Encoding); +#else + _serverOutput = new CancellableStreamReader(serverOutput, encoding ?? NoBomUtf8Encoding); +#endif SetConnected(); @@ -57,43 +66,6 @@ public StreamClientSessionTransport( readTask.Start(); } - /// - /// Initializes a new instance of the class. - /// - /// - /// The server's input stream. Messages written to this stream will be sent to the server. - /// - /// - /// The server's output stream. Messages read from this stream will be received from the server. - /// - /// - /// The encoding used for reading and writing messages from the input and output streams. Defaults to UTF-8 without BOM if null. - /// - /// - /// A name that identifies this transport endpoint in logs. - /// - /// - /// Optional factory for creating loggers. If null, a NullLogger is used. - /// - /// - /// This constructor starts a background task to read messages from the server output stream. - /// The transport will be marked as connected once initialized. - /// - public StreamClientSessionTransport(Stream serverInput, Stream serverOutput, Encoding? encoding, string endpointName, ILoggerFactory? loggerFactory) - : this( - new StreamWriter(serverInput, encoding ?? NoBomUtf8Encoding), -#if NET - new StreamReader(serverOutput, encoding ?? NoBomUtf8Encoding), -#else - new CancellableStreamReader(serverOutput, encoding ?? NoBomUtf8Encoding), -#endif - endpointName, - loggerFactory) - { - Throw.IfNull(serverInput); - Throw.IfNull(serverOutput); - } - /// public override async Task SendMessageAsync(JsonRpcMessage message, CancellationToken cancellationToken = default) { @@ -103,16 +75,15 @@ public override async Task SendMessageAsync(JsonRpcMessage message, Cancellation id = messageWithId.Id.ToString(); } - var json = JsonSerializer.Serialize(message, McpJsonUtilities.JsonContext.Default.JsonRpcMessage); - - LogTransportSendingMessageSensitive(Name, json); + LogTransportSendingMessageSensitive(message); using var _ = await _sendLock.LockAsync(cancellationToken).ConfigureAwait(false); try { - // Write the message followed by a newline using our UTF-8 writer - await _serverInput.WriteLineAsync(json).ConfigureAwait(false); - await _serverInput.FlushAsync(cancellationToken).ConfigureAwait(false); + var json = JsonSerializer.SerializeToUtf8Bytes(message, McpJsonUtilities.JsonContext.Default.JsonRpcMessage); + await _serverInputStream.WriteAsync(json, cancellationToken).ConfigureAwait(false); + await _serverInputStream.WriteAsync(s_newlineBytes, cancellationToken).ConfigureAwait(false); + await _serverInputStream.FlushAsync(cancellationToken).ConfigureAwait(false); } catch (Exception ex) { diff --git a/src/ModelContextProtocol.Core/Server/StreamServerTransport.cs b/src/ModelContextProtocol.Core/Server/StreamServerTransport.cs index 1ab106e26..2202337f1 100644 --- a/src/ModelContextProtocol.Core/Server/StreamServerTransport.cs +++ b/src/ModelContextProtocol.Core/Server/StreamServerTransport.cs @@ -74,9 +74,9 @@ public override async Task SendMessageAsync(JsonRpcMessage message, Cancellation try { - var json = JsonSerializer.Serialize(message, McpJsonUtilities.JsonContext.Default.JsonRpcMessage); - LogTransportSendingMessageSensitive(Name, json); - await _outputStream.WriteAsync(Encoding.UTF8.GetBytes(json), cancellationToken).ConfigureAwait(false); + var json = JsonSerializer.SerializeToUtf8Bytes(message, McpJsonUtilities.JsonContext.Default.JsonRpcMessage); + LogTransportSendingMessageSensitive(message); + await _outputStream.WriteAsync(json, cancellationToken).ConfigureAwait(false); await _outputStream.WriteAsync(s_newlineBytes, cancellationToken).ConfigureAwait(false); await _outputStream.FlushAsync(cancellationToken).ConfigureAwait(false); } From 523a0cbf8fff71a87e74d09c2487fb971decaebc Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Sun, 15 Feb 2026 13:10:14 +0000 Subject: [PATCH 03/10] Revert StreamClientSessionTransport refactoring to fix CI failure The refactoring to bypass TextWriter and write directly to the underlying Stream changed the timing of pipe writes, causing the StdioClientTransportTests.CreateAsync_ValidProcessInvalidServer_StdErrCallbackInvoked test to fail on macOS and Ubuntu CI. Reverting to keep the original TextWriter-based write path while still using the deferred logging overload to avoid unnecessary serialization when trace logging is disabled. Co-authored-by: stephentoub <2642209+stephentoub@users.noreply.github.com> --- .../Client/StreamClientSessionTransport.cs | 73 +++++++++++++------ 1 file changed, 51 insertions(+), 22 deletions(-) diff --git a/src/ModelContextProtocol.Core/Client/StreamClientSessionTransport.cs b/src/ModelContextProtocol.Core/Client/StreamClientSessionTransport.cs index 19306349f..83181e88e 100644 --- a/src/ModelContextProtocol.Core/Client/StreamClientSessionTransport.cs +++ b/src/ModelContextProtocol.Core/Client/StreamClientSessionTransport.cs @@ -8,12 +8,10 @@ namespace ModelContextProtocol.Client; /// Provides the client side of a stream-based session transport. internal class StreamClientSessionTransport : TransportBase { - private static readonly byte[] s_newlineBytes = "\n"u8.ToArray(); - internal static UTF8Encoding NoBomUtf8Encoding { get; } = new(encoderShouldEmitUTF8Identifier: false); private readonly TextReader _serverOutput; - private readonly Stream _serverInputStream; + private readonly TextWriter _serverInput; private readonly SemaphoreSlim _sendLock = new(1, 1); private CancellationTokenSource? _shutdownCts = new(); private Task? _readTask; @@ -22,13 +20,12 @@ internal class StreamClientSessionTransport : TransportBase /// Initializes a new instance of the class. /// /// - /// The server's input stream. Messages written to this stream will be sent to the server. + /// The text writer connected to the server's input stream. + /// Messages written to this writer will be sent to the server. /// /// - /// The server's output stream. Messages read from this stream will be received from the server. - /// - /// - /// The encoding used for reading and writing messages from the input and output streams. Defaults to UTF-8 without BOM if null. + /// The text reader connected to the server's output stream. + /// Messages read from this reader will be received from the server. /// /// /// A name that identifies this transport endpoint in logs. @@ -40,18 +37,12 @@ internal class StreamClientSessionTransport : TransportBase /// This constructor starts a background task to read messages from the server output stream. /// The transport will be marked as connected once initialized. /// - public StreamClientSessionTransport(Stream serverInput, Stream serverOutput, Encoding? encoding, string endpointName, ILoggerFactory? loggerFactory) + public StreamClientSessionTransport( + TextWriter serverInput, TextReader serverOutput, string endpointName, ILoggerFactory? loggerFactory) : base(endpointName, loggerFactory) { - Throw.IfNull(serverInput); - Throw.IfNull(serverOutput); - - _serverInputStream = serverInput; -#if NET - _serverOutput = new StreamReader(serverOutput, encoding ?? NoBomUtf8Encoding); -#else - _serverOutput = new CancellableStreamReader(serverOutput, encoding ?? NoBomUtf8Encoding); -#endif + _serverOutput = serverOutput; + _serverInput = serverInput; SetConnected(); @@ -66,6 +57,43 @@ public StreamClientSessionTransport(Stream serverInput, Stream serverOutput, Enc readTask.Start(); } + /// + /// Initializes a new instance of the class. + /// + /// + /// The server's input stream. Messages written to this stream will be sent to the server. + /// + /// + /// The server's output stream. Messages read from this stream will be received from the server. + /// + /// + /// The encoding used for reading and writing messages from the input and output streams. Defaults to UTF-8 without BOM if null. + /// + /// + /// A name that identifies this transport endpoint in logs. + /// + /// + /// Optional factory for creating loggers. If null, a NullLogger is used. + /// + /// + /// This constructor starts a background task to read messages from the server output stream. + /// The transport will be marked as connected once initialized. + /// + public StreamClientSessionTransport(Stream serverInput, Stream serverOutput, Encoding? encoding, string endpointName, ILoggerFactory? loggerFactory) + : this( + new StreamWriter(serverInput, encoding ?? NoBomUtf8Encoding), +#if NET + new StreamReader(serverOutput, encoding ?? NoBomUtf8Encoding), +#else + new CancellableStreamReader(serverOutput, encoding ?? NoBomUtf8Encoding), +#endif + endpointName, + loggerFactory) + { + Throw.IfNull(serverInput); + Throw.IfNull(serverOutput); + } + /// public override async Task SendMessageAsync(JsonRpcMessage message, CancellationToken cancellationToken = default) { @@ -77,13 +105,14 @@ public override async Task SendMessageAsync(JsonRpcMessage message, Cancellation LogTransportSendingMessageSensitive(message); + var json = JsonSerializer.Serialize(message, McpJsonUtilities.JsonContext.Default.JsonRpcMessage); + using var _ = await _sendLock.LockAsync(cancellationToken).ConfigureAwait(false); try { - var json = JsonSerializer.SerializeToUtf8Bytes(message, McpJsonUtilities.JsonContext.Default.JsonRpcMessage); - await _serverInputStream.WriteAsync(json, cancellationToken).ConfigureAwait(false); - await _serverInputStream.WriteAsync(s_newlineBytes, cancellationToken).ConfigureAwait(false); - await _serverInputStream.FlushAsync(cancellationToken).ConfigureAwait(false); + // Write the message followed by a newline using our UTF-8 writer + await _serverInput.WriteLineAsync(json).ConfigureAwait(false); + await _serverInput.FlushAsync(cancellationToken).ConfigureAwait(false); } catch (Exception ex) { From dca09940277cb97f41c220e98841f6edc8a68007 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Sun, 15 Feb 2026 13:46:28 +0000 Subject: [PATCH 04/10] Re-apply StreamClientSessionTransport optimization: use SerializeToUtf8Bytes The previous CI failure was a pre-existing flaky test (StdErrCallbackInvoked), not caused by this change. The test's stderr callback mechanism (process.BeginErrorReadLine in StdioClientTransport) is independent of how stdin writes are performed. Changes: - Remove TextWriter-based constructor and StreamWriter wrapping - Store raw Stream for writes, use SerializeToUtf8Bytes + direct stream writes - Keep TextReader wrapping for reads (needed for ReadLineAsync) Co-authored-by: stephentoub <2642209+stephentoub@users.noreply.github.com> --- .../Client/StreamClientSessionTransport.cs | 73 ++++++------------- 1 file changed, 22 insertions(+), 51 deletions(-) diff --git a/src/ModelContextProtocol.Core/Client/StreamClientSessionTransport.cs b/src/ModelContextProtocol.Core/Client/StreamClientSessionTransport.cs index 83181e88e..19306349f 100644 --- a/src/ModelContextProtocol.Core/Client/StreamClientSessionTransport.cs +++ b/src/ModelContextProtocol.Core/Client/StreamClientSessionTransport.cs @@ -8,10 +8,12 @@ namespace ModelContextProtocol.Client; /// Provides the client side of a stream-based session transport. internal class StreamClientSessionTransport : TransportBase { + private static readonly byte[] s_newlineBytes = "\n"u8.ToArray(); + internal static UTF8Encoding NoBomUtf8Encoding { get; } = new(encoderShouldEmitUTF8Identifier: false); private readonly TextReader _serverOutput; - private readonly TextWriter _serverInput; + private readonly Stream _serverInputStream; private readonly SemaphoreSlim _sendLock = new(1, 1); private CancellationTokenSource? _shutdownCts = new(); private Task? _readTask; @@ -20,12 +22,13 @@ internal class StreamClientSessionTransport : TransportBase /// Initializes a new instance of the class. /// /// - /// The text writer connected to the server's input stream. - /// Messages written to this writer will be sent to the server. + /// The server's input stream. Messages written to this stream will be sent to the server. /// /// - /// The text reader connected to the server's output stream. - /// Messages read from this reader will be received from the server. + /// The server's output stream. Messages read from this stream will be received from the server. + /// + /// + /// The encoding used for reading and writing messages from the input and output streams. Defaults to UTF-8 without BOM if null. /// /// /// A name that identifies this transport endpoint in logs. @@ -37,12 +40,18 @@ internal class StreamClientSessionTransport : TransportBase /// This constructor starts a background task to read messages from the server output stream. /// The transport will be marked as connected once initialized. /// - public StreamClientSessionTransport( - TextWriter serverInput, TextReader serverOutput, string endpointName, ILoggerFactory? loggerFactory) + public StreamClientSessionTransport(Stream serverInput, Stream serverOutput, Encoding? encoding, string endpointName, ILoggerFactory? loggerFactory) : base(endpointName, loggerFactory) { - _serverOutput = serverOutput; - _serverInput = serverInput; + Throw.IfNull(serverInput); + Throw.IfNull(serverOutput); + + _serverInputStream = serverInput; +#if NET + _serverOutput = new StreamReader(serverOutput, encoding ?? NoBomUtf8Encoding); +#else + _serverOutput = new CancellableStreamReader(serverOutput, encoding ?? NoBomUtf8Encoding); +#endif SetConnected(); @@ -57,43 +66,6 @@ public StreamClientSessionTransport( readTask.Start(); } - /// - /// Initializes a new instance of the class. - /// - /// - /// The server's input stream. Messages written to this stream will be sent to the server. - /// - /// - /// The server's output stream. Messages read from this stream will be received from the server. - /// - /// - /// The encoding used for reading and writing messages from the input and output streams. Defaults to UTF-8 without BOM if null. - /// - /// - /// A name that identifies this transport endpoint in logs. - /// - /// - /// Optional factory for creating loggers. If null, a NullLogger is used. - /// - /// - /// This constructor starts a background task to read messages from the server output stream. - /// The transport will be marked as connected once initialized. - /// - public StreamClientSessionTransport(Stream serverInput, Stream serverOutput, Encoding? encoding, string endpointName, ILoggerFactory? loggerFactory) - : this( - new StreamWriter(serverInput, encoding ?? NoBomUtf8Encoding), -#if NET - new StreamReader(serverOutput, encoding ?? NoBomUtf8Encoding), -#else - new CancellableStreamReader(serverOutput, encoding ?? NoBomUtf8Encoding), -#endif - endpointName, - loggerFactory) - { - Throw.IfNull(serverInput); - Throw.IfNull(serverOutput); - } - /// public override async Task SendMessageAsync(JsonRpcMessage message, CancellationToken cancellationToken = default) { @@ -105,14 +77,13 @@ public override async Task SendMessageAsync(JsonRpcMessage message, Cancellation LogTransportSendingMessageSensitive(message); - var json = JsonSerializer.Serialize(message, McpJsonUtilities.JsonContext.Default.JsonRpcMessage); - using var _ = await _sendLock.LockAsync(cancellationToken).ConfigureAwait(false); try { - // Write the message followed by a newline using our UTF-8 writer - await _serverInput.WriteLineAsync(json).ConfigureAwait(false); - await _serverInput.FlushAsync(cancellationToken).ConfigureAwait(false); + var json = JsonSerializer.SerializeToUtf8Bytes(message, McpJsonUtilities.JsonContext.Default.JsonRpcMessage); + await _serverInputStream.WriteAsync(json, cancellationToken).ConfigureAwait(false); + await _serverInputStream.WriteAsync(s_newlineBytes, cancellationToken).ConfigureAwait(false); + await _serverInputStream.FlushAsync(cancellationToken).ConfigureAwait(false); } catch (Exception ex) { From 87112a1d0aec9a525b06a61e451d605cb1fd26ab Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Sun, 15 Feb 2026 14:28:49 +0000 Subject: [PATCH 05/10] Cache MediaTypeHeaderValue instance for application/json content type Use a static readonly MediaTypeHeaderValue instance shared between McpHttpClient and ClientOAuthProvider instead of creating a new instance on every request. Co-authored-by: stephentoub <2642209+stephentoub@users.noreply.github.com> --- .../Authentication/ClientOAuthProvider.cs | 2 +- src/ModelContextProtocol.Core/Client/McpHttpClient.cs | 5 ++++- 2 files changed, 5 insertions(+), 2 deletions(-) diff --git a/src/ModelContextProtocol.Core/Authentication/ClientOAuthProvider.cs b/src/ModelContextProtocol.Core/Authentication/ClientOAuthProvider.cs index 25b61ed44..83f664f1e 100644 --- a/src/ModelContextProtocol.Core/Authentication/ClientOAuthProvider.cs +++ b/src/ModelContextProtocol.Core/Authentication/ClientOAuthProvider.cs @@ -620,7 +620,7 @@ private async Task PerformDynamicClientRegistrationAsync( var requestBytes = JsonSerializer.SerializeToUtf8Bytes(registrationRequest, McpJsonUtilities.JsonContext.Default.DynamicClientRegistrationRequest); using var requestContent = new ByteArrayContent(requestBytes); - requestContent.Headers.ContentType = new MediaTypeHeaderValue("application/json") { CharSet = "utf-8" }; + requestContent.Headers.ContentType = McpHttpClient.s_applicationJsonContentType; using var request = new HttpRequestMessage(HttpMethod.Post, authServerMetadata.RegistrationEndpoint) { diff --git a/src/ModelContextProtocol.Core/Client/McpHttpClient.cs b/src/ModelContextProtocol.Core/Client/McpHttpClient.cs index 52c481bea..7caf50143 100644 --- a/src/ModelContextProtocol.Core/Client/McpHttpClient.cs +++ b/src/ModelContextProtocol.Core/Client/McpHttpClient.cs @@ -1,5 +1,6 @@ using ModelContextProtocol.Protocol; using System.Diagnostics; +using System.Net.Http.Headers; #if NET using System.Net.Http.Json; @@ -11,6 +12,8 @@ namespace ModelContextProtocol.Client; internal class McpHttpClient(HttpClient httpClient) { + internal static readonly MediaTypeHeaderValue s_applicationJsonContentType = new("application/json") { CharSet = "utf-8" }; + internal virtual async Task SendAsync(HttpRequestMessage request, JsonRpcMessage? message, CancellationToken cancellationToken) { Debug.Assert(request.Content is null, "The request body should only be supplied as a JsonRpcMessage"); @@ -33,7 +36,7 @@ internal virtual async Task SendAsync(HttpRequestMessage re #else var bytes = JsonSerializer.SerializeToUtf8Bytes(message, McpJsonUtilities.JsonContext.Default.JsonRpcMessage); var content = new ByteArrayContent(bytes); - content.Headers.ContentType = new System.Net.Http.Headers.MediaTypeHeaderValue("application/json") { CharSet = "utf-8" }; + content.Headers.ContentType = s_applicationJsonContentType; return content; #endif } From 4216490da521497718d2f3d7f83eb7060c069e92 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Wed, 18 Feb 2026 01:12:26 +0000 Subject: [PATCH 06/10] 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> --- .../Transport/StdioServerTransportTests.cs | 48 +++++++++++++++ .../Transport/StreamClientTransportTests.cs | 60 +++++++++++++++++++ 2 files changed, 108 insertions(+) create mode 100644 tests/ModelContextProtocol.Tests/Transport/StreamClientTransportTests.cs diff --git a/tests/ModelContextProtocol.Tests/Transport/StdioServerTransportTests.cs b/tests/ModelContextProtocol.Tests/Transport/StdioServerTransportTests.cs index 54a1a9669..fbbb83b13 100644 --- a/tests/ModelContextProtocol.Tests/Transport/StdioServerTransportTests.cs +++ b/tests/ModelContextProtocol.Tests/Transport/StdioServerTransportTests.cs @@ -227,6 +227,54 @@ public async Task SendMessageAsync_Should_Log_At_Trace_Level() Assert.Contains(traceLogMessages, x => x.Message.Contains("\"method\":\"test\"") && x.Message.Contains("\"id\":44")); } + [Fact] + public async Task SendMessageAsync_Should_Use_LF_Not_CRLF() + { + using var output = new MemoryStream(); + + await using var transport = new StreamServerTransport( + new Pipe().Reader.AsStream(), + output, + loggerFactory: LoggerFactory); + + var message = new JsonRpcRequest { Method = "test", Id = new RequestId(44) }; + + await transport.SendMessageAsync(message, TestContext.Current.CancellationToken); + + byte[] bytes = output.ToArray(); + + // The output should end with exactly \n (0x0A), not \r\n (0x0D 0x0A). + Assert.True(bytes.Length > 1, "Output should contain message data"); + Assert.Equal((byte)'\n', bytes[^1]); + Assert.NotEqual((byte)'\r', bytes[^2]); + } + + [Fact] + public async Task ReadMessagesAsync_Should_Accept_LF_Delimited_Messages() + { + var message = new JsonRpcRequest { Method = "test", Id = new RequestId(44) }; + var json = JsonSerializer.Serialize(message, McpJsonUtilities.DefaultOptions); + + Pipe pipe = new(); + using var input = pipe.Reader.AsStream(); + + await using var transport = new StreamServerTransport( + input, + Stream.Null, + loggerFactory: LoggerFactory); + + // Write the message with \n line ending (not \r\n) + await pipe.Writer.WriteAsync(Encoding.UTF8.GetBytes($"{json}\n"), TestContext.Current.CancellationToken); + + var canRead = await transport.MessageReader.WaitToReadAsync(TestContext.Current.CancellationToken); + + Assert.True(canRead, "Should be able to read a \\n-delimited message"); + Assert.True(transport.MessageReader.TryPeek(out var readMessage)); + Assert.NotNull(readMessage); + Assert.IsType(readMessage); + Assert.Equal("44", ((JsonRpcRequest)readMessage).Id.ToString()); + } + [Fact] public async Task ReadMessagesAsync_Should_Log_Received_At_Trace_Level() { diff --git a/tests/ModelContextProtocol.Tests/Transport/StreamClientTransportTests.cs b/tests/ModelContextProtocol.Tests/Transport/StreamClientTransportTests.cs new file mode 100644 index 000000000..e3cdc91e5 --- /dev/null +++ b/tests/ModelContextProtocol.Tests/Transport/StreamClientTransportTests.cs @@ -0,0 +1,60 @@ +using ModelContextProtocol.Protocol; +using ModelContextProtocol.Tests.Utils; +using System.IO.Pipelines; +using System.Text; +using System.Text.Json; + +namespace ModelContextProtocol.Tests.Transport; + +public class StreamClientTransportTests(ITestOutputHelper testOutputHelper) : LoggedTest(testOutputHelper) +{ + [Fact] + public async Task SendMessageAsync_Should_Use_LF_Not_CRLF() + { + using var serverInput = new MemoryStream(); + Pipe serverOutputPipe = new(); + + var transport = new StreamClientTransport(serverInput, serverOutputPipe.Reader.AsStream(), LoggerFactory); + await using var sessionTransport = await transport.ConnectAsync(TestContext.Current.CancellationToken); + + var message = new JsonRpcRequest { Method = "test", Id = new RequestId(44) }; + + await sessionTransport.SendMessageAsync(message, TestContext.Current.CancellationToken); + + byte[] bytes = serverInput.ToArray(); + + // The output should end with exactly \n (0x0A), not \r\n (0x0D 0x0A). + Assert.True(bytes.Length > 1, "Output should contain message data"); + Assert.Equal((byte)'\n', bytes[^1]); + Assert.NotEqual((byte)'\r', bytes[^2]); + + // Also verify the JSON content is valid + var json = Encoding.UTF8.GetString(bytes).TrimEnd('\n'); + var expected = JsonSerializer.Serialize(message, McpJsonUtilities.DefaultOptions); + Assert.Equal(expected, json); + } + + [Fact] + public async Task ReadMessagesAsync_Should_Accept_LF_Delimited_Messages() + { + Pipe serverInputPipe = new(); + Pipe serverOutputPipe = new(); + + var transport = new StreamClientTransport(serverInputPipe.Writer.AsStream(), serverOutputPipe.Reader.AsStream(), LoggerFactory); + await using var sessionTransport = await transport.ConnectAsync(TestContext.Current.CancellationToken); + + var message = new JsonRpcRequest { Method = "test", Id = new RequestId(44) }; + var json = JsonSerializer.Serialize(message, McpJsonUtilities.DefaultOptions); + + // Write a \n-delimited message to the server's output (which the client reads) + await serverOutputPipe.Writer.WriteAsync(Encoding.UTF8.GetBytes($"{json}\n"), TestContext.Current.CancellationToken); + + var canRead = await sessionTransport.MessageReader.WaitToReadAsync(TestContext.Current.CancellationToken); + + Assert.True(canRead, "Should be able to read a \\n-delimited message"); + Assert.True(sessionTransport.MessageReader.TryPeek(out var readMessage)); + Assert.NotNull(readMessage); + Assert.IsType(readMessage); + Assert.Equal("44", ((JsonRpcRequest)readMessage).Id.ToString()); + } +} From 8fddfa147571fd8b5cbd322142f7439931f3531c Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Wed, 18 Feb 2026 01:25:27 +0000 Subject: [PATCH 07/10] Replace LF read tests with CRLF read tests, skipped on non-Windows Per review feedback, the LF read tests were redundant since we now always emit LF. Replace them with CRLF acceptance tests that validate backward compatibility with \r\n-delimited messages, skipped on non-Windows platforms. Co-authored-by: halter73 <54385+halter73@users.noreply.github.com> --- .../Transport/StdioServerTransportTests.cs | 12 +++++++----- .../Transport/StreamClientTransportTests.cs | 12 +++++++----- 2 files changed, 14 insertions(+), 10 deletions(-) diff --git a/tests/ModelContextProtocol.Tests/Transport/StdioServerTransportTests.cs b/tests/ModelContextProtocol.Tests/Transport/StdioServerTransportTests.cs index fbbb83b13..b508cc496 100644 --- a/tests/ModelContextProtocol.Tests/Transport/StdioServerTransportTests.cs +++ b/tests/ModelContextProtocol.Tests/Transport/StdioServerTransportTests.cs @@ -249,8 +249,10 @@ public async Task SendMessageAsync_Should_Use_LF_Not_CRLF() Assert.NotEqual((byte)'\r', bytes[^2]); } - [Fact] - public async Task ReadMessagesAsync_Should_Accept_LF_Delimited_Messages() + public static bool IsWindows => PlatformDetection.IsWindows; + + [Fact(Skip = "Non-Windows platform", SkipUnless = nameof(IsWindows))] + public async Task ReadMessagesAsync_Should_Accept_CRLF_Delimited_Messages() { var message = new JsonRpcRequest { Method = "test", Id = new RequestId(44) }; var json = JsonSerializer.Serialize(message, McpJsonUtilities.DefaultOptions); @@ -263,12 +265,12 @@ public async Task ReadMessagesAsync_Should_Accept_LF_Delimited_Messages() Stream.Null, loggerFactory: LoggerFactory); - // Write the message with \n line ending (not \r\n) - await pipe.Writer.WriteAsync(Encoding.UTF8.GetBytes($"{json}\n"), TestContext.Current.CancellationToken); + // Write the message with \r\n line ending + await pipe.Writer.WriteAsync(Encoding.UTF8.GetBytes($"{json}\r\n"), TestContext.Current.CancellationToken); var canRead = await transport.MessageReader.WaitToReadAsync(TestContext.Current.CancellationToken); - Assert.True(canRead, "Should be able to read a \\n-delimited message"); + Assert.True(canRead, "Should be able to read a \\r\\n-delimited message"); Assert.True(transport.MessageReader.TryPeek(out var readMessage)); Assert.NotNull(readMessage); Assert.IsType(readMessage); diff --git a/tests/ModelContextProtocol.Tests/Transport/StreamClientTransportTests.cs b/tests/ModelContextProtocol.Tests/Transport/StreamClientTransportTests.cs index e3cdc91e5..b54345310 100644 --- a/tests/ModelContextProtocol.Tests/Transport/StreamClientTransportTests.cs +++ b/tests/ModelContextProtocol.Tests/Transport/StreamClientTransportTests.cs @@ -8,6 +8,8 @@ namespace ModelContextProtocol.Tests.Transport; public class StreamClientTransportTests(ITestOutputHelper testOutputHelper) : LoggedTest(testOutputHelper) { + public static bool IsWindows => PlatformDetection.IsWindows; + [Fact] public async Task SendMessageAsync_Should_Use_LF_Not_CRLF() { @@ -34,8 +36,8 @@ public async Task SendMessageAsync_Should_Use_LF_Not_CRLF() Assert.Equal(expected, json); } - [Fact] - public async Task ReadMessagesAsync_Should_Accept_LF_Delimited_Messages() + [Fact(Skip = "Non-Windows platform", SkipUnless = nameof(IsWindows))] + public async Task ReadMessagesAsync_Should_Accept_CRLF_Delimited_Messages() { Pipe serverInputPipe = new(); Pipe serverOutputPipe = new(); @@ -46,12 +48,12 @@ public async Task ReadMessagesAsync_Should_Accept_LF_Delimited_Messages() var message = new JsonRpcRequest { Method = "test", Id = new RequestId(44) }; var json = JsonSerializer.Serialize(message, McpJsonUtilities.DefaultOptions); - // Write a \n-delimited message to the server's output (which the client reads) - await serverOutputPipe.Writer.WriteAsync(Encoding.UTF8.GetBytes($"{json}\n"), TestContext.Current.CancellationToken); + // Write a \r\n-delimited message to the server's output (which the client reads) + await serverOutputPipe.Writer.WriteAsync(Encoding.UTF8.GetBytes($"{json}\r\n"), TestContext.Current.CancellationToken); var canRead = await sessionTransport.MessageReader.WaitToReadAsync(TestContext.Current.CancellationToken); - Assert.True(canRead, "Should be able to read a \\n-delimited message"); + Assert.True(canRead, "Should be able to read a \\r\\n-delimited message"); Assert.True(sessionTransport.MessageReader.TryPeek(out var readMessage)); Assert.NotNull(readMessage); Assert.IsType(readMessage); From a26f019688a6099c97fb0e00492e917c5b74b79f Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Wed, 18 Feb 2026 02:04:07 +0000 Subject: [PATCH 08/10] Move StreamClientTransport tests into StdioClientTransportTests Per review feedback, consolidate stream client transport tests into StdioClientTransportTests.cs for consistency with how StreamServerTransport tests live in StdioServerTransportTests.cs. Co-authored-by: halter73 <54385+halter73@users.noreply.github.com> --- .../Transport/StdioClientTransportTests.cs | 53 ++++++++++++++++ .../Transport/StreamClientTransportTests.cs | 62 ------------------- 2 files changed, 53 insertions(+), 62 deletions(-) delete mode 100644 tests/ModelContextProtocol.Tests/Transport/StreamClientTransportTests.cs diff --git a/tests/ModelContextProtocol.Tests/Transport/StdioClientTransportTests.cs b/tests/ModelContextProtocol.Tests/Transport/StdioClientTransportTests.cs index c33ed7f72..3ddaa406e 100644 --- a/tests/ModelContextProtocol.Tests/Transport/StdioClientTransportTests.cs +++ b/tests/ModelContextProtocol.Tests/Transport/StdioClientTransportTests.cs @@ -1,14 +1,17 @@ using ModelContextProtocol.Client; using ModelContextProtocol.Protocol; using ModelContextProtocol.Tests.Utils; +using System.IO.Pipelines; using System.Runtime.InteropServices; using System.Text; +using System.Text.Json; namespace ModelContextProtocol.Tests.Transport; public class StdioClientTransportTests(ITestOutputHelper testOutputHelper) : LoggedTest(testOutputHelper) { public static bool IsStdErrCallbackSupported => !PlatformDetection.IsMonoRuntime; + public static bool IsWindows => PlatformDetection.IsWindows; [Fact] public async Task CreateAsync_ValidProcessInvalidServer_Throws() @@ -136,4 +139,54 @@ public async Task EscapesCliArgumentsCorrectly(string? cliArgumentValue) var content = Assert.IsType(Assert.Single(result.Content)); Assert.Equal(cliArgumentValue ?? "", content.Text); } + + [Fact] + public async Task SendMessageAsync_Should_Use_LF_Not_CRLF() + { + using var serverInput = new MemoryStream(); + Pipe serverOutputPipe = new(); + + var transport = new StreamClientTransport(serverInput, serverOutputPipe.Reader.AsStream(), LoggerFactory); + await using var sessionTransport = await transport.ConnectAsync(TestContext.Current.CancellationToken); + + var message = new JsonRpcRequest { Method = "test", Id = new RequestId(44) }; + + await sessionTransport.SendMessageAsync(message, TestContext.Current.CancellationToken); + + byte[] bytes = serverInput.ToArray(); + + // The output should end with exactly \n (0x0A), not \r\n (0x0D 0x0A). + Assert.True(bytes.Length > 1, "Output should contain message data"); + Assert.Equal((byte)'\n', bytes[^1]); + Assert.NotEqual((byte)'\r', bytes[^2]); + + // Also verify the JSON content is valid + var json = Encoding.UTF8.GetString(bytes).TrimEnd('\n'); + var expected = JsonSerializer.Serialize(message, McpJsonUtilities.DefaultOptions); + Assert.Equal(expected, json); + } + + [Fact(Skip = "Non-Windows platform", SkipUnless = nameof(IsWindows))] + public async Task ReadMessagesAsync_Should_Accept_CRLF_Delimited_Messages() + { + Pipe serverInputPipe = new(); + Pipe serverOutputPipe = new(); + + var transport = new StreamClientTransport(serverInputPipe.Writer.AsStream(), serverOutputPipe.Reader.AsStream(), LoggerFactory); + await using var sessionTransport = await transport.ConnectAsync(TestContext.Current.CancellationToken); + + var message = new JsonRpcRequest { Method = "test", Id = new RequestId(44) }; + var json = JsonSerializer.Serialize(message, McpJsonUtilities.DefaultOptions); + + // Write a \r\n-delimited message to the server's output (which the client reads) + await serverOutputPipe.Writer.WriteAsync(Encoding.UTF8.GetBytes($"{json}\r\n"), TestContext.Current.CancellationToken); + + var canRead = await sessionTransport.MessageReader.WaitToReadAsync(TestContext.Current.CancellationToken); + + Assert.True(canRead, "Should be able to read a \\r\\n-delimited message"); + Assert.True(sessionTransport.MessageReader.TryPeek(out var readMessage)); + Assert.NotNull(readMessage); + Assert.IsType(readMessage); + Assert.Equal("44", ((JsonRpcRequest)readMessage).Id.ToString()); + } } diff --git a/tests/ModelContextProtocol.Tests/Transport/StreamClientTransportTests.cs b/tests/ModelContextProtocol.Tests/Transport/StreamClientTransportTests.cs deleted file mode 100644 index b54345310..000000000 --- a/tests/ModelContextProtocol.Tests/Transport/StreamClientTransportTests.cs +++ /dev/null @@ -1,62 +0,0 @@ -using ModelContextProtocol.Protocol; -using ModelContextProtocol.Tests.Utils; -using System.IO.Pipelines; -using System.Text; -using System.Text.Json; - -namespace ModelContextProtocol.Tests.Transport; - -public class StreamClientTransportTests(ITestOutputHelper testOutputHelper) : LoggedTest(testOutputHelper) -{ - public static bool IsWindows => PlatformDetection.IsWindows; - - [Fact] - public async Task SendMessageAsync_Should_Use_LF_Not_CRLF() - { - using var serverInput = new MemoryStream(); - Pipe serverOutputPipe = new(); - - var transport = new StreamClientTransport(serverInput, serverOutputPipe.Reader.AsStream(), LoggerFactory); - await using var sessionTransport = await transport.ConnectAsync(TestContext.Current.CancellationToken); - - var message = new JsonRpcRequest { Method = "test", Id = new RequestId(44) }; - - await sessionTransport.SendMessageAsync(message, TestContext.Current.CancellationToken); - - byte[] bytes = serverInput.ToArray(); - - // The output should end with exactly \n (0x0A), not \r\n (0x0D 0x0A). - Assert.True(bytes.Length > 1, "Output should contain message data"); - Assert.Equal((byte)'\n', bytes[^1]); - Assert.NotEqual((byte)'\r', bytes[^2]); - - // Also verify the JSON content is valid - var json = Encoding.UTF8.GetString(bytes).TrimEnd('\n'); - var expected = JsonSerializer.Serialize(message, McpJsonUtilities.DefaultOptions); - Assert.Equal(expected, json); - } - - [Fact(Skip = "Non-Windows platform", SkipUnless = nameof(IsWindows))] - public async Task ReadMessagesAsync_Should_Accept_CRLF_Delimited_Messages() - { - Pipe serverInputPipe = new(); - Pipe serverOutputPipe = new(); - - var transport = new StreamClientTransport(serverInputPipe.Writer.AsStream(), serverOutputPipe.Reader.AsStream(), LoggerFactory); - await using var sessionTransport = await transport.ConnectAsync(TestContext.Current.CancellationToken); - - var message = new JsonRpcRequest { Method = "test", Id = new RequestId(44) }; - var json = JsonSerializer.Serialize(message, McpJsonUtilities.DefaultOptions); - - // Write a \r\n-delimited message to the server's output (which the client reads) - await serverOutputPipe.Writer.WriteAsync(Encoding.UTF8.GetBytes($"{json}\r\n"), TestContext.Current.CancellationToken); - - var canRead = await sessionTransport.MessageReader.WaitToReadAsync(TestContext.Current.CancellationToken); - - Assert.True(canRead, "Should be able to read a \\r\\n-delimited message"); - Assert.True(sessionTransport.MessageReader.TryPeek(out var readMessage)); - Assert.NotNull(readMessage); - Assert.IsType(readMessage); - Assert.Equal("44", ((JsonRpcRequest)readMessage).Id.ToString()); - } -} From 9530eeba5752ed86f804e8ad242dfcf468fe6081 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Wed, 18 Feb 2026 02:10:42 +0000 Subject: [PATCH 09/10] Move IsWindows property to top of StdioServerTransportTests class Co-authored-by: halter73 <54385+halter73@users.noreply.github.com> --- .../Transport/StdioServerTransportTests.cs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/tests/ModelContextProtocol.Tests/Transport/StdioServerTransportTests.cs b/tests/ModelContextProtocol.Tests/Transport/StdioServerTransportTests.cs index b508cc496..0ed1996ac 100644 --- a/tests/ModelContextProtocol.Tests/Transport/StdioServerTransportTests.cs +++ b/tests/ModelContextProtocol.Tests/Transport/StdioServerTransportTests.cs @@ -11,6 +11,8 @@ namespace ModelContextProtocol.Tests.Transport; public class StdioServerTransportTests : LoggedTest { + public static bool IsWindows => PlatformDetection.IsWindows; + private readonly McpServerOptions _serverOptions; public StdioServerTransportTests(ITestOutputHelper testOutputHelper) @@ -249,8 +251,6 @@ public async Task SendMessageAsync_Should_Use_LF_Not_CRLF() Assert.NotEqual((byte)'\r', bytes[^2]); } - public static bool IsWindows => PlatformDetection.IsWindows; - [Fact(Skip = "Non-Windows platform", SkipUnless = nameof(IsWindows))] public async Task ReadMessagesAsync_Should_Accept_CRLF_Delimited_Messages() { From 5380b2cec72fe6c5b28e5a375919c678b962e1ac Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Wed, 18 Feb 2026 11:04:16 +0000 Subject: [PATCH 10/10] Remove Windows-only skip from CRLF read acceptance tests TextReader.ReadLineAsync handles \r\n on all platforms, so there's no reason to restrict these tests to Windows. Co-authored-by: stephentoub <2642209+stephentoub@users.noreply.github.com> --- .../Transport/StdioClientTransportTests.cs | 3 +-- .../Transport/StdioServerTransportTests.cs | 4 +--- 2 files changed, 2 insertions(+), 5 deletions(-) diff --git a/tests/ModelContextProtocol.Tests/Transport/StdioClientTransportTests.cs b/tests/ModelContextProtocol.Tests/Transport/StdioClientTransportTests.cs index 3ddaa406e..16cd352c3 100644 --- a/tests/ModelContextProtocol.Tests/Transport/StdioClientTransportTests.cs +++ b/tests/ModelContextProtocol.Tests/Transport/StdioClientTransportTests.cs @@ -11,7 +11,6 @@ namespace ModelContextProtocol.Tests.Transport; public class StdioClientTransportTests(ITestOutputHelper testOutputHelper) : LoggedTest(testOutputHelper) { public static bool IsStdErrCallbackSupported => !PlatformDetection.IsMonoRuntime; - public static bool IsWindows => PlatformDetection.IsWindows; [Fact] public async Task CreateAsync_ValidProcessInvalidServer_Throws() @@ -166,7 +165,7 @@ public async Task SendMessageAsync_Should_Use_LF_Not_CRLF() Assert.Equal(expected, json); } - [Fact(Skip = "Non-Windows platform", SkipUnless = nameof(IsWindows))] + [Fact] public async Task ReadMessagesAsync_Should_Accept_CRLF_Delimited_Messages() { Pipe serverInputPipe = new(); diff --git a/tests/ModelContextProtocol.Tests/Transport/StdioServerTransportTests.cs b/tests/ModelContextProtocol.Tests/Transport/StdioServerTransportTests.cs index 0ed1996ac..fa5b425ba 100644 --- a/tests/ModelContextProtocol.Tests/Transport/StdioServerTransportTests.cs +++ b/tests/ModelContextProtocol.Tests/Transport/StdioServerTransportTests.cs @@ -11,8 +11,6 @@ namespace ModelContextProtocol.Tests.Transport; public class StdioServerTransportTests : LoggedTest { - public static bool IsWindows => PlatformDetection.IsWindows; - private readonly McpServerOptions _serverOptions; public StdioServerTransportTests(ITestOutputHelper testOutputHelper) @@ -251,7 +249,7 @@ public async Task SendMessageAsync_Should_Use_LF_Not_CRLF() Assert.NotEqual((byte)'\r', bytes[^2]); } - [Fact(Skip = "Non-Windows platform", SkipUnless = nameof(IsWindows))] + [Fact] public async Task ReadMessagesAsync_Should_Accept_CRLF_Delimited_Messages() { var message = new JsonRpcRequest { Method = "test", Id = new RequestId(44) };