@@ -61,6 +61,11 @@ internal sealed partial class McpSessionHandler : IAsyncDisposable
6161
6262 // This _sessionId is solely used to identify the session in telemetry and logs.
6363 private readonly string _sessionId = Guid . NewGuid ( ) . ToString ( "N" ) ;
64+
65+ // The negotiated MCP protocol version (set after initialization).
66+ // Note: This is exposed via property for telemetry purposes.
67+ private string ? _negotiatedProtocolVersion ;
68+
6469 private long _lastRequestId ;
6570
6671 private CancellationTokenSource ? _messageProcessingCts ;
@@ -110,6 +115,15 @@ public McpSessionHandler(
110115 /// </summary>
111116 public string EndpointName { get ; set ; }
112117
118+ /// <summary>
119+ /// Gets or sets the negotiated MCP protocol version for telemetry.
120+ /// </summary>
121+ public string ? NegotiatedProtocolVersion
122+ {
123+ get => _negotiatedProtocolVersion ;
124+ set => _negotiatedProtocolVersion = value ;
125+ }
126+
113127 /// <summary>
114128 /// Starts processing messages from the transport. This method will block until the transport is disconnected.
115129 /// This is generally started in a background task or thread from the initialization logic of the derived class.
@@ -261,24 +275,40 @@ private async Task HandleMessageAsync(JsonRpcMessage message, CancellationToken
261275 {
262276 Histogram < double > durationMetric = _isServer ? s_serverOperationDuration : s_clientOperationDuration ;
263277 string method = GetMethodName ( message ) ;
278+ string ? target = ExtractTargetFromMessage ( message , method ) ;
264279
265280 long ? startingTimestamp = durationMetric . Enabled ? Stopwatch . GetTimestamp ( ) : null ;
266281
267- Activity ? activity = Diagnostics . ShouldInstrumentMessage ( message ) ?
268- Diagnostics . ActivitySource . StartActivity (
269- CreateActivityName ( method ) ,
270- ActivityKind . Server ,
271- parentContext : _propagator . ExtractActivityContext ( message ) ,
272- links : Diagnostics . ActivityLinkFromCurrent ( ) ) :
273- null ;
282+ // Per MCP semantic conventions: If outer GenAI instrumentation is already tracing the tool execution
283+ // (i.e., Activity.Current has gen_ai.operation.name = execute_tool), we should add MCP attributes
284+ // to that activity instead of creating a new one.
285+ Activity ? activity = null ;
286+ bool usingOuterActivity = false ;
287+ if ( Diagnostics . ShouldInstrumentMessage ( message ) )
288+ {
289+ if ( method == RequestMethods . ToolsCall && Diagnostics . TryGetOuterToolExecutionActivity ( out var outerActivity ) )
290+ {
291+ // Add MCP-specific attributes to the existing tool execution span
292+ activity = outerActivity ;
293+ usingOuterActivity = true ;
294+ }
295+ else
296+ {
297+ activity = Diagnostics . ActivitySource . StartActivity (
298+ CreateActivityName ( method , target ) ,
299+ ActivityKind . Server ,
300+ parentContext : _propagator . ExtractActivityContext ( message ) ,
301+ links : Diagnostics . ActivityLinkFromCurrent ( ) ) ;
302+ }
303+ }
274304
275305 TagList tags = default ;
276306 bool addTags = activity is { IsAllDataRequested : true } || startingTimestamp is not null ;
277307 try
278308 {
279309 if ( addTags )
280310 {
281- AddTags ( ref tags , activity , message , method ) ;
311+ AddTags ( ref tags , activity , message , method , target , usingOuterActivity ) ;
282312 }
283313
284314 switch ( message )
@@ -319,7 +349,7 @@ private async Task HandleMessageAsync(JsonRpcMessage message, CancellationToken
319349 }
320350 finally
321351 {
322- FinalizeDiagnostics ( activity , startingTimestamp , durationMetric , ref tags ) ;
352+ FinalizeDiagnostics ( activity , startingTimestamp , durationMetric , ref tags , disposeActivity : ! usingOuterActivity ) ;
323353 }
324354 }
325355
@@ -422,11 +452,27 @@ public async Task<JsonRpcResponse> SendRequestAsync(JsonRpcRequest request, Canc
422452
423453 Histogram < double > durationMetric = _isServer ? s_serverOperationDuration : s_clientOperationDuration ;
424454 string method = request . Method ;
455+ string ? target = ExtractTargetFromMessage ( request , method ) ;
425456
426457 long ? startingTimestamp = durationMetric . Enabled ? Stopwatch . GetTimestamp ( ) : null ;
427- using Activity ? activity = Diagnostics . ShouldInstrumentMessage ( request ) ?
428- Diagnostics . ActivitySource . StartActivity ( McpSessionHandler . CreateActivityName ( method ) , ActivityKind . Client ) :
429- null ;
458+
459+ // Per MCP semantic conventions: If outer GenAI instrumentation is already tracing the tool execution
460+ // (i.e., Activity.Current has gen_ai.operation.name = execute_tool), we should add MCP attributes
461+ // to that activity instead of creating a new one.
462+ Activity ? activity = null ;
463+ bool usingOuterActivity = false ;
464+ if ( Diagnostics . ShouldInstrumentMessage ( request ) )
465+ {
466+ if ( method == RequestMethods . ToolsCall && Diagnostics . TryGetOuterToolExecutionActivity ( out var outerActivity ) )
467+ {
468+ activity = outerActivity ;
469+ usingOuterActivity = true ;
470+ }
471+ else
472+ {
473+ activity = Diagnostics . ActivitySource . StartActivity ( CreateActivityName ( method , target ) , ActivityKind . Client ) ;
474+ }
475+ }
430476
431477 // Set request ID
432478 if ( request . Id . Id is null )
@@ -445,7 +491,7 @@ public async Task<JsonRpcResponse> SendRequestAsync(JsonRpcRequest request, Canc
445491 {
446492 if ( addTags )
447493 {
448- AddTags ( ref tags , activity , request , method ) ;
494+ AddTags ( ref tags , activity , request , method , target , usingOuterActivity ) ;
449495 }
450496
451497 if ( _logger . IsEnabled ( LogLevel . Trace ) )
@@ -506,7 +552,7 @@ public async Task<JsonRpcResponse> SendRequestAsync(JsonRpcRequest request, Canc
506552 finally
507553 {
508554 _pendingRequests . TryRemove ( request . Id , out _ ) ;
509- FinalizeDiagnostics ( activity , startingTimestamp , durationMetric , ref tags ) ;
555+ FinalizeDiagnostics ( activity , startingTimestamp , durationMetric , ref tags , disposeActivity : ! usingOuterActivity ) ;
510556 }
511557 }
512558
@@ -518,10 +564,11 @@ public async Task SendMessageAsync(JsonRpcMessage message, CancellationToken can
518564
519565 Histogram < double > durationMetric = _isServer ? s_serverOperationDuration : s_clientOperationDuration ;
520566 string method = GetMethodName ( message ) ;
567+ string ? target = ExtractTargetFromMessage ( message , method ) ;
521568
522569 long ? startingTimestamp = durationMetric . Enabled ? Stopwatch . GetTimestamp ( ) : null ;
523570 using Activity ? activity = Diagnostics . ShouldInstrumentMessage ( message ) ?
524- Diagnostics . ActivitySource . StartActivity ( McpSessionHandler . CreateActivityName ( method ) , ActivityKind . Client ) :
571+ Diagnostics . ActivitySource . StartActivity ( CreateActivityName ( method , target ) , ActivityKind . Client ) :
525572 null ;
526573
527574 TagList tags = default ;
@@ -534,7 +581,7 @@ public async Task SendMessageAsync(JsonRpcMessage message, CancellationToken can
534581 {
535582 if ( addTags )
536583 {
537- AddTags ( ref tags , activity , message , method ) ;
584+ AddTags ( ref tags , activity , message , method , target , usingOuterActivity : false ) ;
538585 }
539586
540587 if ( _logger . IsEnabled ( LogLevel . Trace ) )
@@ -565,7 +612,7 @@ public async Task SendMessageAsync(JsonRpcMessage message, CancellationToken can
565612 }
566613 finally
567614 {
568- FinalizeDiagnostics ( activity , startingTimestamp , durationMetric , ref tags ) ;
615+ FinalizeDiagnostics ( activity , startingTimestamp , durationMetric , ref tags , disposeActivity : true ) ;
569616 }
570617 }
571618
@@ -589,6 +636,38 @@ private Task SendToRelatedTransportAsync(JsonRpcMessage message, CancellationTok
589636
590637 private static string CreateActivityName ( string method ) => method ;
591638
639+ /// <summary>
640+ /// Creates a span name according to semantic conventions: "{mcp.method.name} {target}" where
641+ /// target is the tool name, prompt name, or resource URI when applicable.
642+ /// </summary>
643+ private static string CreateActivityName ( string method , string ? target ) =>
644+ target is null ? method : $ "{ method } { target } ";
645+
646+ /// <summary>
647+ /// Extracts the target (tool name, prompt name, or resource URI) from a message for use in span naming.
648+ /// </summary>
649+ private static string ? ExtractTargetFromMessage ( JsonRpcMessage message , string method )
650+ {
651+ JsonObject ? paramsObj = message switch
652+ {
653+ JsonRpcRequest request => request . Params as JsonObject ,
654+ JsonRpcNotification notification => notification . Params as JsonObject ,
655+ _ => null
656+ } ;
657+
658+ if ( paramsObj is null )
659+ {
660+ return null ;
661+ }
662+
663+ return method switch
664+ {
665+ RequestMethods . ToolsCall or RequestMethods . PromptsGet => GetStringProperty ( paramsObj , "name" ) ,
666+ // Note: resource URI is not included in span name by default due to high cardinality per semantic conventions
667+ _ => null
668+ } ;
669+ }
670+
592671 private static string GetMethodName ( JsonRpcMessage message ) =>
593672 message switch
594673 {
@@ -597,11 +676,23 @@ private static string GetMethodName(JsonRpcMessage message) =>
597676 _ => "unknownMethod"
598677 } ;
599678
600- private void AddTags ( ref TagList tags , Activity ? activity , JsonRpcMessage message , string method )
679+ private void AddTags ( ref TagList tags , Activity ? activity , JsonRpcMessage message , string method , string ? target , bool usingOuterActivity )
601680 {
602681 tags . Add ( "mcp.method.name" , method ) ;
603682 tags . Add ( "network.transport" , _transportKind ) ;
604683
684+ // Per semantic conventions: network.protocol.name when applicable (HTTP transports)
685+ if ( _transportKind == "tcp" )
686+ {
687+ tags . Add ( "network.protocol.name" , "http" ) ;
688+ }
689+
690+ // Per semantic conventions: mcp.protocol.version is Recommended
691+ if ( _negotiatedProtocolVersion is not null )
692+ {
693+ tags . Add ( "mcp.protocol.version" , _negotiatedProtocolVersion ) ;
694+ }
695+
605696 // TODO: When using HTTP transport, add:
606697 // - server.address and server.port on client spans and metrics
607698 // - client.address and client.port on server spans (not metrics because of cardinality)
@@ -615,25 +706,18 @@ private void AddTags(ref TagList tags, Activity? activity, JsonRpcMessage messag
615706 {
616707 activity . AddTag ( "jsonrpc.request.id" , withId . Id . Id ? . ToString ( ) ) ;
617708 }
618- }
619-
620- JsonObject ? paramsObj = message switch
621- {
622- JsonRpcRequest request => request . Params as JsonObject ,
623- JsonRpcNotification notification => notification . Params as JsonObject ,
624- _ => null
625- } ;
626709
627- if ( paramsObj == null )
628- {
629- return ;
710+ // If we're adding tags to an outer activity, we don't need to set DisplayName as it's already set
711+ if ( ! usingOuterActivity && target is not null )
712+ {
713+ activity . DisplayName = $ "{ method } { target } ";
714+ }
630715 }
631716
632- string ? target = null ;
717+ // Add target-specific tags based on method
633718 switch ( method )
634719 {
635720 case RequestMethods . ToolsCall :
636- target = GetStringProperty ( paramsObj , "name" ) ;
637721 if ( target is not null )
638722 {
639723 // Per semantic conventions: gen_ai.tool.name for tool operations
@@ -644,7 +728,6 @@ private void AddTags(ref TagList tags, Activity? activity, JsonRpcMessage messag
644728 break ;
645729
646730 case RequestMethods . PromptsGet :
647- target = GetStringProperty ( paramsObj , "name" ) ;
648731 if ( target is not null )
649732 {
650733 // Per semantic conventions: gen_ai.prompt.name for prompt operations
@@ -656,18 +739,22 @@ private void AddTags(ref TagList tags, Activity? activity, JsonRpcMessage messag
656739 case RequestMethods . ResourcesSubscribe :
657740 case RequestMethods . ResourcesUnsubscribe :
658741 case NotificationMethods . ResourceUpdatedNotification :
659- target = GetStringProperty ( paramsObj , "uri" ) ;
660- if ( target is not null )
661742 {
662- tags . Add ( "mcp.resource.uri" , target ) ;
743+ // Get resource URI from params (not included in span name due to high cardinality)
744+ JsonObject ? paramsObj = message switch
745+ {
746+ JsonRpcRequest request => request . Params as JsonObject ,
747+ JsonRpcNotification notification => notification . Params as JsonObject ,
748+ _ => null
749+ } ;
750+ string ? uri = paramsObj is not null ? GetStringProperty ( paramsObj , "uri" ) : null ;
751+ if ( uri is not null )
752+ {
753+ tags . Add ( "mcp.resource.uri" , uri ) ;
754+ }
663755 }
664756 break ;
665757 }
666-
667- if ( activity is { IsAllDataRequested : true } )
668- {
669- activity . DisplayName = target == null ? method : $ "{ method } { target } ";
670- }
671758 }
672759
673760 private static void AddExceptionTags ( ref TagList tags , Activity ? activity , Exception e )
@@ -718,7 +805,7 @@ private static void AddResponseTags(ref TagList tags, Activity? activity, JsonNo
718805 }
719806
720807 private static void FinalizeDiagnostics (
721- Activity ? activity , long ? startingTimestamp , Histogram < double > durationMetric , ref TagList tags )
808+ Activity ? activity , long ? startingTimestamp , Histogram < double > durationMetric , ref TagList tags , bool disposeActivity = true )
722809 {
723810 try
724811 {
@@ -737,7 +824,11 @@ private static void FinalizeDiagnostics(
737824 }
738825 finally
739826 {
740- activity ? . Dispose ( ) ;
827+ // Only dispose the activity if we created it (not when reusing an outer GenAI activity)
828+ if ( disposeActivity )
829+ {
830+ activity ? . Dispose ( ) ;
831+ }
741832 }
742833 }
743834
0 commit comments