Skip to content

Commit 26c6fc1

Browse files
committed
Fix stdio encoding to always be UTF8
1 parent a97a74f commit 26c6fc1

File tree

4 files changed

+50
-12
lines changed

4 files changed

+50
-12
lines changed

src/ModelContextProtocol/Logging/Log.cs

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -113,8 +113,8 @@ internal static partial class Log
113113
[LoggerMessage(Level = LogLevel.Error, Message = "Transport not connected for {endpointName}")]
114114
internal static partial void TransportNotConnected(this ILogger logger, string endpointName);
115115

116-
[LoggerMessage(Level = LogLevel.Information, Message = "Transport sending message for {endpointName} with ID {messageId}, JSON {json}")]
117-
internal static partial void TransportSendingMessage(this ILogger logger, string endpointName, string messageId, string json);
116+
[LoggerMessage(Level = LogLevel.Information, Message = "Transport sending message for {endpointName} with ID {messageId}")]
117+
internal static partial void TransportSendingMessage(this ILogger logger, string endpointName, string messageId);
118118

119119
[LoggerMessage(Level = LogLevel.Information, Message = "Transport message sent for {endpointName} with ID {messageId}")]
120120
internal static partial void TransportSentMessage(this ILogger logger, string endpointName, string messageId);

src/ModelContextProtocol/Protocol/Transport/HttpListenerSseServerTransport.cs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -76,7 +76,7 @@ public override async Task SendMessageAsync(IJsonRpcMessage message, Cancellatio
7676
if (_logger.IsEnabled(LogLevel.Debug))
7777
{
7878
var json = JsonSerializer.Serialize(message, McpJsonUtilities.DefaultOptions.GetTypeInfo<IJsonRpcMessage>());
79-
_logger.TransportSendingMessage(EndpointName, id, json);
79+
_logger.TransportSendingMessage(EndpointName, id);
8080
}
8181

8282
await _sseResponseStreamTransport.SendMessageAsync(message, cancellationToken).ConfigureAwait(false);

src/ModelContextProtocol/Protocol/Transport/StdioClientTransport.cs

Lines changed: 35 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
using System.Diagnostics;
2+
using System.Text;
23
using System.Text.Json;
34
using ModelContextProtocol.Configuration;
45
using ModelContextProtocol.Logging;
@@ -59,7 +60,7 @@ public async Task ConnectAsync(CancellationToken cancellationToken = default)
5960

6061
_shutdownCts = new CancellationTokenSource();
6162

62-
var startInfo = new ProcessStartInfo
63+
ProcessStartInfo startInfo = new()
6364
{
6465
FileName = _options.Command,
6566
RedirectStandardInput = true,
@@ -68,6 +69,8 @@ public async Task ConnectAsync(CancellationToken cancellationToken = default)
6869
UseShellExecute = false,
6970
CreateNoWindow = true,
7071
WorkingDirectory = _options.WorkingDirectory ?? Environment.CurrentDirectory,
72+
StandardOutputEncoding = Encoding.UTF8,
73+
StandardErrorEncoding = Encoding.UTF8,
7174
};
7275

7376
if (!string.IsNullOrWhiteSpace(_options.Arguments))
@@ -92,11 +95,39 @@ public async Task ConnectAsync(CancellationToken cancellationToken = default)
9295
// Set up error logging
9396
_process.ErrorDataReceived += (sender, args) => _logger.TransportError(EndpointName, args.Data ?? "(no data)");
9497

95-
if (!_process.Start())
98+
bool processStarted;
99+
#if NET
100+
startInfo.StandardInputEncoding = Encoding.UTF8;
101+
processStarted = _process.Start();
102+
#else
103+
// netstandard2.0 lacks ProcessStartInfo.StandardInputEncoding. Instead, it always
104+
// uses Console.InputEncoding, so we need to temporarily change Console.InputEncoding
105+
// if it's not already UTF-8.
106+
Encoding oldStdinEncoding = Console.InputEncoding;
107+
if (oldStdinEncoding.CodePage == Encoding.UTF8.CodePage)
108+
{
109+
processStarted = _process.Start();
110+
}
111+
else
112+
{
113+
try
114+
{
115+
Console.InputEncoding = Encoding.UTF8;
116+
processStarted = _process.Start();
117+
}
118+
finally
119+
{
120+
Console.InputEncoding = oldStdinEncoding;
121+
}
122+
}
123+
#endif
124+
125+
if (!processStarted)
96126
{
97127
_logger.TransportProcessStartFailed(EndpointName);
98128
throw new McpTransportException("Failed to start MCP server process");
99129
}
130+
100131
_logger.TransportProcessStarted(EndpointName, _process.Id);
101132
_processStarted = true;
102133
_process.BeginErrorReadLine();
@@ -132,8 +163,9 @@ public override async Task SendMessageAsync(IJsonRpcMessage message, Cancellatio
132163

133164
try
134165
{
166+
_logger.TransportSendingMessage(EndpointName, id);
167+
135168
var json = JsonSerializer.Serialize(message, _jsonOptions.GetTypeInfo<IJsonRpcMessage>());
136-
_logger.TransportSendingMessage(EndpointName, id, json);
137169

138170
// Write the message followed by a newline
139171
await _process!.StandardInput.WriteLineAsync(json.AsMemory(), cancellationToken).ConfigureAwait(false);

src/ModelContextProtocol/Protocol/Transport/StdioServerTransport.cs

Lines changed: 12 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@
77
using Microsoft.Extensions.Logging;
88
using Microsoft.Extensions.Logging.Abstractions;
99
using Microsoft.Extensions.Options;
10+
using System.Text;
1011

1112
namespace ModelContextProtocol.Protocol.Transport;
1213

@@ -15,12 +16,14 @@ namespace ModelContextProtocol.Protocol.Transport;
1516
/// </summary>
1617
public sealed class StdioServerTransport : TransportBase, IServerTransport
1718
{
19+
private static readonly byte[] s_newLineBytes = Encoding.UTF8.GetBytes(Environment.NewLine);
20+
1821
private readonly string _serverName;
1922
private readonly ILogger _logger;
2023

2124
private readonly JsonSerializerOptions _jsonOptions = McpJsonUtilities.DefaultOptions;
22-
private readonly TextReader _stdin = Console.In;
23-
private readonly TextWriter _stdout = Console.Out;
25+
private readonly StreamReader _stdin = new StreamReader(Console.OpenStandardInput(), Encoding.UTF8);
26+
private readonly Stream _stdout = new BufferedStream(Console.OpenStandardOutput());
2427

2528
private Task? _readTask;
2629
private CancellationTokenSource? _shutdownCts;
@@ -114,11 +117,11 @@ public override async Task SendMessageAsync(IJsonRpcMessage message, Cancellatio
114117

115118
try
116119
{
117-
var json = JsonSerializer.Serialize(message, _jsonOptions.GetTypeInfo<IJsonRpcMessage>());
118-
_logger.TransportSendingMessage(EndpointName, id, json);
120+
_logger.TransportSendingMessage(EndpointName, id);
119121

120-
await _stdout.WriteLineAsync(json.AsMemory(), cancellationToken).ConfigureAwait(false);
121-
await _stdout.FlushAsync(cancellationToken).ConfigureAwait(false);
122+
await JsonSerializer.SerializeAsync(_stdout, message, _jsonOptions.GetTypeInfo<IJsonRpcMessage>(), cancellationToken);
123+
await _stdout.WriteAsync(s_newLineBytes, cancellationToken);
124+
await _stdout.FlushAsync(cancellationToken);
122125

123126
_logger.TransportSentMessage(EndpointName, id);
124127
}
@@ -239,6 +242,9 @@ private async Task CleanupAsync(CancellationToken cancellationToken)
239242
_logger.TransportReadTaskCleanedUp(EndpointName);
240243
}
241244

245+
_stdin.Dispose();
246+
_stdout.Dispose();
247+
242248
SetConnected(false);
243249
_logger.TransportCleanedUp(EndpointName);
244250
}

0 commit comments

Comments
 (0)