When FastMCP runs behind an OTEL-instrumented HTTP framework (e.g. Starlette, FastAPI with opentelemetry-instrumentation-starlette), each incoming HTTP request creates an HTTP span. If the MCP client also propagates a traceparent in _meta, the server-side MCP operation span has two candidate parents:
- The ambient HTTP request span from the outer transport layer
- The MCP
traceparent from the MCP client's trace
A span can only have one parent. Today FastMCP uses the MCP _meta context as the parent (correct for end-to-end MCP traces). This means the ambient HTTP span is silently dropped from the span hierarchy.
The spec-correct approach
OpenTelemetry's Links are designed for exactly this fan-in pattern (also used for Kafka consumers, pub/sub, batch jobs, etc.). The right model is:
- Parent: MCP
traceparent from _meta (the MCP client owns the logical trace)
- Link: ambient HTTP request span (the transport context is preserved but not the primary trace)
What was implemented in PR #4046
The PR stored the ambient HTTP span context in the ASGI scope during request dispatch:
# In StreamableHTTPASGIApp.__call__:
ambient_span_context = trace.get_current_span().get_span_context()
if ambient_span_context.is_valid:
scope[AMBIENT_SPAN_CONTEXT_SCOPE_KEY] = ambient_span_context
Then in _get_parent_trace_context() it checked whether the resolved MCP parent differed from the ambient HTTP span and, if so, recorded the HTTP span as a Link:
if ambient_span_context.is_valid and parent_span_context != ambient_span_context:
return parent_context, [Link(ambient_span_context)]
Why it was deferred
This scenario only arises when all three conditions are simultaneously true:
- HTTP transport (not stdio)
- Another OTEL-instrumented framework wrapping FastMCP
- MCP client is also propagating trace context in
_meta
The implementation added meaningful complexity (ASGI scope plumbing, a new constant, changes to _get_parent_trace_context signature) for a fairly narrow edge case. We removed it from PR #4046 to keep the initial interop feature minimal.
When to revisit
This is worth implementing when there is concrete user demand — e.g. someone running FastMCP mounted in a FastAPI app with starlette-instrumentation and distributed tracing active. At that point the Link approach is the right solution and the implementation in PR #4046 is a reasonable starting point.
Related
When FastMCP runs behind an OTEL-instrumented HTTP framework (e.g. Starlette, FastAPI with
opentelemetry-instrumentation-starlette), each incoming HTTP request creates an HTTP span. If the MCP client also propagates atraceparentin_meta, the server-side MCP operation span has two candidate parents:traceparentfrom the MCP client's traceA span can only have one
parent. Today FastMCP uses the MCP_metacontext as the parent (correct for end-to-end MCP traces). This means the ambient HTTP span is silently dropped from the span hierarchy.The spec-correct approach
OpenTelemetry's Links are designed for exactly this fan-in pattern (also used for Kafka consumers, pub/sub, batch jobs, etc.). The right model is:
traceparentfrom_meta(the MCP client owns the logical trace)What was implemented in PR #4046
The PR stored the ambient HTTP span context in the ASGI scope during request dispatch:
Then in
_get_parent_trace_context()it checked whether the resolved MCP parent differed from the ambient HTTP span and, if so, recorded the HTTP span as aLink:Why it was deferred
This scenario only arises when all three conditions are simultaneously true:
_metaThe implementation added meaningful complexity (ASGI scope plumbing, a new constant, changes to
_get_parent_trace_contextsignature) for a fairly narrow edge case. We removed it from PR #4046 to keep the initial interop feature minimal.When to revisit
This is worth implementing when there is concrete user demand — e.g. someone running FastMCP mounted in a FastAPI app with starlette-instrumentation and distributed tracing active. At that point the Link approach is the right solution and the implementation in PR #4046 is a reasonable starting point.
Related