Skip to content

Commit f47d99f

Browse files
committed
Address PR feedback
1 parent d2852c0 commit f47d99f

File tree

6 files changed

+127
-170
lines changed

6 files changed

+127
-170
lines changed

src/ModelContextProtocol/Logging/Log.cs

Lines changed: 32 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -114,7 +114,7 @@ internal static partial class Log
114114
internal static partial void TransportNotConnected(this ILogger logger, string endpointName);
115115

116116
[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);
117+
internal static partial void TransportSendingMessage(this ILogger logger, string endpointName, string messageId, string? json = null);
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);
@@ -347,4 +347,35 @@ public static partial void SSETransportPostNotAccepted(
347347
string endpointName,
348348
string messageId,
349349
string responseContent);
350+
351+
/// <summary>
352+
/// Logs the byte representation of a message in UTF-8 encoding.
353+
/// </summary>
354+
/// <param name="logger">The logger to use.</param>
355+
/// <param name="endpointName">The name of the endpoint.</param>
356+
/// <param name="byteRepresentation">The byte representation as a hex string.</param>
357+
[LoggerMessage(EventId = 39000, Level = LogLevel.Trace, Message = "Transport {EndpointName}: Message bytes (UTF-8): {ByteRepresentation}")]
358+
private static partial void TransportMessageBytes(this ILogger logger, string endpointName, string byteRepresentation);
359+
360+
/// <summary>
361+
/// Logs the byte representation of a message for diagnostic purposes.
362+
/// This is useful for diagnosing encoding issues with non-ASCII characters.
363+
/// </summary>
364+
/// <param name="logger">The logger to use.</param>
365+
/// <param name="endpointName">The name of the endpoint.</param>
366+
/// <param name="message">The message to log bytes for.</param>
367+
internal static void TransportMessageBytesUtf8(this ILogger logger, string endpointName, string message)
368+
{
369+
if (logger.IsEnabled(LogLevel.Trace))
370+
{
371+
var bytes = System.Text.Encoding.UTF8.GetBytes(message);
372+
var byteRepresentation =
373+
#if NET
374+
Convert.ToHexString(bytes);
375+
#else
376+
BitConverter.ToString(bytes).Replace("-", " ");
377+
#endif
378+
logger.TransportMessageBytes(endpointName, byteRepresentation);
379+
}
380+
}
350381
}

src/ModelContextProtocol/Logging/LoggerExtensions.cs

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

src/ModelContextProtocol/Protocol/Transport/StdioClientTransport.cs

Lines changed: 49 additions & 43 deletions
Original file line numberDiff line numberDiff line change
@@ -21,8 +21,6 @@ public sealed class StdioClientTransport : TransportBase, IClientTransport
2121
private readonly ILogger _logger;
2222
private readonly JsonSerializerOptions _jsonOptions;
2323
private Process? _process;
24-
private StreamWriter? _stdInWriter;
25-
private StreamReader? _stdOutReader;
2624
private Task? _readTask;
2725
private CancellationTokenSource? _shutdownCts;
2826
private bool _processStarted;
@@ -62,6 +60,8 @@ public async Task ConnectAsync(CancellationToken cancellationToken = default)
6260

6361
_shutdownCts = new CancellationTokenSource();
6462

63+
UTF8Encoding noBomUTF8 = new(encoderShouldEmitUTF8Identifier: false);
64+
6565
var startInfo = new ProcessStartInfo
6666
{
6767
FileName = _options.Command,
@@ -71,6 +71,11 @@ public async Task ConnectAsync(CancellationToken cancellationToken = default)
7171
UseShellExecute = false,
7272
CreateNoWindow = true,
7373
WorkingDirectory = _options.WorkingDirectory ?? Environment.CurrentDirectory,
74+
StandardOutputEncoding = noBomUTF8,
75+
StandardErrorEncoding = noBomUTF8,
76+
#if NET
77+
StandardInputEncoding = noBomUTF8,
78+
#endif
7479
};
7580

7681
if (!string.IsNullOrWhiteSpace(_options.Arguments))
@@ -95,19 +100,34 @@ public async Task ConnectAsync(CancellationToken cancellationToken = default)
95100
// Set up error logging
96101
_process.ErrorDataReceived += (sender, args) => _logger.TransportError(EndpointName, args.Data ?? "(no data)");
97102

98-
if (!_process.Start())
103+
// We need both stdin and stdout to use a no-BOM UTF-8 encoding. On .NET Core,
104+
// we can use ProcessStartInfo.StandardOutputEncoding/StandardInputEncoding, but
105+
// StandardInputEncoding doesn't exist on .NET Framework; instead, it always picks
106+
// up the encoding from Console.InputEncoding. As such, when not targeting .NET Core,
107+
// we temporarily change Console.InputEncoding to no-BOM UTF-8 around the Process.Start
108+
// call, to ensure it picks up the correct encoding.
109+
#if NET
110+
_processStarted = _process.Start();
111+
#else
112+
Encoding originalInputEncoding = Console.InputEncoding;
113+
try
114+
{
115+
Console.InputEncoding = noBomUTF8;
116+
_processStarted = _process.Start();
117+
}
118+
finally
119+
{
120+
Console.InputEncoding = originalInputEncoding;
121+
}
122+
#endif
123+
124+
if (!_processStarted)
99125
{
100126
_logger.TransportProcessStartFailed(EndpointName);
101127
throw new McpTransportException("Failed to start MCP server process");
102128
}
129+
103130
_logger.TransportProcessStarted(EndpointName, _process.Id);
104-
_processStarted = true;
105-
106-
// Create streams with explicit UTF-8 encoding to ensure proper Unicode character handling
107-
// This is especially important for non-ASCII characters like Chinese text and emoji
108-
var utf8Encoding = new UTF8Encoding(false); // No BOM
109-
_stdInWriter = new StreamWriter(_process.StandardInput.BaseStream, utf8Encoding) { AutoFlush = true };
110-
_stdOutReader = new StreamReader(_process.StandardOutput.BaseStream, utf8Encoding);
111131

112132
_process.BeginErrorReadLine();
113133

@@ -128,7 +148,7 @@ public async Task ConnectAsync(CancellationToken cancellationToken = default)
128148
/// <inheritdoc/>
129149
public override async Task SendMessageAsync(IJsonRpcMessage message, CancellationToken cancellationToken = default)
130150
{
131-
if (!IsConnected || _process?.HasExited == true || _stdInWriter == null)
151+
if (!IsConnected || _process?.HasExited == true)
132152
{
133153
_logger.TransportNotConnected(EndpointName);
134154
throw new McpTransportException("Transport is not connected");
@@ -147,8 +167,8 @@ public override async Task SendMessageAsync(IJsonRpcMessage message, Cancellatio
147167
_logger.TransportMessageBytesUtf8(EndpointName, json);
148168

149169
// Write the message followed by a newline using our UTF-8 writer
150-
await _stdInWriter.WriteLineAsync(json).ConfigureAwait(false);
151-
await _stdInWriter.FlushAsync(cancellationToken).ConfigureAwait(false);
170+
await _process!.StandardInput.WriteLineAsync(json).ConfigureAwait(false);
171+
await _process.StandardInput.FlushAsync(cancellationToken).ConfigureAwait(false);
152172

153173
_logger.TransportSentMessage(EndpointName, id);
154174
}
@@ -172,10 +192,10 @@ private async Task ReadMessagesAsync(CancellationToken cancellationToken)
172192
{
173193
_logger.TransportEnteringReadMessagesLoop(EndpointName);
174194

175-
while (!cancellationToken.IsCancellationRequested && !_process!.HasExited && _stdOutReader != null)
195+
while (!cancellationToken.IsCancellationRequested && !_process!.HasExited)
176196
{
177197
_logger.TransportWaitingForMessage(EndpointName);
178-
var line = await _stdOutReader.ReadLineAsync(cancellationToken).ConfigureAwait(false);
198+
var line = await _process.StandardOutput.ReadLineAsync(cancellationToken).ConfigureAwait(false);
179199
if (line == null)
180200
{
181201
_logger.TransportEndOfStream(EndpointName);
@@ -240,25 +260,8 @@ private async Task ProcessMessageAsync(string line, CancellationToken cancellati
240260
private async Task CleanupAsync(CancellationToken cancellationToken)
241261
{
242262
_logger.TransportCleaningUp(EndpointName);
243-
244-
if (_stdInWriter != null)
245-
{
246-
try
247-
{
248-
_logger.TransportClosingStdin(EndpointName);
249-
_stdInWriter.Close();
250-
}
251-
catch (Exception ex)
252-
{
253-
_logger.TransportShutdownFailed(EndpointName, ex);
254-
}
255263

256-
_stdInWriter = null;
257-
}
258-
259-
_stdOutReader = null;
260-
261-
if (_process != null && _processStarted && !_process.HasExited)
264+
if (_process is Process process && _processStarted && !process.HasExited)
262265
{
263266
try
264267
{
@@ -267,15 +270,17 @@ private async Task CleanupAsync(CancellationToken cancellationToken)
267270

268271
// Kill the while process tree because the process may spawn child processes
269272
// and Node.js does not kill its children when it exits properly
270-
_process.KillTree(_options.ShutdownTimeout);
273+
process.KillTree(_options.ShutdownTimeout);
271274
}
272275
catch (Exception ex)
273276
{
274277
_logger.TransportShutdownFailed(EndpointName, ex);
275278
}
276-
277-
_process.Dispose();
278-
_process = null;
279+
finally
280+
{
281+
process.Dispose();
282+
_process = null;
283+
}
279284
}
280285

281286
if (_shutdownCts is { } shutdownCts)
@@ -285,29 +290,30 @@ private async Task CleanupAsync(CancellationToken cancellationToken)
285290
_shutdownCts = null;
286291
}
287292

288-
if (_readTask != null)
293+
if (_readTask is Task readTask)
289294
{
290295
try
291296
{
292297
_logger.TransportWaitingForReadTask(EndpointName);
293-
await _readTask.WaitAsync(TimeSpan.FromSeconds(5), cancellationToken).ConfigureAwait(false);
298+
await readTask.WaitAsync(TimeSpan.FromSeconds(5), cancellationToken).ConfigureAwait(false);
294299
}
295300
catch (TimeoutException)
296301
{
297302
_logger.TransportCleanupReadTaskTimeout(EndpointName);
298-
// Continue with cleanup
299303
}
300304
catch (OperationCanceledException)
301305
{
302306
_logger.TransportCleanupReadTaskCancelled(EndpointName);
303-
// Ignore cancellation
304307
}
305308
catch (Exception ex)
306309
{
307310
_logger.TransportCleanupReadTaskFailed(EndpointName, ex);
308311
}
309-
_readTask = null;
310-
_logger.TransportReadTaskCleanedUp(EndpointName);
312+
finally
313+
{
314+
_logger.TransportReadTaskCleanedUp(EndpointName);
315+
_readTask = null;
316+
}
311317
}
312318

313319
SetConnected(false);

0 commit comments

Comments
 (0)