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