Skip to content

Commit ee8c485

Browse files
authored
Add Trace-level logging for JSON-RPC payloads in transports (#1191)
1 parent 5b189af commit ee8c485

File tree

6 files changed

+90
-2
lines changed

6 files changed

+90
-2
lines changed

src/ModelContextProtocol.Core/Client/SseClientSessionTransport.cs

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -80,6 +80,8 @@ public override async Task SendMessageAsync(
8080
messageId = messageWithId.Id.ToString();
8181
}
8282

83+
LogTransportSendingMessageSensitive(message);
84+
8385
using var httpRequestMessage = new HttpRequestMessage(HttpMethod.Post, _messageEndpoint);
8486
StreamableHttpClientSessionTransport.CopyAdditionalHeaders(httpRequestMessage.Headers, _options.AdditionalHeaders, sessionId: null, protocolVersion: null);
8587
var response = await _httpClient.SendAsync(httpRequestMessage, message, cancellationToken).ConfigureAwait(false);

src/ModelContextProtocol.Core/Client/StreamClientSessionTransport.cs

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -105,6 +105,8 @@ public override async Task SendMessageAsync(JsonRpcMessage message, Cancellation
105105

106106
var json = JsonSerializer.Serialize(message, McpJsonUtilities.JsonContext.Default.JsonRpcMessage);
107107

108+
LogTransportSendingMessageSensitive(Name, json);
109+
108110
using var _ = await _sendLock.LockAsync(cancellationToken).ConfigureAwait(false);
109111
try
110112
{

src/ModelContextProtocol.Core/Client/StreamableHttpClientSessionTransport.cs

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -75,6 +75,8 @@ internal async Task<HttpResponseMessage> SendHttpRequestAsync(JsonRpcMessage mes
7575
$"Call {nameof(McpClient)}.{nameof(McpClient.ResumeSessionAsync)} to resume existing sessions.");
7676
}
7777

78+
LogTransportSendingMessageSensitive(message);
79+
7880
using var sendCts = CancellationTokenSource.CreateLinkedTokenSource(cancellationToken, _connectionCts.Token);
7981
cancellationToken = sendCts.Token;
8082

src/ModelContextProtocol.Core/Protocol/TransportBase.cs

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
using Microsoft.Extensions.Logging;
22
using Microsoft.Extensions.Logging.Abstractions;
33
using System.Diagnostics;
4+
using System.Text.Json;
45
using System.Threading.Channels;
56

67
namespace ModelContextProtocol.Protocol;
@@ -166,6 +167,21 @@ protected void SetDisconnected(Exception? error = null)
166167
[LoggerMessage(Level = LogLevel.Error, Message = "{EndpointName} transport send failed for message ID '{MessageId}'.")]
167168
private protected partial void LogTransportSendFailed(string endpointName, string messageId, Exception exception);
168169

170+
[LoggerMessage(Level = LogLevel.Trace, Message = "{EndpointName} transport sending message. Message: '{Message}'.")]
171+
private protected partial void LogTransportSendingMessageSensitive(string endpointName, string message);
172+
173+
/// <summary>
174+
/// Logs a sending message at Trace level if trace logging is enabled.
175+
/// </summary>
176+
/// <param name="message">The JSON-RPC message to log.</param>
177+
private protected void LogTransportSendingMessageSensitive(JsonRpcMessage message)
178+
{
179+
if (_logger.IsEnabled(LogLevel.Trace))
180+
{
181+
LogTransportSendingMessageSensitive(Name, JsonSerializer.Serialize(message, McpJsonUtilities.JsonContext.Default.JsonRpcMessage));
182+
}
183+
}
184+
169185
[LoggerMessage(Level = LogLevel.Information, Message = "{EndpointName} transport reading messages.")]
170186
private protected partial void LogTransportEnteringReadMessagesLoop(string endpointName);
171187

src/ModelContextProtocol.Core/Server/StreamServerTransport.cs

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -74,7 +74,9 @@ public override async Task SendMessageAsync(JsonRpcMessage message, Cancellation
7474

7575
try
7676
{
77-
await JsonSerializer.SerializeAsync(_outputStream, message, McpJsonUtilities.DefaultOptions.GetTypeInfo(typeof(JsonRpcMessage)), cancellationToken).ConfigureAwait(false);
77+
var json = JsonSerializer.Serialize(message, McpJsonUtilities.JsonContext.Default.JsonRpcMessage);
78+
LogTransportSendingMessageSensitive(Name, json);
79+
await _outputStream.WriteAsync(Encoding.UTF8.GetBytes(json), cancellationToken).ConfigureAwait(false);
7880
await _outputStream.WriteAsync(s_newlineBytes, cancellationToken).ConfigureAwait(false);
7981
await _outputStream.FlushAsync(cancellationToken).ConfigureAwait(false);
8082
}

tests/ModelContextProtocol.Tests/Transport/StdioServerTransportTests.cs

Lines changed: 65 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
1-
using ModelContextProtocol.Protocol;
1+
using Microsoft.Extensions.Logging;
2+
using ModelContextProtocol.Protocol;
23
using ModelContextProtocol.Server;
34
using ModelContextProtocol.Tests.Utils;
45
using System.IO.Pipelines;
@@ -21,6 +22,14 @@ public StdioServerTransportTests(ITestOutputHelper testOutputHelper)
2122
InitializationTimeout = TimeSpan.FromSeconds(10),
2223
ServerInstructions = "Test Instructions"
2324
};
25+
26+
// Override the LoggerFactory to use Trace level for testing Trace-level logging
27+
LoggerFactory = Microsoft.Extensions.Logging.LoggerFactory.Create(builder =>
28+
{
29+
builder.AddProvider(XunitLoggerProvider);
30+
builder.AddProvider(MockLoggerProvider);
31+
builder.SetMinimumLevel(LogLevel.Trace);
32+
});
2433
}
2534

2635
[Fact(Skip="https://github.com/modelcontextprotocol/csharp-sdk/issues/143")]
@@ -193,4 +202,59 @@ public async Task SendMessageAsync_Should_Preserve_Unicode_Characters()
193202
Assert.True(magnifyingGlassFound, "Magnifying glass emoji not found in result");
194203
Assert.True(rocketFound, "Rocket emoji not found in result");
195204
}
205+
206+
[Fact]
207+
public async Task SendMessageAsync_Should_Log_At_Trace_Level()
208+
{
209+
// Arrange
210+
using var output = new MemoryStream();
211+
212+
await using var transport = new StreamServerTransport(
213+
new Pipe().Reader.AsStream(),
214+
output,
215+
loggerFactory: LoggerFactory);
216+
217+
// Act
218+
var message = new JsonRpcRequest { Method = "test", Id = new RequestId(44) };
219+
await transport.SendMessageAsync(message, TestContext.Current.CancellationToken);
220+
221+
// Assert
222+
var traceLogMessages = MockLoggerProvider.LogMessages
223+
.Where(x => x.LogLevel == LogLevel.Trace && x.Message.Contains("transport sending message"))
224+
.ToList();
225+
226+
Assert.NotEmpty(traceLogMessages);
227+
Assert.Contains(traceLogMessages, x => x.Message.Contains("\"method\":\"test\"") && x.Message.Contains("\"id\":44"));
228+
}
229+
230+
[Fact]
231+
public async Task ReadMessagesAsync_Should_Log_Received_At_Trace_Level()
232+
{
233+
// Arrange
234+
var message = new JsonRpcRequest { Method = "test", Id = new RequestId(99) };
235+
var json = JsonSerializer.Serialize(message, McpJsonUtilities.DefaultOptions);
236+
237+
Pipe pipe = new();
238+
using var input = pipe.Reader.AsStream();
239+
240+
await using var transport = new StreamServerTransport(
241+
input,
242+
Stream.Null,
243+
loggerFactory: LoggerFactory);
244+
245+
// Act
246+
await pipe.Writer.WriteAsync(Encoding.UTF8.GetBytes($"{json}\n"), TestContext.Current.CancellationToken);
247+
248+
// Wait for the message to be processed
249+
var canRead = await transport.MessageReader.WaitToReadAsync(TestContext.Current.CancellationToken);
250+
Assert.True(canRead, "Nothing to read here from transport message reader");
251+
252+
// Assert
253+
var traceLogMessages = MockLoggerProvider.LogMessages
254+
.Where(x => x.LogLevel == LogLevel.Trace && x.Message.Contains("transport received message"))
255+
.ToList();
256+
257+
Assert.NotEmpty(traceLogMessages);
258+
Assert.Contains(traceLogMessages, x => x.Message.Contains("\"method\":\"test\"") && x.Message.Contains("\"id\":99"));
259+
}
196260
}

0 commit comments

Comments
 (0)