@@ -21,8 +21,6 @@ public sealed class StdioClientTransport : TransportBase, IClientTransport
21
21
private readonly ILogger _logger ;
22
22
private readonly JsonSerializerOptions _jsonOptions ;
23
23
private Process ? _process ;
24
- private StreamWriter ? _stdInWriter ;
25
- private StreamReader ? _stdOutReader ;
26
24
private Task ? _readTask ;
27
25
private CancellationTokenSource ? _shutdownCts ;
28
26
private bool _processStarted ;
@@ -62,6 +60,8 @@ public async Task ConnectAsync(CancellationToken cancellationToken = default)
62
60
63
61
_shutdownCts = new CancellationTokenSource ( ) ;
64
62
63
+ UTF8Encoding noBomUTF8 = new ( encoderShouldEmitUTF8Identifier : false ) ;
64
+
65
65
var startInfo = new ProcessStartInfo
66
66
{
67
67
FileName = _options . Command ,
@@ -71,6 +71,11 @@ public async Task ConnectAsync(CancellationToken cancellationToken = default)
71
71
UseShellExecute = false ,
72
72
CreateNoWindow = true ,
73
73
WorkingDirectory = _options . WorkingDirectory ?? Environment . CurrentDirectory ,
74
+ StandardOutputEncoding = noBomUTF8 ,
75
+ StandardErrorEncoding = noBomUTF8 ,
76
+ #if NET
77
+ StandardInputEncoding = noBomUTF8 ,
78
+ #endif
74
79
} ;
75
80
76
81
if ( ! string . IsNullOrWhiteSpace ( _options . Arguments ) )
@@ -95,19 +100,34 @@ public async Task ConnectAsync(CancellationToken cancellationToken = default)
95
100
// Set up error logging
96
101
_process . ErrorDataReceived += ( sender , args ) => _logger . TransportError ( EndpointName , args . Data ?? "(no data)" ) ;
97
102
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 )
99
125
{
100
126
_logger . TransportProcessStartFailed ( EndpointName ) ;
101
127
throw new McpTransportException ( "Failed to start MCP server process" ) ;
102
128
}
129
+
103
130
_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 ) ;
111
131
112
132
_process . BeginErrorReadLine ( ) ;
113
133
@@ -128,7 +148,7 @@ public async Task ConnectAsync(CancellationToken cancellationToken = default)
128
148
/// <inheritdoc/>
129
149
public override async Task SendMessageAsync ( IJsonRpcMessage message , CancellationToken cancellationToken = default )
130
150
{
131
- if ( ! IsConnected || _process ? . HasExited == true || _stdInWriter == null )
151
+ if ( ! IsConnected || _process ? . HasExited == true )
132
152
{
133
153
_logger . TransportNotConnected ( EndpointName ) ;
134
154
throw new McpTransportException ( "Transport is not connected" ) ;
@@ -147,8 +167,8 @@ public override async Task SendMessageAsync(IJsonRpcMessage message, Cancellatio
147
167
_logger . TransportMessageBytesUtf8 ( EndpointName , json ) ;
148
168
149
169
// 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 ) ;
152
172
153
173
_logger . TransportSentMessage ( EndpointName , id ) ;
154
174
}
@@ -172,10 +192,10 @@ private async Task ReadMessagesAsync(CancellationToken cancellationToken)
172
192
{
173
193
_logger . TransportEnteringReadMessagesLoop ( EndpointName ) ;
174
194
175
- while ( ! cancellationToken . IsCancellationRequested && ! _process ! . HasExited && _stdOutReader != null )
195
+ while ( ! cancellationToken . IsCancellationRequested && ! _process ! . HasExited )
176
196
{
177
197
_logger . TransportWaitingForMessage ( EndpointName ) ;
178
- var line = await _stdOutReader . ReadLineAsync ( cancellationToken ) . ConfigureAwait ( false ) ;
198
+ var line = await _process . StandardOutput . ReadLineAsync ( cancellationToken ) . ConfigureAwait ( false ) ;
179
199
if ( line == null )
180
200
{
181
201
_logger . TransportEndOfStream ( EndpointName ) ;
@@ -240,25 +260,8 @@ private async Task ProcessMessageAsync(string line, CancellationToken cancellati
240
260
private async Task CleanupAsync ( CancellationToken cancellationToken )
241
261
{
242
262
_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
- }
255
263
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 )
262
265
{
263
266
try
264
267
{
@@ -267,15 +270,17 @@ private async Task CleanupAsync(CancellationToken cancellationToken)
267
270
268
271
// Kill the while process tree because the process may spawn child processes
269
272
// and Node.js does not kill its children when it exits properly
270
- _process . KillTree ( _options . ShutdownTimeout ) ;
273
+ process . KillTree ( _options . ShutdownTimeout ) ;
271
274
}
272
275
catch ( Exception ex )
273
276
{
274
277
_logger . TransportShutdownFailed ( EndpointName , ex ) ;
275
278
}
276
-
277
- _process . Dispose ( ) ;
278
- _process = null ;
279
+ finally
280
+ {
281
+ process . Dispose ( ) ;
282
+ _process = null ;
283
+ }
279
284
}
280
285
281
286
if ( _shutdownCts is { } shutdownCts )
@@ -285,29 +290,30 @@ private async Task CleanupAsync(CancellationToken cancellationToken)
285
290
_shutdownCts = null ;
286
291
}
287
292
288
- if ( _readTask != null )
293
+ if ( _readTask is Task readTask )
289
294
{
290
295
try
291
296
{
292
297
_logger . TransportWaitingForReadTask ( EndpointName ) ;
293
- await _readTask . WaitAsync ( TimeSpan . FromSeconds ( 5 ) , cancellationToken ) . ConfigureAwait ( false ) ;
298
+ await readTask . WaitAsync ( TimeSpan . FromSeconds ( 5 ) , cancellationToken ) . ConfigureAwait ( false ) ;
294
299
}
295
300
catch ( TimeoutException )
296
301
{
297
302
_logger . TransportCleanupReadTaskTimeout ( EndpointName ) ;
298
- // Continue with cleanup
299
303
}
300
304
catch ( OperationCanceledException )
301
305
{
302
306
_logger . TransportCleanupReadTaskCancelled ( EndpointName ) ;
303
- // Ignore cancellation
304
307
}
305
308
catch ( Exception ex )
306
309
{
307
310
_logger . TransportCleanupReadTaskFailed ( EndpointName , ex ) ;
308
311
}
309
- _readTask = null ;
310
- _logger . TransportReadTaskCleanedUp ( EndpointName ) ;
312
+ finally
313
+ {
314
+ _logger . TransportReadTaskCleanedUp ( EndpointName ) ;
315
+ _readTask = null ;
316
+ }
311
317
}
312
318
313
319
SetConnected ( false ) ;
0 commit comments