-
Notifications
You must be signed in to change notification settings - Fork 672
Expand file tree
/
Copy pathStdioClientSessionTransport.cs
More file actions
190 lines (167 loc) · 7.2 KB
/
StdioClientSessionTransport.cs
File metadata and controls
190 lines (167 loc) · 7.2 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
using Microsoft.Extensions.Logging;
using ModelContextProtocol.Protocol;
using System.Diagnostics;
namespace ModelContextProtocol.Client;
/// <summary>Provides the client side of a stdio-based session transport.</summary>
internal sealed class StdioClientSessionTransport : StreamClientSessionTransport
{
private readonly StdioClientTransportOptions _options;
private readonly Process _process;
private readonly Queue<string> _stderrRollingLog;
private readonly DataReceivedEventHandler _errorHandler;
private int _cleanedUp = 0;
private readonly int? _processId;
public StdioClientSessionTransport(StdioClientTransportOptions options, Process process, string endpointName, Queue<string> stderrRollingLog, DataReceivedEventHandler errorHandler, ILoggerFactory? loggerFactory) :
base(process.StandardInput.BaseStream, process.StandardOutput.BaseStream, encoding: null, endpointName, loggerFactory)
{
_options = options;
_process = process;
_stderrRollingLog = stderrRollingLog;
_errorHandler = errorHandler;
try { _processId = process.Id; } catch { }
}
/// <inheritdoc/>
public override async Task SendMessageAsync(JsonRpcMessage message, CancellationToken cancellationToken = default)
{
try
{
await base.SendMessageAsync(message, cancellationToken).ConfigureAwait(false);
}
catch (IOException)
{
// We failed to send due to an I/O error. If the server process has exited, which is then very likely the cause
// for the I/O error, we should throw an exception for that instead.
if (await GetUnexpectedExitExceptionAsync().ConfigureAwait(false) is Exception processExitException)
{
throw processExitException;
}
throw;
}
}
/// <inheritdoc/>
protected override async ValueTask CleanupAsync(Exception? error = null, CancellationToken cancellationToken = default)
{
// Only run the full stdio cleanup once (handler detach, process kill, etc.).
// If another call is already handling cleanup, cancel the shutdown token
// to unblock it (e.g. if it's stuck in WaitForExitAsync) and let it
// call SetDisconnected with full StdioClientCompletionDetails.
if (Interlocked.Exchange(ref _cleanedUp, 1) != 0)
{
CancelShutdown();
return;
}
// We've not yet forcefully terminated the server. If it's already shut down, something went wrong,
// so create an exception with details about that.
error ??= await GetUnexpectedExitExceptionAsync().ConfigureAwait(false);
// Ensure all pending ErrorDataReceived events are drained before detaching
// the handler. GetUnexpectedExitExceptionAsync does this when HasExited is
// true, but there is a narrow window on Linux where the process has closed
// stdout (causing EOF in ReadMessagesAsync) yet hasn't been fully reaped,
// so HasExited returns false and the drain is skipped. An unconditional
// wait here covers that gap. When the drain already happened above, the
// call returns immediately.
await WaitForProcessExitAsync().ConfigureAwait(false);
// Detach the stderr handler so no further ErrorDataReceived events
// are dispatched during or after process disposal.
_process.ErrorDataReceived -= _errorHandler;
// Terminate the server process (or confirm it already exited), then build
// and publish strongly-typed completion details while the process handle
// is still valid so we can read the exit code.
try
{
StdioClientTransport.DisposeProcess(
_process,
processRunning: true,
_options.ShutdownTimeout,
beforeDispose: () => SetDisconnected(new ClientTransportClosedException(BuildCompletionDetails(error))));
}
catch (Exception ex)
{
LogTransportShutdownFailed(Name, ex);
SetDisconnected(new ClientTransportClosedException(BuildCompletionDetails(error)));
}
// And handle cleanup in the base type. SetDisconnected has already been
// called above, so the base call is a no-op for disconnect state but
// still performs other cleanup (cancelling the read task, etc.).
await base.CleanupAsync(error, cancellationToken).ConfigureAwait(false);
}
private async ValueTask<Exception?> GetUnexpectedExitExceptionAsync()
{
if (!StdioClientTransport.HasExited(_process))
{
return null;
}
Debug.Assert(StdioClientTransport.HasExited(_process));
await WaitForProcessExitAsync().ConfigureAwait(false);
string errorMessage = "MCP server process exited unexpectedly";
string? exitCode = null;
try
{
exitCode = $" (exit code: {(uint)_process.ExitCode})";
}
catch { }
lock (_stderrRollingLog)
{
if (_stderrRollingLog.Count > 0)
{
errorMessage =
$"{errorMessage}{exitCode}{Environment.NewLine}" +
$"Server's stderr tail:{Environment.NewLine}" +
$"{string.Join(Environment.NewLine, _stderrRollingLog)}";
}
}
return new IOException(errorMessage);
}
/// <summary>
/// Waits for the process to exit within <see cref="StdioClientTransportOptions.ShutdownTimeout"/>
/// and flushes pending <see cref="Process.ErrorDataReceived"/> events.
/// </summary>
/// <remarks>
/// On .NET, <c>Process.WaitForExitAsync</c> also waits for asynchronous output readers
/// to complete, ensuring all events have been dispatched. On .NET Framework,
/// <see cref="Process.WaitForExit(int)"/> does not guarantee this; the parameterless
/// <see cref="Process.WaitForExit()"/> overload is needed to flush the event queue.
/// This method is idempotent—calling it after the process has already been waited on
/// returns immediately.
/// </remarks>
private async ValueTask WaitForProcessExitAsync()
{
try
{
#if NET
using var timeoutCts = new CancellationTokenSource(_options.ShutdownTimeout);
await _process.WaitForExitAsync(timeoutCts.Token).ConfigureAwait(false);
#else
if (_process.WaitForExit((int)_options.ShutdownTimeout.TotalMilliseconds))
{
_process.WaitForExit();
}
#endif
}
catch { }
}
private StdioClientCompletionDetails BuildCompletionDetails(Exception? error)
{
StdioClientCompletionDetails details = new()
{
Exception = error,
ProcessId = _processId,
};
try
{
if (StdioClientTransport.HasExited(_process))
{
details.ExitCode = _process.ExitCode;
}
}
catch { }
lock (_stderrRollingLog)
{
if (_stderrRollingLog.Count > 0)
{
details.StandardErrorTail = _stderrRollingLog.ToArray();
}
}
return details;
}
}