-
Notifications
You must be signed in to change notification settings - Fork 571
Description
Problem Statement
MCP tool calls do not continue distributed traces from their parent HTTP requests within the same service. The MCP integration creates new transactions with new trace IDs instead of joining the existing trace from the incoming HTTP request.
Environment:
- sentry-python version: 2.48.0
- MCP server: FastMCP (HTTP/SSE transport) within FastAPI application
- Service architecture:
- Service A (Go + Sentry) → Service B (Python/FastAPI + Sentry + MCP Server)
- HTTP trace propagates correctly to Service B's FastAPI handler
- MCP tool execution in Service B breaks the trace chain
Current behavior:
When get_start_span_function() executes during MCP tool handler wrapping, the HTTP transaction isn't accessible due to async context isolation. The function checks the current scope, but only finds unrelated spans (e.g., Redis, database operations) with different trace IDs, not the HTTP transaction that has the correct trace context from the incoming request.
Expected behavior:
MCP tool transaction should continue the HTTP request's trace (same trace_id, becomes child of HTTP span).
Actual behavior:
New transaction created with new trace_id from unrelated span in scope (e.g., Redis connection span), breaking the trace chain.
Root cause:
Async context isolation prevents the HTTP transaction from being accessible in the MCP tool handler's execution context.
Solution Brainstorm
Current workaround
I patched sentry-sdk to get it working for my project:
"""
Fix for MCP integration to properly continue distributed traces as transactions.
The issue: The MCP integration's get_start_span_function() doesn't check for
propagated trace context from incoming sentry-trace headers, so it creates new
transactions with new trace_ids instead of continuing the distributed trace.
The fix: When sentry-trace headers are present in the MCP request, use
continue_trace() to create a transaction that continues the existing trace
(same trace_id, but still a transaction for MCP Insights dashboard).
This maintains both:
1. Distributed tracing (same trace_id across services)
2. MCP Insights dashboard functionality (transactions, not spans)
"""
import logging
from typing import Any, Callable
import sentry_sdk
from sentry_sdk.tracing import Transaction
logger = logging.getLogger(__name__)
def create_transaction_from_trace_headers(
op: str = None,
name: str = None,
origin: str = "manual",
**kwargs: Any
) -> Transaction:
"""
Creates a transaction that continues the distributed trace from MCP request headers.
This uses continue_trace() which creates a Transaction with the trace_id
and parent_span_id from the sentry-trace header, so the transaction
becomes a child of the distributed trace while remaining a transaction
(not a span) for MCP Insights compatibility.
"""
from mcp.server.lowlevel.server import request_ctx
try:
ctx = request_ctx.get()
if ctx and hasattr(ctx, "request") and ctx.request is not None:
request = ctx.request
if hasattr(request, "headers"):
headers = {}
# Extract sentry-trace and baggage headers
sentry_trace = request.headers.get("sentry-trace")
baggage = request.headers.get("baggage")
if sentry_trace:
headers["sentry-trace"] = sentry_trace
if baggage:
headers["baggage"] = baggage
if headers:
# Use continue_trace to create a transaction that continues the trace
# This maintains the same trace_id but creates a transaction (not a span)
isolation_scope = sentry_sdk.get_isolation_scope()
transaction = isolation_scope.continue_trace(
environ_or_headers=headers,
op=op,
name=name,
origin=origin
)
# Start the transaction on the scope so it becomes the active transaction
# This allows the MCP integration to set data on it
return sentry_sdk.start_transaction(transaction=transaction)
except Exception:
# Could not extract headers - fall through to creating new transaction
pass
# Fallback to regular transaction if headers not available
return sentry_sdk.start_transaction(op=op, name=name, origin=origin, **kwargs)
def patched_get_start_span_function() -> Callable:
"""
Patched version that checks for propagated trace in MCP request headers.
Returns a function that will create either:
- A transaction continuing the distributed trace (if sentry-trace header present)
- A span (if there's a transaction in scope)
- A new transaction (if neither of the above)
"""
# First, check if we have sentry-trace headers in MCP request context
has_trace_header = False
try:
from mcp.server.lowlevel.server import request_ctx
ctx = request_ctx.get()
if ctx and hasattr(ctx, "request") and ctx.request is not None:
request = ctx.request
if hasattr(request, "headers"):
sentry_trace = request.headers.get("sentry-trace")
if sentry_trace:
has_trace_header = True
except Exception:
# MCP not installed or no request context - fall through to normal logic
pass
# If we have trace headers, return our custom function that creates
# a transaction continuing the trace
if has_trace_header:
return create_transaction_from_trace_headers
# Otherwise, use original logic
current_span = sentry_sdk.get_current_span()
scope = sentry_sdk.get_current_scope()
transaction = scope.transaction if scope else None
if transaction:
return sentry_sdk.start_span
if current_span and current_span.containing_transaction:
return sentry_sdk.start_span
# No transaction or propagated trace found
return sentry_sdk.start_transaction
# Apply the patch to both the utils module AND the mcp integration module
import sentry_sdk.ai.utils
sentry_sdk.ai.utils.get_start_span_function = patched_get_start_span_function
# Also patch it in the mcp module where it's already imported
import sentry_sdk.integrations.mcp
sentry_sdk.integrations.mcp.get_start_span_function = patched_get_start_span_function
logger.info("MCP integration patched: get_start_span_function creates transactions from trace headers")Proper Solution:
Modifie get_start_span_function() in sentry_sdk/ai/utils.py to check for sentry-trace headers in MCP request context before falling back to scope checking.
Where _create_transaction_from_mcp_headers extracts headers and uses continue_trace() to create a transaction inheriting the trace context.
For instance: #5271
Open questions:
- HTTP headers vs
_metafield: This approach reads trace context from HTTP headers. feat(develop): Add distributed tracing for MCP sentry-docs#15752 documents using the _meta field in
MCP protocol. Should both be supported? - Transport compatibility: HTTP headers work for SSE/HTTP transports, while
_metawould work for all transports (stdio, SSE, HTTP). What's preferred? - Transactions vs Spans: This creates transactions (not spans) to maintain MCP Insights dashboard compatibility while continuing the distributed trace. Is this the right trade-off?
Metadata
Metadata
Assignees
Projects
Status

