1- using System . Diagnostics ;
2- using System . Text . Json ;
1+ using Microsoft . Extensions . Logging ;
2+ using Microsoft . Extensions . Logging . Abstractions ;
33using ModelContextProtocol . Configuration ;
44using ModelContextProtocol . Logging ;
55using ModelContextProtocol . Protocol . Messages ;
66using ModelContextProtocol . Utils ;
77using ModelContextProtocol . Utils . Json ;
8- using Microsoft . Extensions . Logging ;
9- using Microsoft . Extensions . Logging . Abstractions ;
8+ using System . Diagnostics ;
9+ using System . Text ;
10+ using System . Text . Json ;
1011
1112namespace ModelContextProtocol . Protocol . Transport ;
1213
@@ -59,6 +60,8 @@ public async Task ConnectAsync(CancellationToken cancellationToken = default)
5960
6061 _shutdownCts = new CancellationTokenSource ( ) ;
6162
63+ UTF8Encoding noBomUTF8 = new ( encoderShouldEmitUTF8Identifier : false ) ;
64+
6265 var startInfo = new ProcessStartInfo
6366 {
6467 FileName = _options . Command ,
@@ -68,6 +71,11 @@ public async Task ConnectAsync(CancellationToken cancellationToken = default)
6871 UseShellExecute = false ,
6972 CreateNoWindow = true ,
7073 WorkingDirectory = _options . WorkingDirectory ?? Environment . CurrentDirectory ,
74+ StandardOutputEncoding = noBomUTF8 ,
75+ StandardErrorEncoding = noBomUTF8 ,
76+ #if NET
77+ StandardInputEncoding = noBomUTF8 ,
78+ #endif
7179 } ;
7280
7381 if ( ! string . IsNullOrWhiteSpace ( _options . Arguments ) )
@@ -92,13 +100,35 @@ public async Task ConnectAsync(CancellationToken cancellationToken = default)
92100 // Set up error logging
93101 _process . ErrorDataReceived += ( sender , args ) => _logger . TransportError ( EndpointName , args . Data ?? "(no data)" ) ;
94102
95- 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 )
96125 {
97126 _logger . TransportProcessStartFailed ( EndpointName ) ;
98127 throw new McpTransportException ( "Failed to start MCP server process" ) ;
99128 }
129+
100130 _logger . TransportProcessStarted ( EndpointName , _process . Id ) ;
101- _processStarted = true ;
131+
102132 _process . BeginErrorReadLine ( ) ;
103133
104134 // Start reading messages in the background
@@ -134,9 +164,10 @@ public override async Task SendMessageAsync(IJsonRpcMessage message, Cancellatio
134164 {
135165 var json = JsonSerializer . Serialize ( message , _jsonOptions . GetTypeInfo < IJsonRpcMessage > ( ) ) ;
136166 _logger . TransportSendingMessage ( EndpointName , id , json ) ;
167+ _logger . TransportMessageBytesUtf8 ( EndpointName , json ) ;
137168
138- // Write the message followed by a newline
139- await _process ! . StandardInput . WriteLineAsync ( json . AsMemory ( ) , cancellationToken ) . ConfigureAwait ( false ) ;
169+ // Write the message followed by a newline using our UTF-8 writer
170+ await _process ! . StandardInput . WriteLineAsync ( json ) . ConfigureAwait ( false ) ;
140171 await _process . StandardInput . FlushAsync ( cancellationToken ) . ConfigureAwait ( false ) ;
141172
142173 _logger . TransportSentMessage ( EndpointName , id ) ;
@@ -161,12 +192,10 @@ private async Task ReadMessagesAsync(CancellationToken cancellationToken)
161192 {
162193 _logger . TransportEnteringReadMessagesLoop ( EndpointName ) ;
163194
164- using var reader = _process ! . StandardOutput ;
165-
166- while ( ! cancellationToken . IsCancellationRequested && ! _process . HasExited )
195+ while ( ! cancellationToken . IsCancellationRequested && ! _process ! . HasExited )
167196 {
168197 _logger . TransportWaitingForMessage ( EndpointName ) ;
169- var line = await reader . ReadLineAsync ( cancellationToken ) . ConfigureAwait ( false ) ;
198+ var line = await _process . StandardOutput . ReadLineAsync ( cancellationToken ) . ConfigureAwait ( false ) ;
170199 if ( line == null )
171200 {
172201 _logger . TransportEndOfStream ( EndpointName ) ;
@@ -179,6 +208,7 @@ private async Task ReadMessagesAsync(CancellationToken cancellationToken)
179208 }
180209
181210 _logger . TransportReceivedMessage ( EndpointName , line ) ;
211+ _logger . TransportMessageBytesUtf8 ( EndpointName , line ) ;
182212
183213 await ProcessMessageAsync ( line , cancellationToken ) . ConfigureAwait ( false ) ;
184214 }
@@ -230,28 +260,27 @@ private async Task ProcessMessageAsync(string line, CancellationToken cancellati
230260 private async Task CleanupAsync ( CancellationToken cancellationToken )
231261 {
232262 _logger . TransportCleaningUp ( EndpointName ) ;
233- if ( _process != null && _processStarted && ! _process . HasExited )
263+
264+ if ( _process is Process process && _processStarted && ! process . HasExited )
234265 {
235266 try
236267 {
237- // Try to close stdin to signal the process to exit
238- _logger . TransportClosingStdin ( EndpointName ) ;
239- _process . StandardInput . Close ( ) ;
240-
241268 // Wait for the process to exit
242269 _logger . TransportWaitingForShutdown ( EndpointName ) ;
243270
244271 // Kill the while process tree because the process may spawn child processes
245272 // and Node.js does not kill its children when it exits properly
246- _process . KillTree ( _options . ShutdownTimeout ) ;
273+ process . KillTree ( _options . ShutdownTimeout ) ;
247274 }
248275 catch ( Exception ex )
249276 {
250277 _logger . TransportShutdownFailed ( EndpointName , ex ) ;
251278 }
252-
253- _process . Dispose ( ) ;
254- _process = null ;
279+ finally
280+ {
281+ process . Dispose ( ) ;
282+ _process = null ;
283+ }
255284 }
256285
257286 if ( _shutdownCts is { } shutdownCts )
@@ -261,29 +290,30 @@ private async Task CleanupAsync(CancellationToken cancellationToken)
261290 _shutdownCts = null ;
262291 }
263292
264- if ( _readTask != null )
293+ if ( _readTask is Task readTask )
265294 {
266295 try
267296 {
268297 _logger . TransportWaitingForReadTask ( EndpointName ) ;
269- await _readTask . WaitAsync ( TimeSpan . FromSeconds ( 5 ) , cancellationToken ) . ConfigureAwait ( false ) ;
298+ await readTask . WaitAsync ( TimeSpan . FromSeconds ( 5 ) , cancellationToken ) . ConfigureAwait ( false ) ;
270299 }
271300 catch ( TimeoutException )
272301 {
273302 _logger . TransportCleanupReadTaskTimeout ( EndpointName ) ;
274- // Continue with cleanup
275303 }
276304 catch ( OperationCanceledException )
277305 {
278306 _logger . TransportCleanupReadTaskCancelled ( EndpointName ) ;
279- // Ignore cancellation
280307 }
281308 catch ( Exception ex )
282309 {
283310 _logger . TransportCleanupReadTaskFailed ( EndpointName , ex ) ;
284311 }
285- _readTask = null ;
286- _logger . TransportReadTaskCleanedUp ( EndpointName ) ;
312+ finally
313+ {
314+ _logger . TransportReadTaskCleanedUp ( EndpointName ) ;
315+ _readTask = null ;
316+ }
287317 }
288318
289319 SetConnected ( false ) ;
0 commit comments