From d661159baa42d05d92acaf37b9ef5f6d569d1c83 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Wed, 28 Jan 2026 22:34:32 +0000 Subject: [PATCH 1/8] Initial plan From 7cd6a014f75e6f3604bdea84df7d162313323d8e Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Wed, 28 Jan 2026 22:43:56 +0000 Subject: [PATCH 2/8] Add Trace-level logging for JSON-RPC payloads in transports - Add LogTransportSendingMessage and LogTransportSendingMessageSensitive to TransportBase - Update StreamServerTransport to log sent messages at Trace level - Update StreamClientSessionTransport to log sent messages at Trace level - Update SseClientSessionTransport to log sent messages at Trace level - Update StreamableHttpClientSessionTransport to log sent messages at Trace level - Add tests for Trace-level logging of sent and received messages Co-authored-by: stephentoub <2642209+stephentoub@users.noreply.github.com> --- .../Client/SseClientSessionTransport.cs | 9 +++ .../Client/StreamClientSessionTransport.cs | 9 +++ .../StreamableHttpClientSessionTransport.cs | 15 ++++ .../Protocol/TransportBase.cs | 6 ++ .../Server/StreamServerTransport.cs | 12 +++- .../Transport/StdioServerTransportTests.cs | 71 ++++++++++++++++++- 6 files changed, 120 insertions(+), 2 deletions(-) diff --git a/src/ModelContextProtocol.Core/Client/SseClientSessionTransport.cs b/src/ModelContextProtocol.Core/Client/SseClientSessionTransport.cs index 60950dfa5..93aec40e1 100644 --- a/src/ModelContextProtocol.Core/Client/SseClientSessionTransport.cs +++ b/src/ModelContextProtocol.Core/Client/SseClientSessionTransport.cs @@ -80,6 +80,15 @@ public override async Task SendMessageAsync( messageId = messageWithId.Id.ToString(); } + if (_logger.IsEnabled(LogLevel.Trace)) + { + LogTransportSendingMessageSensitive(Name, JsonSerializer.Serialize(message, McpJsonUtilities.JsonContext.Default.JsonRpcMessage)); + } + else + { + LogTransportSendingMessage(Name, messageId); + } + using var httpRequestMessage = new HttpRequestMessage(HttpMethod.Post, _messageEndpoint); StreamableHttpClientSessionTransport.CopyAdditionalHeaders(httpRequestMessage.Headers, _options.AdditionalHeaders, sessionId: null, protocolVersion: null); var response = await _httpClient.SendAsync(httpRequestMessage, message, cancellationToken).ConfigureAwait(false); diff --git a/src/ModelContextProtocol.Core/Client/StreamClientSessionTransport.cs b/src/ModelContextProtocol.Core/Client/StreamClientSessionTransport.cs index c896bd433..26365c2cf 100644 --- a/src/ModelContextProtocol.Core/Client/StreamClientSessionTransport.cs +++ b/src/ModelContextProtocol.Core/Client/StreamClientSessionTransport.cs @@ -105,6 +105,15 @@ public override async Task SendMessageAsync(JsonRpcMessage message, Cancellation var json = JsonSerializer.Serialize(message, McpJsonUtilities.JsonContext.Default.JsonRpcMessage); + if (Logger.IsEnabled(LogLevel.Trace)) + { + LogTransportSendingMessageSensitive(Name, json); + } + else + { + LogTransportSendingMessage(Name, id); + } + using var _ = await _sendLock.LockAsync(cancellationToken).ConfigureAwait(false); try { diff --git a/src/ModelContextProtocol.Core/Client/StreamableHttpClientSessionTransport.cs b/src/ModelContextProtocol.Core/Client/StreamableHttpClientSessionTransport.cs index 017512589..962712e1f 100644 --- a/src/ModelContextProtocol.Core/Client/StreamableHttpClientSessionTransport.cs +++ b/src/ModelContextProtocol.Core/Client/StreamableHttpClientSessionTransport.cs @@ -75,6 +75,21 @@ internal async Task SendHttpRequestAsync(JsonRpcMessage mes $"Call {nameof(McpClient)}.{nameof(McpClient.ResumeSessionAsync)} to resume existing sessions."); } + string messageId = "(no id)"; + if (message is JsonRpcMessageWithId messageWithId) + { + messageId = messageWithId.Id.ToString(); + } + + if (_logger.IsEnabled(LogLevel.Trace)) + { + LogTransportSendingMessageSensitive(Name, JsonSerializer.Serialize(message, McpJsonUtilities.JsonContext.Default.JsonRpcMessage)); + } + else + { + LogTransportSendingMessage(Name, messageId); + } + using var sendCts = CancellationTokenSource.CreateLinkedTokenSource(cancellationToken, _connectionCts.Token); cancellationToken = sendCts.Token; diff --git a/src/ModelContextProtocol.Core/Protocol/TransportBase.cs b/src/ModelContextProtocol.Core/Protocol/TransportBase.cs index 97897b53f..7ed15a9b9 100644 --- a/src/ModelContextProtocol.Core/Protocol/TransportBase.cs +++ b/src/ModelContextProtocol.Core/Protocol/TransportBase.cs @@ -166,6 +166,12 @@ protected void SetDisconnected(Exception? error = null) [LoggerMessage(Level = LogLevel.Error, Message = "{EndpointName} transport send failed for message ID '{MessageId}'.")] private protected partial void LogTransportSendFailed(string endpointName, string messageId, Exception exception); + [LoggerMessage(Level = LogLevel.Debug, Message = "{EndpointName} transport sending message with ID '{MessageId}'.")] + private protected partial void LogTransportSendingMessage(string endpointName, string messageId); + + [LoggerMessage(Level = LogLevel.Trace, Message = "{EndpointName} transport sending message. Message: '{Message}'.")] + private protected partial void LogTransportSendingMessageSensitive(string endpointName, string message); + [LoggerMessage(Level = LogLevel.Information, Message = "{EndpointName} transport reading messages.")] private protected partial void LogTransportEnteringReadMessagesLoop(string endpointName); diff --git a/src/ModelContextProtocol.Core/Server/StreamServerTransport.cs b/src/ModelContextProtocol.Core/Server/StreamServerTransport.cs index 7747d7f18..9d6a2b033 100644 --- a/src/ModelContextProtocol.Core/Server/StreamServerTransport.cs +++ b/src/ModelContextProtocol.Core/Server/StreamServerTransport.cs @@ -74,7 +74,17 @@ public override async Task SendMessageAsync(JsonRpcMessage message, Cancellation try { - await JsonSerializer.SerializeAsync(_outputStream, message, McpJsonUtilities.DefaultOptions.GetTypeInfo(typeof(JsonRpcMessage)), cancellationToken).ConfigureAwait(false); + if (Logger.IsEnabled(LogLevel.Trace)) + { + var json = JsonSerializer.Serialize(message, McpJsonUtilities.JsonContext.Default.JsonRpcMessage); + LogTransportSendingMessageSensitive(Name, json); + await _outputStream.WriteAsync(Encoding.UTF8.GetBytes(json), cancellationToken).ConfigureAwait(false); + } + else + { + LogTransportSendingMessage(Name, id); + await JsonSerializer.SerializeAsync(_outputStream, message, McpJsonUtilities.DefaultOptions.GetTypeInfo(typeof(JsonRpcMessage)), cancellationToken).ConfigureAwait(false); + } await _outputStream.WriteAsync(s_newlineBytes, cancellationToken).ConfigureAwait(false); await _outputStream.FlushAsync(cancellationToken).ConfigureAwait(false); } diff --git a/tests/ModelContextProtocol.Tests/Transport/StdioServerTransportTests.cs b/tests/ModelContextProtocol.Tests/Transport/StdioServerTransportTests.cs index cbe44da15..cf62cc3f4 100644 --- a/tests/ModelContextProtocol.Tests/Transport/StdioServerTransportTests.cs +++ b/tests/ModelContextProtocol.Tests/Transport/StdioServerTransportTests.cs @@ -1,4 +1,5 @@ -using ModelContextProtocol.Protocol; +using Microsoft.Extensions.Logging; +using ModelContextProtocol.Protocol; using ModelContextProtocol.Server; using ModelContextProtocol.Tests.Utils; using System.IO.Pipelines; @@ -193,4 +194,72 @@ public async Task SendMessageAsync_Should_Preserve_Unicode_Characters() Assert.True(magnifyingGlassFound, "Magnifying glass emoji not found in result"); Assert.True(rocketFound, "Rocket emoji not found in result"); } + + [Fact] + public async Task SendMessageAsync_Should_Log_At_Trace_Level() + { + // Arrange + var mockLoggerProvider = new MockLoggerProvider(); + using var traceLoggerFactory = Microsoft.Extensions.Logging.LoggerFactory.Create(builder => + { + builder.AddProvider(mockLoggerProvider); + builder.SetMinimumLevel(LogLevel.Trace); + }); + using var output = new MemoryStream(); + + await using var transport = new StreamServerTransport( + new Pipe().Reader.AsStream(), + output, + loggerFactory: traceLoggerFactory); + + // Act + var message = new JsonRpcRequest { Method = "test", Id = new RequestId(44) }; + await transport.SendMessageAsync(message, TestContext.Current.CancellationToken); + + // Assert + var traceLogMessages = mockLoggerProvider.LogMessages + .Where(x => x.LogLevel == LogLevel.Trace && x.Message.Contains("transport sending message")) + .ToList(); + + Assert.NotEmpty(traceLogMessages); + Assert.Contains(traceLogMessages, x => x.Message.Contains("\"method\":\"test\"") && x.Message.Contains("\"id\":44")); + } + + [Fact] + public async Task ReadMessagesAsync_Should_Log_Received_At_Trace_Level() + { + // Arrange + var mockLoggerProvider = new MockLoggerProvider(); + using var traceLoggerFactory = Microsoft.Extensions.Logging.LoggerFactory.Create(builder => + { + builder.AddProvider(mockLoggerProvider); + builder.SetMinimumLevel(LogLevel.Trace); + }); + + var message = new JsonRpcRequest { Method = "test", Id = new RequestId(99) }; + var json = JsonSerializer.Serialize(message, McpJsonUtilities.DefaultOptions); + + Pipe pipe = new(); + using var input = pipe.Reader.AsStream(); + + await using var transport = new StreamServerTransport( + input, + Stream.Null, + loggerFactory: traceLoggerFactory); + + // Act + await pipe.Writer.WriteAsync(Encoding.UTF8.GetBytes($"{json}\n"), TestContext.Current.CancellationToken); + + // Wait for the message to be processed + var canRead = await transport.MessageReader.WaitToReadAsync(TestContext.Current.CancellationToken); + Assert.True(canRead, "Nothing to read here from transport message reader"); + + // Assert + var traceLogMessages = mockLoggerProvider.LogMessages + .Where(x => x.LogLevel == LogLevel.Trace && x.Message.Contains("transport received message")) + .ToList(); + + Assert.NotEmpty(traceLogMessages); + Assert.Contains(traceLogMessages, x => x.Message.Contains("\"method\":\"test\"") && x.Message.Contains("\"id\":99")); + } } From cc7e97c28cede1f1beada415f82a91237430b728 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Wed, 28 Jan 2026 23:01:52 +0000 Subject: [PATCH 3/8] Remove Debug-level logging, keep only Trace-level logging for sent messages Addresses review feedback to only have Trace-level logging for JSON-RPC payloads. Co-authored-by: stephentoub <2642209+stephentoub@users.noreply.github.com> --- .../Client/SseClientSessionTransport.cs | 9 +-------- .../Client/StreamClientSessionTransport.cs | 9 +-------- .../StreamableHttpClientSessionTransport.cs | 15 +-------------- .../Protocol/TransportBase.cs | 3 --- .../Server/StreamServerTransport.cs | 14 +++----------- 5 files changed, 6 insertions(+), 44 deletions(-) diff --git a/src/ModelContextProtocol.Core/Client/SseClientSessionTransport.cs b/src/ModelContextProtocol.Core/Client/SseClientSessionTransport.cs index 93aec40e1..4fb8d3620 100644 --- a/src/ModelContextProtocol.Core/Client/SseClientSessionTransport.cs +++ b/src/ModelContextProtocol.Core/Client/SseClientSessionTransport.cs @@ -80,14 +80,7 @@ public override async Task SendMessageAsync( messageId = messageWithId.Id.ToString(); } - if (_logger.IsEnabled(LogLevel.Trace)) - { - LogTransportSendingMessageSensitive(Name, JsonSerializer.Serialize(message, McpJsonUtilities.JsonContext.Default.JsonRpcMessage)); - } - else - { - LogTransportSendingMessage(Name, messageId); - } + LogTransportSendingMessageSensitive(Name, JsonSerializer.Serialize(message, McpJsonUtilities.JsonContext.Default.JsonRpcMessage)); using var httpRequestMessage = new HttpRequestMessage(HttpMethod.Post, _messageEndpoint); StreamableHttpClientSessionTransport.CopyAdditionalHeaders(httpRequestMessage.Headers, _options.AdditionalHeaders, sessionId: null, protocolVersion: null); diff --git a/src/ModelContextProtocol.Core/Client/StreamClientSessionTransport.cs b/src/ModelContextProtocol.Core/Client/StreamClientSessionTransport.cs index 26365c2cf..d582abe31 100644 --- a/src/ModelContextProtocol.Core/Client/StreamClientSessionTransport.cs +++ b/src/ModelContextProtocol.Core/Client/StreamClientSessionTransport.cs @@ -105,14 +105,7 @@ public override async Task SendMessageAsync(JsonRpcMessage message, Cancellation var json = JsonSerializer.Serialize(message, McpJsonUtilities.JsonContext.Default.JsonRpcMessage); - if (Logger.IsEnabled(LogLevel.Trace)) - { - LogTransportSendingMessageSensitive(Name, json); - } - else - { - LogTransportSendingMessage(Name, id); - } + LogTransportSendingMessageSensitive(Name, json); using var _ = await _sendLock.LockAsync(cancellationToken).ConfigureAwait(false); try diff --git a/src/ModelContextProtocol.Core/Client/StreamableHttpClientSessionTransport.cs b/src/ModelContextProtocol.Core/Client/StreamableHttpClientSessionTransport.cs index 962712e1f..c27702e89 100644 --- a/src/ModelContextProtocol.Core/Client/StreamableHttpClientSessionTransport.cs +++ b/src/ModelContextProtocol.Core/Client/StreamableHttpClientSessionTransport.cs @@ -75,20 +75,7 @@ internal async Task SendHttpRequestAsync(JsonRpcMessage mes $"Call {nameof(McpClient)}.{nameof(McpClient.ResumeSessionAsync)} to resume existing sessions."); } - string messageId = "(no id)"; - if (message is JsonRpcMessageWithId messageWithId) - { - messageId = messageWithId.Id.ToString(); - } - - if (_logger.IsEnabled(LogLevel.Trace)) - { - LogTransportSendingMessageSensitive(Name, JsonSerializer.Serialize(message, McpJsonUtilities.JsonContext.Default.JsonRpcMessage)); - } - else - { - LogTransportSendingMessage(Name, messageId); - } + LogTransportSendingMessageSensitive(Name, JsonSerializer.Serialize(message, McpJsonUtilities.JsonContext.Default.JsonRpcMessage)); using var sendCts = CancellationTokenSource.CreateLinkedTokenSource(cancellationToken, _connectionCts.Token); cancellationToken = sendCts.Token; diff --git a/src/ModelContextProtocol.Core/Protocol/TransportBase.cs b/src/ModelContextProtocol.Core/Protocol/TransportBase.cs index 7ed15a9b9..c67b7e490 100644 --- a/src/ModelContextProtocol.Core/Protocol/TransportBase.cs +++ b/src/ModelContextProtocol.Core/Protocol/TransportBase.cs @@ -166,9 +166,6 @@ protected void SetDisconnected(Exception? error = null) [LoggerMessage(Level = LogLevel.Error, Message = "{EndpointName} transport send failed for message ID '{MessageId}'.")] private protected partial void LogTransportSendFailed(string endpointName, string messageId, Exception exception); - [LoggerMessage(Level = LogLevel.Debug, Message = "{EndpointName} transport sending message with ID '{MessageId}'.")] - private protected partial void LogTransportSendingMessage(string endpointName, string messageId); - [LoggerMessage(Level = LogLevel.Trace, Message = "{EndpointName} transport sending message. Message: '{Message}'.")] private protected partial void LogTransportSendingMessageSensitive(string endpointName, string message); diff --git a/src/ModelContextProtocol.Core/Server/StreamServerTransport.cs b/src/ModelContextProtocol.Core/Server/StreamServerTransport.cs index 9d6a2b033..1ab106e26 100644 --- a/src/ModelContextProtocol.Core/Server/StreamServerTransport.cs +++ b/src/ModelContextProtocol.Core/Server/StreamServerTransport.cs @@ -74,17 +74,9 @@ public override async Task SendMessageAsync(JsonRpcMessage message, Cancellation try { - if (Logger.IsEnabled(LogLevel.Trace)) - { - var json = JsonSerializer.Serialize(message, McpJsonUtilities.JsonContext.Default.JsonRpcMessage); - LogTransportSendingMessageSensitive(Name, json); - await _outputStream.WriteAsync(Encoding.UTF8.GetBytes(json), cancellationToken).ConfigureAwait(false); - } - else - { - LogTransportSendingMessage(Name, id); - await JsonSerializer.SerializeAsync(_outputStream, message, McpJsonUtilities.DefaultOptions.GetTypeInfo(typeof(JsonRpcMessage)), cancellationToken).ConfigureAwait(false); - } + var json = JsonSerializer.Serialize(message, McpJsonUtilities.JsonContext.Default.JsonRpcMessage); + LogTransportSendingMessageSensitive(Name, json); + await _outputStream.WriteAsync(Encoding.UTF8.GetBytes(json), cancellationToken).ConfigureAwait(false); await _outputStream.WriteAsync(s_newlineBytes, cancellationToken).ConfigureAwait(false); await _outputStream.FlushAsync(cancellationToken).ConfigureAwait(false); } From 9d9d8afa128b2ae087b2f5f8aee7f4c613bf9e7d Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Wed, 28 Jan 2026 23:06:42 +0000 Subject: [PATCH 4/8] Guard Trace logging calls with IsEnabled check Wrap logging calls with Logger.IsEnabled(LogLevel.Trace) to avoid expensive JSON serialization when trace logging is not enabled. Co-authored-by: stephentoub <2642209+stephentoub@users.noreply.github.com> --- .../Client/SseClientSessionTransport.cs | 5 ++++- .../Client/StreamClientSessionTransport.cs | 5 ++++- .../Client/StreamableHttpClientSessionTransport.cs | 5 ++++- .../Server/StreamServerTransport.cs | 5 ++++- 4 files changed, 16 insertions(+), 4 deletions(-) diff --git a/src/ModelContextProtocol.Core/Client/SseClientSessionTransport.cs b/src/ModelContextProtocol.Core/Client/SseClientSessionTransport.cs index 4fb8d3620..8929bbc3e 100644 --- a/src/ModelContextProtocol.Core/Client/SseClientSessionTransport.cs +++ b/src/ModelContextProtocol.Core/Client/SseClientSessionTransport.cs @@ -80,7 +80,10 @@ public override async Task SendMessageAsync( messageId = messageWithId.Id.ToString(); } - LogTransportSendingMessageSensitive(Name, JsonSerializer.Serialize(message, McpJsonUtilities.JsonContext.Default.JsonRpcMessage)); + if (_logger.IsEnabled(LogLevel.Trace)) + { + LogTransportSendingMessageSensitive(Name, JsonSerializer.Serialize(message, McpJsonUtilities.JsonContext.Default.JsonRpcMessage)); + } using var httpRequestMessage = new HttpRequestMessage(HttpMethod.Post, _messageEndpoint); StreamableHttpClientSessionTransport.CopyAdditionalHeaders(httpRequestMessage.Headers, _options.AdditionalHeaders, sessionId: null, protocolVersion: null); diff --git a/src/ModelContextProtocol.Core/Client/StreamClientSessionTransport.cs b/src/ModelContextProtocol.Core/Client/StreamClientSessionTransport.cs index d582abe31..f90c5ca43 100644 --- a/src/ModelContextProtocol.Core/Client/StreamClientSessionTransport.cs +++ b/src/ModelContextProtocol.Core/Client/StreamClientSessionTransport.cs @@ -105,7 +105,10 @@ public override async Task SendMessageAsync(JsonRpcMessage message, Cancellation var json = JsonSerializer.Serialize(message, McpJsonUtilities.JsonContext.Default.JsonRpcMessage); - LogTransportSendingMessageSensitive(Name, json); + if (Logger.IsEnabled(LogLevel.Trace)) + { + LogTransportSendingMessageSensitive(Name, json); + } using var _ = await _sendLock.LockAsync(cancellationToken).ConfigureAwait(false); try diff --git a/src/ModelContextProtocol.Core/Client/StreamableHttpClientSessionTransport.cs b/src/ModelContextProtocol.Core/Client/StreamableHttpClientSessionTransport.cs index c27702e89..83026bf85 100644 --- a/src/ModelContextProtocol.Core/Client/StreamableHttpClientSessionTransport.cs +++ b/src/ModelContextProtocol.Core/Client/StreamableHttpClientSessionTransport.cs @@ -75,7 +75,10 @@ internal async Task SendHttpRequestAsync(JsonRpcMessage mes $"Call {nameof(McpClient)}.{nameof(McpClient.ResumeSessionAsync)} to resume existing sessions."); } - LogTransportSendingMessageSensitive(Name, JsonSerializer.Serialize(message, McpJsonUtilities.JsonContext.Default.JsonRpcMessage)); + if (_logger.IsEnabled(LogLevel.Trace)) + { + LogTransportSendingMessageSensitive(Name, JsonSerializer.Serialize(message, McpJsonUtilities.JsonContext.Default.JsonRpcMessage)); + } using var sendCts = CancellationTokenSource.CreateLinkedTokenSource(cancellationToken, _connectionCts.Token); cancellationToken = sendCts.Token; diff --git a/src/ModelContextProtocol.Core/Server/StreamServerTransport.cs b/src/ModelContextProtocol.Core/Server/StreamServerTransport.cs index 1ab106e26..a56690dfb 100644 --- a/src/ModelContextProtocol.Core/Server/StreamServerTransport.cs +++ b/src/ModelContextProtocol.Core/Server/StreamServerTransport.cs @@ -75,7 +75,10 @@ public override async Task SendMessageAsync(JsonRpcMessage message, Cancellation try { var json = JsonSerializer.Serialize(message, McpJsonUtilities.JsonContext.Default.JsonRpcMessage); - LogTransportSendingMessageSensitive(Name, json); + if (Logger.IsEnabled(LogLevel.Trace)) + { + LogTransportSendingMessageSensitive(Name, json); + } await _outputStream.WriteAsync(Encoding.UTF8.GetBytes(json), cancellationToken).ConfigureAwait(false); await _outputStream.WriteAsync(s_newlineBytes, cancellationToken).ConfigureAwait(false); await _outputStream.FlushAsync(cancellationToken).ConfigureAwait(false); From 669781d9e9d556d9a08bdd8ea3a337b8298efbab Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Wed, 28 Jan 2026 23:10:48 +0000 Subject: [PATCH 5/8] Add IsEnabled guards for received message logging Guard LogTransportReceivedMessageSensitive calls with IsEnabled(LogLevel.Trace) checks to avoid unnecessary work when trace logging is disabled. Co-authored-by: stephentoub <2642209+stephentoub@users.noreply.github.com> --- .../Client/SseClientSessionTransport.cs | 5 ++++- .../Client/StreamClientSessionTransport.cs | 5 ++++- .../Client/StreamableHttpClientSessionTransport.cs | 5 ++++- .../Server/StreamServerTransport.cs | 5 ++++- 4 files changed, 16 insertions(+), 4 deletions(-) diff --git a/src/ModelContextProtocol.Core/Client/SseClientSessionTransport.cs b/src/ModelContextProtocol.Core/Client/SseClientSessionTransport.cs index 8929bbc3e..dfee63577 100644 --- a/src/ModelContextProtocol.Core/Client/SseClientSessionTransport.cs +++ b/src/ModelContextProtocol.Core/Client/SseClientSessionTransport.cs @@ -198,7 +198,10 @@ private async Task ProcessSseMessage(string data, CancellationToken cancellation return; } - LogTransportReceivedMessageSensitive(Name, data); + if (_logger.IsEnabled(LogLevel.Trace)) + { + LogTransportReceivedMessageSensitive(Name, data); + } try { diff --git a/src/ModelContextProtocol.Core/Client/StreamClientSessionTransport.cs b/src/ModelContextProtocol.Core/Client/StreamClientSessionTransport.cs index f90c5ca43..92a665f58 100644 --- a/src/ModelContextProtocol.Core/Client/StreamClientSessionTransport.cs +++ b/src/ModelContextProtocol.Core/Client/StreamClientSessionTransport.cs @@ -148,7 +148,10 @@ private async Task ReadMessagesAsync(CancellationToken cancellationToken) continue; } - LogTransportReceivedMessageSensitive(Name, line); + if (Logger.IsEnabled(LogLevel.Trace)) + { + LogTransportReceivedMessageSensitive(Name, line); + } await ProcessMessageAsync(line, cancellationToken).ConfigureAwait(false); } diff --git a/src/ModelContextProtocol.Core/Client/StreamableHttpClientSessionTransport.cs b/src/ModelContextProtocol.Core/Client/StreamableHttpClientSessionTransport.cs index 83026bf85..4feced2fa 100644 --- a/src/ModelContextProtocol.Core/Client/StreamableHttpClientSessionTransport.cs +++ b/src/ModelContextProtocol.Core/Client/StreamableHttpClientSessionTransport.cs @@ -347,7 +347,10 @@ private async Task ProcessSseResponseAsync( private async Task ProcessMessageAsync(string data, JsonRpcRequest? relatedRpcRequest, CancellationToken cancellationToken) { - LogTransportReceivedMessageSensitive(Name, data); + if (_logger.IsEnabled(LogLevel.Trace)) + { + LogTransportReceivedMessageSensitive(Name, data); + } try { diff --git a/src/ModelContextProtocol.Core/Server/StreamServerTransport.cs b/src/ModelContextProtocol.Core/Server/StreamServerTransport.cs index a56690dfb..b7c8312d4 100644 --- a/src/ModelContextProtocol.Core/Server/StreamServerTransport.cs +++ b/src/ModelContextProtocol.Core/Server/StreamServerTransport.cs @@ -112,7 +112,10 @@ private async Task ReadMessagesAsync() continue; } - LogTransportReceivedMessageSensitive(Name, line); + if (Logger.IsEnabled(LogLevel.Trace)) + { + LogTransportReceivedMessageSensitive(Name, line); + } try { From a3aa70bab17dac7ea6637c6e160300ccadf5d4b4 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Wed, 28 Jan 2026 23:15:39 +0000 Subject: [PATCH 6/8] Add LogTransportSendingMessageSensitive overload taking JsonRpcMessage Add an overload that takes a JsonRpcMessage directly, encapsulating the IsEnabled check and JSON serialization to simplify call sites. Co-authored-by: stephentoub <2642209+stephentoub@users.noreply.github.com> --- .../Client/SseClientSessionTransport.cs | 5 +---- .../Client/StreamableHttpClientSessionTransport.cs | 5 +---- .../Protocol/TransportBase.cs | 13 +++++++++++++ 3 files changed, 15 insertions(+), 8 deletions(-) diff --git a/src/ModelContextProtocol.Core/Client/SseClientSessionTransport.cs b/src/ModelContextProtocol.Core/Client/SseClientSessionTransport.cs index dfee63577..cdc0bfaf0 100644 --- a/src/ModelContextProtocol.Core/Client/SseClientSessionTransport.cs +++ b/src/ModelContextProtocol.Core/Client/SseClientSessionTransport.cs @@ -80,10 +80,7 @@ public override async Task SendMessageAsync( messageId = messageWithId.Id.ToString(); } - if (_logger.IsEnabled(LogLevel.Trace)) - { - LogTransportSendingMessageSensitive(Name, JsonSerializer.Serialize(message, McpJsonUtilities.JsonContext.Default.JsonRpcMessage)); - } + LogTransportSendingMessageSensitive(message); using var httpRequestMessage = new HttpRequestMessage(HttpMethod.Post, _messageEndpoint); StreamableHttpClientSessionTransport.CopyAdditionalHeaders(httpRequestMessage.Headers, _options.AdditionalHeaders, sessionId: null, protocolVersion: null); diff --git a/src/ModelContextProtocol.Core/Client/StreamableHttpClientSessionTransport.cs b/src/ModelContextProtocol.Core/Client/StreamableHttpClientSessionTransport.cs index 4feced2fa..7b644b523 100644 --- a/src/ModelContextProtocol.Core/Client/StreamableHttpClientSessionTransport.cs +++ b/src/ModelContextProtocol.Core/Client/StreamableHttpClientSessionTransport.cs @@ -75,10 +75,7 @@ internal async Task SendHttpRequestAsync(JsonRpcMessage mes $"Call {nameof(McpClient)}.{nameof(McpClient.ResumeSessionAsync)} to resume existing sessions."); } - if (_logger.IsEnabled(LogLevel.Trace)) - { - LogTransportSendingMessageSensitive(Name, JsonSerializer.Serialize(message, McpJsonUtilities.JsonContext.Default.JsonRpcMessage)); - } + LogTransportSendingMessageSensitive(message); using var sendCts = CancellationTokenSource.CreateLinkedTokenSource(cancellationToken, _connectionCts.Token); cancellationToken = sendCts.Token; diff --git a/src/ModelContextProtocol.Core/Protocol/TransportBase.cs b/src/ModelContextProtocol.Core/Protocol/TransportBase.cs index c67b7e490..e3e8e8c8b 100644 --- a/src/ModelContextProtocol.Core/Protocol/TransportBase.cs +++ b/src/ModelContextProtocol.Core/Protocol/TransportBase.cs @@ -1,6 +1,7 @@ using Microsoft.Extensions.Logging; using Microsoft.Extensions.Logging.Abstractions; using System.Diagnostics; +using System.Text.Json; using System.Threading.Channels; namespace ModelContextProtocol.Protocol; @@ -169,6 +170,18 @@ protected void SetDisconnected(Exception? error = null) [LoggerMessage(Level = LogLevel.Trace, Message = "{EndpointName} transport sending message. Message: '{Message}'.")] private protected partial void LogTransportSendingMessageSensitive(string endpointName, string message); + /// + /// Logs a sending message at Trace level if trace logging is enabled. + /// + /// The JSON-RPC message to log. + private protected void LogTransportSendingMessageSensitive(JsonRpcMessage message) + { + if (_logger.IsEnabled(LogLevel.Trace)) + { + LogTransportSendingMessageSensitive(Name, JsonSerializer.Serialize(message, McpJsonUtilities.JsonContext.Default.JsonRpcMessage)); + } + } + [LoggerMessage(Level = LogLevel.Information, Message = "{EndpointName} transport reading messages.")] private protected partial void LogTransportEnteringReadMessagesLoop(string endpointName); From 5fd721fac93451f3072978338dfca782204d01de Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Wed, 28 Jan 2026 23:21:45 +0000 Subject: [PATCH 7/8] Remove unnecessary IsEnabled guards from logging call sites Since the arguments to logging calls are just passing existing variables (no expensive work), the external guards are redundant - the source-generated logger methods handle the IsEnabled check internally. Co-authored-by: stephentoub <2642209+stephentoub@users.noreply.github.com> --- .../Client/SseClientSessionTransport.cs | 5 +---- .../Client/StreamClientSessionTransport.cs | 10 ++-------- .../Client/StreamableHttpClientSessionTransport.cs | 5 +---- .../Server/StreamServerTransport.cs | 10 ++-------- 4 files changed, 6 insertions(+), 24 deletions(-) diff --git a/src/ModelContextProtocol.Core/Client/SseClientSessionTransport.cs b/src/ModelContextProtocol.Core/Client/SseClientSessionTransport.cs index cdc0bfaf0..fa9ee97aa 100644 --- a/src/ModelContextProtocol.Core/Client/SseClientSessionTransport.cs +++ b/src/ModelContextProtocol.Core/Client/SseClientSessionTransport.cs @@ -195,10 +195,7 @@ private async Task ProcessSseMessage(string data, CancellationToken cancellation return; } - if (_logger.IsEnabled(LogLevel.Trace)) - { - LogTransportReceivedMessageSensitive(Name, data); - } + LogTransportReceivedMessageSensitive(Name, data); try { diff --git a/src/ModelContextProtocol.Core/Client/StreamClientSessionTransport.cs b/src/ModelContextProtocol.Core/Client/StreamClientSessionTransport.cs index 92a665f58..d582abe31 100644 --- a/src/ModelContextProtocol.Core/Client/StreamClientSessionTransport.cs +++ b/src/ModelContextProtocol.Core/Client/StreamClientSessionTransport.cs @@ -105,10 +105,7 @@ public override async Task SendMessageAsync(JsonRpcMessage message, Cancellation var json = JsonSerializer.Serialize(message, McpJsonUtilities.JsonContext.Default.JsonRpcMessage); - if (Logger.IsEnabled(LogLevel.Trace)) - { - LogTransportSendingMessageSensitive(Name, json); - } + LogTransportSendingMessageSensitive(Name, json); using var _ = await _sendLock.LockAsync(cancellationToken).ConfigureAwait(false); try @@ -148,10 +145,7 @@ private async Task ReadMessagesAsync(CancellationToken cancellationToken) continue; } - if (Logger.IsEnabled(LogLevel.Trace)) - { - LogTransportReceivedMessageSensitive(Name, line); - } + LogTransportReceivedMessageSensitive(Name, line); await ProcessMessageAsync(line, cancellationToken).ConfigureAwait(false); } diff --git a/src/ModelContextProtocol.Core/Client/StreamableHttpClientSessionTransport.cs b/src/ModelContextProtocol.Core/Client/StreamableHttpClientSessionTransport.cs index 7b644b523..c33d35322 100644 --- a/src/ModelContextProtocol.Core/Client/StreamableHttpClientSessionTransport.cs +++ b/src/ModelContextProtocol.Core/Client/StreamableHttpClientSessionTransport.cs @@ -344,10 +344,7 @@ private async Task ProcessSseResponseAsync( private async Task ProcessMessageAsync(string data, JsonRpcRequest? relatedRpcRequest, CancellationToken cancellationToken) { - if (_logger.IsEnabled(LogLevel.Trace)) - { - LogTransportReceivedMessageSensitive(Name, data); - } + LogTransportReceivedMessageSensitive(Name, data); try { diff --git a/src/ModelContextProtocol.Core/Server/StreamServerTransport.cs b/src/ModelContextProtocol.Core/Server/StreamServerTransport.cs index b7c8312d4..1ab106e26 100644 --- a/src/ModelContextProtocol.Core/Server/StreamServerTransport.cs +++ b/src/ModelContextProtocol.Core/Server/StreamServerTransport.cs @@ -75,10 +75,7 @@ public override async Task SendMessageAsync(JsonRpcMessage message, Cancellation try { var json = JsonSerializer.Serialize(message, McpJsonUtilities.JsonContext.Default.JsonRpcMessage); - if (Logger.IsEnabled(LogLevel.Trace)) - { - LogTransportSendingMessageSensitive(Name, json); - } + LogTransportSendingMessageSensitive(Name, json); await _outputStream.WriteAsync(Encoding.UTF8.GetBytes(json), cancellationToken).ConfigureAwait(false); await _outputStream.WriteAsync(s_newlineBytes, cancellationToken).ConfigureAwait(false); await _outputStream.FlushAsync(cancellationToken).ConfigureAwait(false); @@ -112,10 +109,7 @@ private async Task ReadMessagesAsync() continue; } - if (Logger.IsEnabled(LogLevel.Trace)) - { - LogTransportReceivedMessageSensitive(Name, line); - } + LogTransportReceivedMessageSensitive(Name, line); try { From 843ccca1f99e05cc83cbdf0fffd8f666532aab86 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Thu, 29 Jan 2026 18:07:04 +0000 Subject: [PATCH 8/8] Use LoggedTest.LoggerFactory and MockLoggerProvider in trace logging tests Simplified the tests by using the base class's LoggerFactory and MockLoggerProvider instead of creating local instances. The LoggerFactory is overridden in the constructor to use LogLevel.Trace for the trace logging tests. Co-authored-by: halter73 <54385+halter73@users.noreply.github.com> --- .../Transport/StdioServerTransportTests.cs | 29 ++++++++----------- 1 file changed, 12 insertions(+), 17 deletions(-) diff --git a/tests/ModelContextProtocol.Tests/Transport/StdioServerTransportTests.cs b/tests/ModelContextProtocol.Tests/Transport/StdioServerTransportTests.cs index cf62cc3f4..3f5756620 100644 --- a/tests/ModelContextProtocol.Tests/Transport/StdioServerTransportTests.cs +++ b/tests/ModelContextProtocol.Tests/Transport/StdioServerTransportTests.cs @@ -22,6 +22,14 @@ public StdioServerTransportTests(ITestOutputHelper testOutputHelper) InitializationTimeout = TimeSpan.FromSeconds(10), ServerInstructions = "Test Instructions" }; + + // Override the LoggerFactory to use Trace level for testing Trace-level logging + LoggerFactory = Microsoft.Extensions.Logging.LoggerFactory.Create(builder => + { + builder.AddProvider(XunitLoggerProvider); + builder.AddProvider(MockLoggerProvider); + builder.SetMinimumLevel(LogLevel.Trace); + }); } [Fact(Skip="https://github.com/modelcontextprotocol/csharp-sdk/issues/143")] @@ -199,25 +207,19 @@ public async Task SendMessageAsync_Should_Preserve_Unicode_Characters() public async Task SendMessageAsync_Should_Log_At_Trace_Level() { // Arrange - var mockLoggerProvider = new MockLoggerProvider(); - using var traceLoggerFactory = Microsoft.Extensions.Logging.LoggerFactory.Create(builder => - { - builder.AddProvider(mockLoggerProvider); - builder.SetMinimumLevel(LogLevel.Trace); - }); using var output = new MemoryStream(); await using var transport = new StreamServerTransport( new Pipe().Reader.AsStream(), output, - loggerFactory: traceLoggerFactory); + loggerFactory: LoggerFactory); // Act var message = new JsonRpcRequest { Method = "test", Id = new RequestId(44) }; await transport.SendMessageAsync(message, TestContext.Current.CancellationToken); // Assert - var traceLogMessages = mockLoggerProvider.LogMessages + var traceLogMessages = MockLoggerProvider.LogMessages .Where(x => x.LogLevel == LogLevel.Trace && x.Message.Contains("transport sending message")) .ToList(); @@ -229,13 +231,6 @@ public async Task SendMessageAsync_Should_Log_At_Trace_Level() public async Task ReadMessagesAsync_Should_Log_Received_At_Trace_Level() { // Arrange - var mockLoggerProvider = new MockLoggerProvider(); - using var traceLoggerFactory = Microsoft.Extensions.Logging.LoggerFactory.Create(builder => - { - builder.AddProvider(mockLoggerProvider); - builder.SetMinimumLevel(LogLevel.Trace); - }); - var message = new JsonRpcRequest { Method = "test", Id = new RequestId(99) }; var json = JsonSerializer.Serialize(message, McpJsonUtilities.DefaultOptions); @@ -245,7 +240,7 @@ public async Task ReadMessagesAsync_Should_Log_Received_At_Trace_Level() await using var transport = new StreamServerTransport( input, Stream.Null, - loggerFactory: traceLoggerFactory); + loggerFactory: LoggerFactory); // Act await pipe.Writer.WriteAsync(Encoding.UTF8.GetBytes($"{json}\n"), TestContext.Current.CancellationToken); @@ -255,7 +250,7 @@ public async Task ReadMessagesAsync_Should_Log_Received_At_Trace_Level() Assert.True(canRead, "Nothing to read here from transport message reader"); // Assert - var traceLogMessages = mockLoggerProvider.LogMessages + var traceLogMessages = MockLoggerProvider.LogMessages .Where(x => x.LogLevel == LogLevel.Trace && x.Message.Contains("transport received message")) .ToList();