From b9b4a230054bdabbb889a22599f1a94a38349265 Mon Sep 17 00:00:00 2001 From: Stephen Toub Date: Tue, 17 Jun 2025 16:03:20 -0400 Subject: [PATCH] Add a callback for receiving stderr output (#525) * Add a callback for receiving stderr output * Add asserts to test --- .../Client/StdioClientTransport.cs | 2 ++ .../Client/StdioClientTransportOptions.cs | 5 ++++ .../Transport/StdioClientTransportTests.cs | 28 +++++++++++++++++++ 3 files changed, 35 insertions(+) diff --git a/src/ModelContextProtocol.Core/Client/StdioClientTransport.cs b/src/ModelContextProtocol.Core/Client/StdioClientTransport.cs index 5a5d10f..e00ddba 100644 --- a/src/ModelContextProtocol.Core/Client/StdioClientTransport.cs +++ b/src/ModelContextProtocol.Core/Client/StdioClientTransport.cs @@ -146,6 +146,8 @@ public sealed partial class StdioClientTransport : IClientTransport stderrRollingLog.Enqueue(data); } + _options.StandardErrorLines?.Invoke(data); + LogReadStderr(logger, endpointName, data); } }; diff --git a/src/ModelContextProtocol.Core/Client/StdioClientTransportOptions.cs b/src/ModelContextProtocol.Core/Client/StdioClientTransportOptions.cs index 6506022..5930e9d 100644 --- a/src/ModelContextProtocol.Core/Client/StdioClientTransportOptions.cs +++ b/src/ModelContextProtocol.Core/Client/StdioClientTransportOptions.cs @@ -69,4 +69,9 @@ public sealed class StdioClientTransportOptions /// /// public TimeSpan ShutdownTimeout { get; set; } = TimeSpan.FromSeconds(5); + + /// + /// Gets or sets a callback that is invoked for each line of stderr received from the server process. + /// + public Action? StandardErrorLines { get; set; } } diff --git a/tests/ModelContextProtocol.Tests/Transport/StdioClientTransportTests.cs b/tests/ModelContextProtocol.Tests/Transport/StdioClientTransportTests.cs index b8d8d71..40602a9 100644 --- a/tests/ModelContextProtocol.Tests/Transport/StdioClientTransportTests.cs +++ b/tests/ModelContextProtocol.Tests/Transport/StdioClientTransportTests.cs @@ -1,6 +1,7 @@ using ModelContextProtocol.Client; using ModelContextProtocol.Tests.Utils; using System.Runtime.InteropServices; +using System.Text; namespace ModelContextProtocol.Tests.Transport; @@ -18,4 +19,31 @@ public class StdioClientTransportTests(ITestOutputHelper testOutputHelper) : Log IOException e = await Assert.ThrowsAsync(() => McpClientFactory.CreateAsync(transport, loggerFactory: LoggerFactory, cancellationToken: TestContext.Current.CancellationToken)); Assert.Contains(id, e.ToString()); } + + [Fact] + public async Task CreateAsync_ValidProcessInvalidServer_StdErrCallbackInvoked() + { + string id = Guid.NewGuid().ToString("N"); + + int count = 0; + StringBuilder sb = new(); + Action stdErrCallback = line => + { + Assert.NotNull(line); + lock (sb) + { + sb.AppendLine(line); + count++; + } + }; + + StdioClientTransport transport = RuntimeInformation.IsOSPlatform(OSPlatform.Windows) ? + new(new() { Command = "cmd", Arguments = ["/C", $"echo \"{id}\" >&2"], StandardErrorLines = stdErrCallback }, LoggerFactory) : + new(new() { Command = "ls", Arguments = [id], StandardErrorLines = stdErrCallback }, LoggerFactory); + + await Assert.ThrowsAsync(() => McpClientFactory.CreateAsync(transport, loggerFactory: LoggerFactory, cancellationToken: TestContext.Current.CancellationToken)); + + Assert.InRange(count, 1, int.MaxValue); + Assert.Contains(id, sb.ToString()); + } }