From 9b0421642c1daa5fcde19b9e21afa3e884080a20 Mon Sep 17 00:00:00 2001 From: Stephen Halter Date: Mon, 31 Mar 2025 15:45:20 -0700 Subject: [PATCH 1/4] Configure hosted stdio servers to log to stderr --- README.md | 14 ++++++++++++-- tests/ModelContextProtocol.TestServer/Program.cs | 4 ++++ 2 files changed, 16 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index 76cae4ee1..a7290beaa 100644 --- a/README.md +++ b/README.md @@ -83,13 +83,24 @@ It includes a simple echo tool as an example (this is included in the same file the employed overload of `WithTools` examines the current assembly for classes with the `McpServerToolType` attribute, and registers all methods with the `McpTool` attribute as tools.) +``` +dotnet add package ModelContextProtocol --prerelease +dotnet add package Microsoft.Extensions.Hosting +``` + ```csharp using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.Hosting; +using Microsoft.Extensions.Logging; using ModelContextProtocol.Server; using System.ComponentModel; var builder = Host.CreateEmptyApplicationBuilder(settings: null); +builder.Logging.AddConsole(consoleLogOptions => +{ + // Configure all logs to go to stderr + consoleLogOptions.LogToStandardErrorThreshold = LogLevel.Trace; +}); builder.Services .AddMcpServer() .WithStdioServerTransport() @@ -101,8 +112,7 @@ public static class EchoTool { [McpServerTool, Description("Echoes the message back to the client.")] public static string Echo(string message) => $"hello {message}"; -} -``` +}``` Tools can have the `IMcpServer` representing the server injected via a parameter to the method, and can use that for interaction with the connected client. Similarly, arguments may be injected via dependency injection. For example, this tool will use the supplied diff --git a/tests/ModelContextProtocol.TestServer/Program.cs b/tests/ModelContextProtocol.TestServer/Program.cs index 312013fa5..8c703601f 100644 --- a/tests/ModelContextProtocol.TestServer/Program.cs +++ b/tests/ModelContextProtocol.TestServer/Program.cs @@ -24,6 +24,10 @@ private static ILoggerFactory CreateLoggerFactory() return LoggerFactory.Create(builder => { + builder.AddConsole(options => + { + options.LogToStandardErrorThreshold = LogLevel.Trace; + }); builder.AddSerilog(); }); } From d38cea98c90b0170d53f3b69b6d3af22b4a4d853 Mon Sep 17 00:00:00 2001 From: Stephen Halter Date: Mon, 31 Mar 2025 15:47:52 -0700 Subject: [PATCH 2/4] Fix newline --- README.md | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/README.md b/README.md index a7290beaa..7423f3ad7 100644 --- a/README.md +++ b/README.md @@ -112,7 +112,8 @@ public static class EchoTool { [McpServerTool, Description("Echoes the message back to the client.")] public static string Echo(string message) => $"hello {message}"; -}``` +} +``` Tools can have the `IMcpServer` representing the server injected via a parameter to the method, and can use that for interaction with the connected client. Similarly, arguments may be injected via dependency injection. For example, this tool will use the supplied From b1c8629fc326202cf4a72e56213488eaf620b4ed Mon Sep 17 00:00:00 2001 From: Stephen Halter Date: Mon, 31 Mar 2025 19:08:57 -0700 Subject: [PATCH 3/4] Use CreateApplicationBuilder where possible --- README.md | 2 +- samples/QuickstartClient/Program.cs | 2 +- samples/QuickstartWeatherServer/Program.cs | 8 +++++++- 3 files changed, 9 insertions(+), 3 deletions(-) diff --git a/README.md b/README.md index 7423f3ad7..364354bcf 100644 --- a/README.md +++ b/README.md @@ -95,7 +95,7 @@ using Microsoft.Extensions.Logging; using ModelContextProtocol.Server; using System.ComponentModel; -var builder = Host.CreateEmptyApplicationBuilder(settings: null); +var builder = Host.CreateApplicationBuilder(args); builder.Logging.AddConsole(consoleLogOptions => { // Configure all logs to go to stderr diff --git a/samples/QuickstartClient/Program.cs b/samples/QuickstartClient/Program.cs index 364c2b870..1ecd40c25 100644 --- a/samples/QuickstartClient/Program.cs +++ b/samples/QuickstartClient/Program.cs @@ -5,7 +5,7 @@ using ModelContextProtocol.Client; using ModelContextProtocol.Protocol.Transport; -var builder = Host.CreateEmptyApplicationBuilder(settings: null); +var builder = Host.CreateApplicationBuilder(args); builder.Configuration .AddEnvironmentVariables() diff --git a/samples/QuickstartWeatherServer/Program.cs b/samples/QuickstartWeatherServer/Program.cs index a191cb163..8c3b38420 100644 --- a/samples/QuickstartWeatherServer/Program.cs +++ b/samples/QuickstartWeatherServer/Program.cs @@ -1,13 +1,19 @@ using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.Hosting; +using Microsoft.Extensions.Logging; using System.Net.Http.Headers; -var builder = Host.CreateEmptyApplicationBuilder(settings: null); +var builder = Host.CreateApplicationBuilder(args); builder.Services.AddMcpServer() .WithStdioServerTransport() .WithToolsFromAssembly(); +builder.Logging.AddConsole(options => +{ + options.LogToStandardErrorThreshold = LogLevel.Trace; +}); + builder.Services.AddSingleton(_ => { var client = new HttpClient() { BaseAddress = new Uri("https://api.weather.gov") }; From 2d30f197669e296afcb964e6c5aa457bf8fae50e Mon Sep 17 00:00:00 2001 From: Stephen Halter Date: Mon, 31 Mar 2025 19:10:10 -0700 Subject: [PATCH 4/4] Address PR feedback --- src/ModelContextProtocol/Logging/Log.cs | 4 ++-- .../Protocol/Transport/StdioClientTransport.cs | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/src/ModelContextProtocol/Logging/Log.cs b/src/ModelContextProtocol/Logging/Log.cs index d22c5d664..aa9b20949 100644 --- a/src/ModelContextProtocol/Logging/Log.cs +++ b/src/ModelContextProtocol/Logging/Log.cs @@ -98,8 +98,8 @@ internal static partial class Log [LoggerMessage(Level = LogLevel.Information, Message = "Creating process for transport for {endpointName} with command {command}, arguments {arguments}, environment {environment}, working directory {workingDirectory}, shutdown timeout {shutdownTimeout}")] internal static partial void CreateProcessForTransport(this ILogger logger, string endpointName, string command, string? arguments, string environment, string workingDirectory, string shutdownTimeout); - [LoggerMessage(Level = LogLevel.Error, Message = "Transport for {endpointName} error: {data}")] - internal static partial void TransportError(this ILogger logger, string endpointName, string data); + [LoggerMessage(Level = LogLevel.Information, Message = "Transport for {endpointName} received stderr log: {data}")] + internal static partial void ReadStderr(this ILogger logger, string endpointName, string data); [LoggerMessage(Level = LogLevel.Information, Message = "Transport process start failed for {endpointName}")] internal static partial void TransportProcessStartFailed(this ILogger logger, string endpointName); diff --git a/src/ModelContextProtocol/Protocol/Transport/StdioClientTransport.cs b/src/ModelContextProtocol/Protocol/Transport/StdioClientTransport.cs index 0fca07e27..774677f13 100644 --- a/src/ModelContextProtocol/Protocol/Transport/StdioClientTransport.cs +++ b/src/ModelContextProtocol/Protocol/Transport/StdioClientTransport.cs @@ -85,7 +85,7 @@ public async Task ConnectAsync(CancellationToken cancellationToken = process = new() { StartInfo = startInfo }; // Set up error logging - process.ErrorDataReceived += (sender, args) => logger.TransportError(endpointName, args.Data ?? "(no data)"); + process.ErrorDataReceived += (sender, args) => logger.ReadStderr(endpointName, args.Data ?? "(no 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