Improve exception diagnostics for stdio client (#376)

* Improve exception diagnostics for stdio client

Track the last several lines of stderr and include those in an exception created as part of CleanupAsync if the server process has already exited. This is based on an assumption that the server should never exit prior to CleanupAsync being called.

* Address feedback and fix test failure
This commit is contained in:
Stephen Toub 2025-05-14 10:46:49 -04:00 • committed by GitHub
commit c11c634945
No known key found for this signature in database
GPG key ID: B5690EEEBB952194

View file

@ -127,8 +127,28 @@ public sealed partial class StdioClientTransport : IClientTransport
process = new() { StartInfo = startInfo };
// Set up error logging
process.ErrorDataReceived += (sender, args) => LogReadStderr(logger, endpointName, args.Data ?? "(no data)");
// Set up stderr handling. Log all stderr output, and keep the last
// few lines in a rolling log for use in exceptions.
const int MaxStderrLength = 10; // keep the last 10 lines of stderr
Queue<string> stderrRollingLog = new(MaxStderrLength);
process.ErrorDataReceived += (sender, args) =>
{
string? data = args.Data;
if (data is not null)
{
lock (stderrRollingLog)
{
if (stderrRollingLog.Count >= MaxStderrLength)
{
stderrRollingLog.Dequeue();
}
stderrRollingLog.Enqueue(data);
}
LogReadStderr(logger, endpointName, data);
}
};
// We need both stdin and stdout to use a no-BOM UTF-8 encoding. On .NET Core,
// we can use ProcessStartInfo.StandardOutputEncoding/StandardInputEncoding, but
@ -154,14 +174,14 @@ public sealed partial class StdioClientTransport : IClientTransport
if (!processStarted)
{
LogTransportProcessStartFailed(logger, endpointName);
throw new InvalidOperationException("Failed to start MCP server process");
throw new IOException("Failed to start MCP server process.");
}
LogTransportProcessStarted(logger, endpointName, process.Id);
process.BeginErrorReadLine();
return new StdioClientSessionTransport(_options, process, endpointName, _loggerFactory);
return new StdioClientSessionTransport(_options, process, endpointName, stderrRollingLog, _loggerFactory);
}
catch (Exception ex)
{
@ -176,7 +196,7 @@ public sealed partial class StdioClientTransport : IClientTransport
LogTransportShutdownFailed(logger, endpointName, ex2);
}
throw new InvalidOperationException("Failed to connect transport", ex);
throw new IOException("Failed to connect transport.", ex);
}
}
@ -185,20 +205,9 @@ public sealed partial class StdioClientTransport : IClientTransport
{
if (process is not null)
{
if (processRunning)
{
try
{
processRunning = !process.HasExited;
}
catch
{
processRunning = false;
}
}
try
{
processRunning = processRunning && !HasExited(process);
if (processRunning)
{
// Wait for the process to exit.
@ -214,6 +223,19 @@ public sealed partial class StdioClientTransport : IClientTransport
}
}
/// <summary>Gets whether <paramref name="process"/> has exited.</summary>
internal static bool HasExited(Process process)
{
try
{
return process.HasExited;
}
catch
{
return true;
}
}
[LoggerMessage(Level = LogLevel.Information, Message = "{EndpointName} connecting.")]
private static partial void LogTransportConnecting(ILogger logger, string endpointName);