1
- using System . Diagnostics ;
2
- using System . Text . Json ;
1
+ using Microsoft . Extensions . Logging ;
2
+ using Microsoft . Extensions . Logging . Abstractions ;
3
3
using ModelContextProtocol . Configuration ;
4
4
using ModelContextProtocol . Logging ;
5
5
using ModelContextProtocol . Protocol . Messages ;
6
6
using ModelContextProtocol . Utils ;
7
7
using 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 ;
10
11
11
12
namespace ModelContextProtocol . Protocol . Transport ;
12
13
@@ -59,6 +60,8 @@ public async Task ConnectAsync(CancellationToken cancellationToken = default)
59
60
60
61
_shutdownCts = new CancellationTokenSource ( ) ;
61
62
63
+ UTF8Encoding noBomUTF8 = new ( encoderShouldEmitUTF8Identifier : false ) ;
64
+
62
65
var startInfo = new ProcessStartInfo
63
66
{
64
67
FileName = _options . Command ,
@@ -68,6 +71,11 @@ public async Task ConnectAsync(CancellationToken cancellationToken = default)
68
71
UseShellExecute = false ,
69
72
CreateNoWindow = true ,
70
73
WorkingDirectory = _options . WorkingDirectory ?? Environment . CurrentDirectory ,
74
+ StandardOutputEncoding = noBomUTF8 ,
75
+ StandardErrorEncoding = noBomUTF8 ,
76
+ #if NET
77
+ StandardInputEncoding = noBomUTF8 ,
78
+ #endif
71
79
} ;
72
80
73
81
if ( ! string . IsNullOrWhiteSpace ( _options . Arguments ) )
@@ -92,13 +100,35 @@ public async Task ConnectAsync(CancellationToken cancellationToken = default)
92
100
// Set up error logging
93
101
_process . ErrorDataReceived += ( sender , args ) => _logger . TransportError ( EndpointName , args . Data ?? "(no data)" ) ;
94
102
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 )
96
125
{
97
126
_logger . TransportProcessStartFailed ( EndpointName ) ;
98
127
throw new McpTransportException ( "Failed to start MCP server process" ) ;
99
128
}
129
+
100
130
_logger . TransportProcessStarted ( EndpointName , _process . Id ) ;
101
- _processStarted = true ;
131
+
102
132
_process . BeginErrorReadLine ( ) ;
103
133
104
134
// Start reading messages in the background
@@ -134,9 +164,10 @@ public override async Task SendMessageAsync(IJsonRpcMessage message, Cancellatio
134
164
{
135
165
var json = JsonSerializer . Serialize ( message , _jsonOptions . GetTypeInfo < IJsonRpcMessage > ( ) ) ;
136
166
_logger . TransportSendingMessage ( EndpointName , id , json ) ;
167
+ _logger . TransportMessageBytesUtf8 ( EndpointName , json ) ;
137
168
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 ) ;
140
171
await _process . StandardInput . FlushAsync ( cancellationToken ) . ConfigureAwait ( false ) ;
141
172
142
173
_logger . TransportSentMessage ( EndpointName , id ) ;
@@ -161,12 +192,10 @@ private async Task ReadMessagesAsync(CancellationToken cancellationToken)
161
192
{
162
193
_logger . TransportEnteringReadMessagesLoop ( EndpointName ) ;
163
194
164
- using var reader = _process ! . StandardOutput ;
165
-
166
- while ( ! cancellationToken . IsCancellationRequested && ! _process . HasExited )
195
+ while ( ! cancellationToken . IsCancellationRequested && ! _process ! . HasExited )
167
196
{
168
197
_logger . TransportWaitingForMessage ( EndpointName ) ;
169
- var line = await reader . ReadLineAsync ( cancellationToken ) . ConfigureAwait ( false ) ;
198
+ var line = await _process . StandardOutput . ReadLineAsync ( cancellationToken ) . ConfigureAwait ( false ) ;
170
199
if ( line == null )
171
200
{
172
201
_logger . TransportEndOfStream ( EndpointName ) ;
@@ -179,6 +208,7 @@ private async Task ReadMessagesAsync(CancellationToken cancellationToken)
179
208
}
180
209
181
210
_logger . TransportReceivedMessage ( EndpointName , line ) ;
211
+ _logger . TransportMessageBytesUtf8 ( EndpointName , line ) ;
182
212
183
213
await ProcessMessageAsync ( line , cancellationToken ) . ConfigureAwait ( false ) ;
184
214
}
@@ -230,28 +260,27 @@ private async Task ProcessMessageAsync(string line, CancellationToken cancellati
230
260
private async Task CleanupAsync ( CancellationToken cancellationToken )
231
261
{
232
262
_logger . TransportCleaningUp ( EndpointName ) ;
233
- if ( _process != null && _processStarted && ! _process . HasExited )
263
+
264
+ if ( _process is Process process && _processStarted && ! process . HasExited )
234
265
{
235
266
try
236
267
{
237
- // Try to close stdin to signal the process to exit
238
- _logger . TransportClosingStdin ( EndpointName ) ;
239
- _process . StandardInput . Close ( ) ;
240
-
241
268
// Wait for the process to exit
242
269
_logger . TransportWaitingForShutdown ( EndpointName ) ;
243
270
244
271
// Kill the while process tree because the process may spawn child processes
245
272
// and Node.js does not kill its children when it exits properly
246
- _process . KillTree ( _options . ShutdownTimeout ) ;
273
+ process . KillTree ( _options . ShutdownTimeout ) ;
247
274
}
248
275
catch ( Exception ex )
249
276
{
250
277
_logger . TransportShutdownFailed ( EndpointName , ex ) ;
251
278
}
252
-
253
- _process . Dispose ( ) ;
254
- _process = null ;
279
+ finally
280
+ {
281
+ process . Dispose ( ) ;
282
+ _process = null ;
283
+ }
255
284
}
256
285
257
286
if ( _shutdownCts is { } shutdownCts )
@@ -261,29 +290,30 @@ private async Task CleanupAsync(CancellationToken cancellationToken)
261
290
_shutdownCts = null ;
262
291
}
263
292
264
- if ( _readTask != null )
293
+ if ( _readTask is Task readTask )
265
294
{
266
295
try
267
296
{
268
297
_logger . TransportWaitingForReadTask ( EndpointName ) ;
269
- await _readTask . WaitAsync ( TimeSpan . FromSeconds ( 5 ) , cancellationToken ) . ConfigureAwait ( false ) ;
298
+ await readTask . WaitAsync ( TimeSpan . FromSeconds ( 5 ) , cancellationToken ) . ConfigureAwait ( false ) ;
270
299
}
271
300
catch ( TimeoutException )
272
301
{
273
302
_logger . TransportCleanupReadTaskTimeout ( EndpointName ) ;
274
- // Continue with cleanup
275
303
}
276
304
catch ( OperationCanceledException )
277
305
{
278
306
_logger . TransportCleanupReadTaskCancelled ( EndpointName ) ;
279
- // Ignore cancellation
280
307
}
281
308
catch ( Exception ex )
282
309
{
283
310
_logger . TransportCleanupReadTaskFailed ( EndpointName , ex ) ;
284
311
}
285
- _readTask = null ;
286
- _logger . TransportReadTaskCleanedUp ( EndpointName ) ;
312
+ finally
313
+ {
314
+ _logger . TransportReadTaskCleanedUp ( EndpointName ) ;
315
+ _readTask = null ;
316
+ }
287
317
}
288
318
289
319
SetConnected ( false ) ;
0 commit comments