@@ -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