Skip to content

Server spans lose HTTP parent when MCP trace context is also present (dual-parent / link scenario) #4132

@strawgate

Description

@strawgate

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:

  1. The ambient HTTP request span from the outer transport layer
  2. 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:

  1. HTTP transport (not stdio)
  2. Another OTEL-instrumented framework wrapping FastMCP
  3. 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

Metadata

Metadata

Assignees

No one assigned

    Labels

    enhancementImprovement to existing functionality. For issues and smaller PR improvements.httpRelated to HTTP transport, networking, or web server functionality.serverRelated to FastMCP server implementation or server-side functionality.

    Type

    No type
    No fields configured for issues without a type.

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions