4
4
using OpenTelemetry . Trace ;
5
5
using System . Diagnostics ;
6
6
using System . IO . Pipelines ;
7
+ using System . Text ;
8
+ using System . Text . Json ;
7
9
8
10
namespace ModelContextProtocol . Tests ;
9
11
@@ -14,6 +16,7 @@ public class DiagnosticTests
14
16
public async Task Session_TracksActivities ( )
15
17
{
16
18
var activities = new List < Activity > ( ) ;
19
+ var clientToServerLog = new List < string > ( ) ;
17
20
18
21
using ( var tracerProvider = OpenTelemetry . Sdk . CreateTracerProviderBuilder ( )
19
22
. AddSource ( "Experimental.ModelContextProtocol" )
@@ -28,7 +31,7 @@ await RunConnected(async (client, server) =>
28
31
29
32
var tool = tools . First ( t => t . Name == "DoubleValue" ) ;
30
33
await tool . InvokeAsync ( new ( ) { [ "amount" ] = 42 } , TestContext . Current . CancellationToken ) ;
31
- } ) ;
34
+ } , clientToServerLog ) ;
32
35
}
33
36
34
37
Assert . NotEmpty ( activities ) ;
@@ -64,6 +67,11 @@ await RunConnected(async (client, server) =>
64
67
65
68
Assert . Equal ( clientListToolsCall . SpanId , serverListToolsCall . ParentSpanId ) ;
66
69
Assert . Equal ( clientListToolsCall . TraceId , serverListToolsCall . TraceId ) ;
70
+
71
+ // Validate that the client trace context encoded to request.params._meta[traceparent]
72
+ using var listToolsJson = JsonDocument . Parse ( clientToServerLog . First ( s => s . Contains ( "\" method\" :\" tools/list\" " ) ) ) ;
73
+ var metaJson = listToolsJson . RootElement . GetProperty ( "params" ) . GetProperty ( "_meta" ) . GetRawText ( ) ;
74
+ Assert . Equal ( $$ """ {"traceparent":"00-{{ clientListToolsCall . TraceId }} -{{ clientListToolsCall . SpanId }} -01"}""" , metaJson ) ;
67
75
}
68
76
69
77
[ Fact ]
@@ -80,7 +88,7 @@ await RunConnected(async (client, server) =>
80
88
{
81
89
await client . CallToolAsync ( "Throw" , cancellationToken : TestContext . Current . CancellationToken ) ;
82
90
await Assert . ThrowsAsync < McpException > ( ( ) => client . CallToolAsync ( "does-not-exist" , cancellationToken : TestContext . Current . CancellationToken ) ) ;
83
- } ) ;
91
+ } , new List < string > ( ) ) ;
84
92
}
85
93
86
94
Assert . NotEmpty ( activities ) ;
@@ -120,11 +128,12 @@ await RunConnected(async (client, server) =>
120
128
Assert . Equal ( "-32602" , doesNotExistToolClient . Tags . Single ( t => t . Key == "rpc.jsonrpc.error_code" ) . Value ) ;
121
129
}
122
130
123
- private static async Task RunConnected ( Func < IMcpClient , IMcpServer , Task > action )
131
+ private static async Task RunConnected ( Func < IMcpClient , IMcpServer , Task > action , List < string > clientToServerLog )
124
132
{
125
133
Pipe clientToServerPipe = new ( ) , serverToClientPipe = new ( ) ;
126
134
StreamServerTransport serverTransport = new ( clientToServerPipe . Reader . AsStream ( ) , serverToClientPipe . Writer . AsStream ( ) ) ;
127
- StreamClientTransport clientTransport = new ( clientToServerPipe . Writer . AsStream ( ) , serverToClientPipe . Reader . AsStream ( ) ) ;
135
+ StreamClientTransport clientTransport = new ( new LoggingStream (
136
+ clientToServerPipe . Writer . AsStream ( ) , clientToServerLog . Add ) , serverToClientPipe . Reader . AsStream ( ) ) ;
128
137
129
138
Task serverTask ;
130
139
@@ -155,3 +164,32 @@ private static async Task RunConnected(Func<IMcpClient, IMcpServer, Task> action
155
164
await serverTask ;
156
165
}
157
166
}
167
+
168
+ public class LoggingStream : Stream
169
+ {
170
+ private readonly Stream _innerStream ;
171
+ private readonly Action < string > _logAction ;
172
+
173
+ public LoggingStream ( Stream innerStream , Action < string > logAction )
174
+ {
175
+ _innerStream = innerStream ?? throw new ArgumentNullException ( nameof ( innerStream ) ) ;
176
+ _logAction = logAction ?? throw new ArgumentNullException ( nameof ( logAction ) ) ;
177
+ }
178
+
179
+ public override void Write ( byte [ ] buffer , int offset , int count )
180
+ {
181
+ var data = Encoding . UTF8 . GetString ( buffer , offset , count ) ;
182
+ _logAction ( data ) ;
183
+ _innerStream . Write ( buffer , offset , count ) ;
184
+ }
185
+
186
+ public override bool CanRead => _innerStream . CanRead ;
187
+ public override bool CanSeek => _innerStream . CanSeek ;
188
+ public override bool CanWrite => _innerStream . CanWrite ;
189
+ public override long Length => _innerStream . Length ;
190
+ public override long Position { get => _innerStream . Position ; set => _innerStream . Position = value ; }
191
+ public override void Flush ( ) => _innerStream . Flush ( ) ;
192
+ public override int Read ( byte [ ] buffer , int offset , int count ) => _innerStream . Read ( buffer , offset , count ) ;
193
+ public override long Seek ( long offset , SeekOrigin origin ) => _innerStream . Seek ( offset , origin ) ;
194
+ public override void SetLength ( long value ) => _innerStream . SetLength ( value ) ;
195
+ }
0 commit comments