Skip to content

Commit 47a4232

Browse files
authored
fix(mcp): replace orphaned spans with proper transactions (#3)
Previously, MCP tool calls created orphaned spans without parent transactions, causing Sentry to display '<unlabeled transaction>' instead of meaningful transaction names. This change updates the tracing implementation to create proper transactions following OpenTelemetry MCP Semantic Conventions: - Transaction name: 'tools/call {tool_name}' - Operation: 'mcp.server' - Proper hub management to ensure context propagation - All MCP attributes correctly attached to transactions Resolves the issue of unlabeled transactions appearing in Sentry, making MCP tool analytics properly visible and filterable by tool name.
1 parent 6e61a04 commit 47a4232

File tree

1 file changed

+32
-26
lines changed

1 file changed

+32
-26
lines changed

internal/cli/mcp/sentry.go

Lines changed: 32 additions & 26 deletions
Original file line numberDiff line numberDiff line change
@@ -43,7 +43,7 @@ const (
4343
)
4444

4545
// WithSentryTracing wraps an MCP tool handler with Sentry tracing.
46-
// It creates spans following OpenTelemetry MCP semantic conventions and
46+
// It creates transactions following OpenTelemetry MCP semantic conventions and
4747
// captures tool execution results and errors.
4848
//
4949
// Example usage:
@@ -56,56 +56,62 @@ const (
5656
// }))
5757
func WithSentryTracing[In, Out any](toolName string, handler mcp.ToolHandlerFor[In, Out]) mcp.ToolHandlerFor[In, Out] {
5858
return func(ctx context.Context, req *mcp.CallToolRequest, args In) (*mcp.CallToolResult, Out, error) {
59-
// Create span for tool execution
60-
span := sentry.StartSpan(ctx, OpMCPServer)
61-
defer span.Finish()
59+
// Get the current hub from context or create a new one
60+
hub := sentry.GetHubFromContext(ctx)
61+
if hub == nil {
62+
hub = sentry.CurrentHub().Clone()
63+
ctx = sentry.SetHubOnContext(ctx, hub)
64+
}
6265

63-
// Set span name following MCP conventions: "tools/call {tool_name}"
64-
span.Description = fmt.Sprintf("tools/call %s", toolName)
66+
// Create transaction for tool execution following MCP conventions
67+
// Transaction name: "tools/call {tool_name}" (e.g., "tools/call get_action_parameters")
68+
transactionName := fmt.Sprintf("tools/call %s", toolName)
69+
transaction := sentry.StartTransaction(ctx,
70+
transactionName,
71+
sentry.WithOpName(OpMCPServer),
72+
sentry.WithTransactionSource(sentry.SourceCustom),
73+
)
74+
defer transaction.Finish()
6575

6676
// Set common MCP attributes
67-
span.SetData(AttrMCPMethodName, "tools/call")
68-
span.SetData(AttrMCPToolName, toolName)
69-
span.SetData(AttrMCPTransport, TransportStdio)
70-
span.SetData(AttrNetworkTransport, NetworkTransportPipe)
71-
span.SetData(AttrNetworkProtocolVer, JSONRPCVersion)
77+
transaction.SetData(AttrMCPMethodName, "tools/call")
78+
transaction.SetData(AttrMCPToolName, toolName)
79+
transaction.SetData(AttrMCPTransport, TransportStdio)
80+
transaction.SetData(AttrNetworkTransport, NetworkTransportPipe)
81+
transaction.SetData(AttrNetworkProtocolVer, JSONRPCVersion)
7282

7383
// Set Sentry-specific attributes
74-
span.SetData("sentry.origin", OriginMCPFunction)
75-
span.SetData("sentry.source", SourceMCPRoute)
84+
transaction.SetData("sentry.origin", OriginMCPFunction)
85+
transaction.SetData("sentry.source", SourceMCPRoute)
7686

7787
// Extract and set request ID if available
7888
if req != nil {
7989
// The CallToolRequest may have metadata we can extract
8090
// For now, we'll use reflection to check if there's an ID field
81-
setRequestMetadata(span, req)
91+
setRequestMetadata(transaction, req)
8292
}
8393

8494
// Extract and set tool arguments
85-
setToolArguments(span, args)
95+
setToolArguments(transaction, args)
8696

87-
// Execute the handler with the span's context
88-
ctx = span.Context()
97+
// Execute the handler with the transaction's context
98+
ctx = transaction.Context()
8999
result, data, err := handler(ctx, req, args)
90100

91101
// Capture error if present
92102
if err != nil {
93-
span.Status = sentry.SpanStatusInternalError
94-
span.SetData(AttrMCPToolResultIsError, true)
103+
transaction.Status = sentry.SpanStatusInternalError
104+
transaction.SetData(AttrMCPToolResultIsError, true)
95105

96106
// Capture the error to Sentry with context
97-
hub := sentry.GetHubFromContext(ctx)
98-
if hub == nil {
99-
hub = sentry.CurrentHub()
100-
}
101107
hub.CaptureException(err)
102108
} else {
103-
span.Status = sentry.SpanStatusOK
104-
span.SetData(AttrMCPToolResultIsError, false)
109+
transaction.Status = sentry.SpanStatusOK
110+
transaction.SetData(AttrMCPToolResultIsError, false)
105111

106112
// Extract result metadata
107113
if result != nil {
108-
setResultMetadata(span, result)
114+
setResultMetadata(transaction, result)
109115
}
110116
}
111117

0 commit comments

Comments
 (0)