Skip to content

Native telemetry breaks third-party OpenTelemetry instrumentations #3993

@adityamehra

Description

@adityamehra

FastMCP's native OpenTelemetry telemetry interferes with third-party instrumentations that need to control the span hierarchy. We encountered this while building opentelemetry-instrumentation-fastmcp, which follows the MCP semantic conventions.

The problem has three parts:

1. start_as_current_span overrides external context. FastMCP's client_span and server_span use tracer.start_as_current_span(...), which unconditionally sets FastMCP's spans as the active trace context. When a third-party instrumentation has already established a parent span (e.g., an invoke_agent or execute_tool span), FastMCP's span replaces it as the active context. Downstream propagate.inject() calls then propagate FastMCP's span ID instead of the instrumentation's, breaking the client → server trace link. The result: client and server spans end up on different traces.

2. No standard flag to disable native telemetry. When users bring their own instrumentation — either via zero-code (opentelemetry-instrument) or manually — there's no way to turn off FastMCP's built-in spans. This leads to duplicate, conflicting span hierarchies where both FastMCP and the external instrumentation create overlapping spans for the same operations.

3. Native spans don't follow MCP semantic conventions. The OTel GenAI MCP semantic conventions define standard attributes (gen_ai.operation.name, mcp.method.name, network.transport, etc.) and a specific span structure for MCP operations. FastMCP's native spans use custom naming and attributes that don't align with these conventions, making them less useful for observability tooling that expects the standard schema.

Minimal repro for issue 1:

from opentelemetry import trace, context, propagate

tracer = trace.get_tracer("my-instrumentation")

# External instrumentation creates and activates a parent span
parent = tracer.start_span("execute_tool get_weather")
token = context.attach(trace.set_span_in_context(parent))

# FastMCP's client_span replaces the active context
with client_span("tools/call", ...):  # uses start_as_current_span internally
    carrier = {}
    propagate.inject(carrier)
    # carrier now has FastMCP's span ID, not the instrumentation's parent
    # → server extracts a different parent → traces break

context.detach(token)

Expected behavior: A way to disable native span creation so external instrumentations can own the span hierarchy while FastMCP's context propagation (inject_trace_context/extract_trace_context) continues to work.

Metadata

Metadata

Assignees

Labels

bugSomething isn't working. Reports of errors, unexpected behavior, or broken functionality.clientRelated to the FastMCP client SDK or client-side functionality.serverRelated to FastMCP server implementation or server-side functionality.

Type

No type

Projects

No projects

Milestone

No milestone

Relationships

None yet

Development

No branches or pull requests

Issue actions