using Microsoft.Extensions.Logging; using ModelContextProtocol.Protocol; using System.Diagnostics; namespace ModelContextProtocol.Client; /// Provides the client side of a stdio-based session transport. internal sealed class StdioClientSessionTransport : StreamClientSessionTransport { private readonly StdioClientTransportOptions _options; private readonly Process _process; private readonly Queue _stderrRollingLog; private int _cleanedUp = 0; private readonly int? _processId; public StdioClientSessionTransport(StdioClientTransportOptions options, Process process, string endpointName, Queue stderrRollingLog, ILoggerFactory? loggerFactory) : base(process.StandardInput.BaseStream, process.StandardOutput.BaseStream, encoding: null, endpointName, loggerFactory) { _options = options; _process = process; _stderrRollingLog = stderrRollingLog; try { _processId = process.Id; } catch { } } /// 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(cancellationToken).ConfigureAwait(false) is Exception processExitException) { throw processExitException; } throw; } } /// protected override async ValueTask CleanupAsync(Exception? error = null, CancellationToken cancellationToken = default) { // Only clean up once. if (Interlocked.Exchange(ref _cleanedUp, 1) != 0) { 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(cancellationToken).ConfigureAwait(false); // 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 GetUnexpectedExitExceptionAsync(CancellationToken cancellationToken) { if (!StdioClientTransport.HasExited(_process)) { return null; } Debug.Assert(StdioClientTransport.HasExited(_process)); try { // The process has exited, but we still need to ensure stderr has been flushed. // WaitForExitAsync only waits for exit; it does not guarantee that all // ErrorDataReceived events have been dispatched. The synchronous WaitForExit() // (no arguments) does ensure that, so call it after WaitForExitAsync completes. #if NET await _process.WaitForExitAsync(cancellationToken).ConfigureAwait(false); #endif _process.WaitForExit(); } catch { } 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); } 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; } }