What
dd-trace-js instruments MCP (@modelcontextprotocol/sdk) with client and server spans, but does not propagate distributed trace context across the MCP boundary. A JS MCP client calling an out-of-process server (notably a non-JS server) produces a server-side trace that is detached from the client's trace.
dd-trace-py already implements this over a defined wire format. Please add the symmetric support to dd-trace-js so a mixed fleet stitches end-to-end.
Why / impact
MCP servers are frequently a different runtime than the agent calling them. Today:
The existing wire format (dd-trace-py)
ddtrace/contrib/internal/mcp/patch.py:
- Inject —
_set_distributed_headers_into_mcp_request wraps BaseSession.send_request: HTTPPropagator.inject(span.context, headers) stored at request.params._meta._dd_trace_context.
- Extract —
_extract_distributed_headers_from_mcp_request on RequestResponder.__enter__: reads params._meta._dd_trace_context → activate_distributed_headers(...).
- Gated by
DD_MCP_DISTRIBUTED_TRACING (default true).
_meta is the MCP-spec-sanctioned place for this (https://modelcontextprotocol.io/specification/2025-06-18/basic#meta).
Proposal for dd-trace-js
- Client inject at
Protocol.request (the single chokepoint every client request flows through; the symmetric point to py's BaseSession.send_request): set request.params._meta._dd_trace_context from a TEXT_MAP injection of the active span. No-op without an active span; best-effort.
- Server extract at
Protocol._onrequest: prefer params._meta._dd_trace_context as the parent when present, falling back to the current same-process ALS context.
- Reuse the
DD_MCP_DISTRIBUTED_TRACING env name for parity with dd-trace-py.
The original instrumentation (#7845) explicitly reserved this:
// NOTE: Protocol.request (...) is intentionally not instrumented here.
// It will be used for distributed tracing header injection when server-side coverage is added.
Repro
- Client: Node,
dd-trace@5.106.0, @langchain/mcp-adapters, @modelcontextprotocol/sdk@1.29.0.
- Server: Python,
ddtrace@4.10.5 (default DD_MCP_DISTRIBUTED_TRACING).
- Observed: server
mcp.tools_call span is parent_id:0 (root); downstream gRPC/Bedrock/Postgres spans nest under that detached root, not the client trace. The Python server's extract works — there's simply no _meta._dd_trace_context to read because the JS client never injects it.
Willing to contribute
Happy to open the PR (client inject + server extract). See related work in #8916 / #8921.
What
dd-trace-js instruments MCP (
@modelcontextprotocol/sdk) with client and server spans, but does not propagate distributed trace context across the MCP boundary. A JS MCP client calling an out-of-process server (notably a non-JS server) produces a server-side trace that is detached from the client's trace.dd-trace-py already implements this over a defined wire format. Please add the symmetric support to dd-trace-js so a mixed fleet stitches end-to-end.
Why / impact
MCP servers are frequently a different runtime than the agent calling them. Today:
Protocol._onrequest, see feat(@modelcontextprotocol/sdk): add tracing for MCP server requests and client operations #8916).The existing wire format (dd-trace-py)
ddtrace/contrib/internal/mcp/patch.py:_set_distributed_headers_into_mcp_requestwrapsBaseSession.send_request:HTTPPropagator.inject(span.context, headers)stored atrequest.params._meta._dd_trace_context._extract_distributed_headers_from_mcp_requestonRequestResponder.__enter__: readsparams._meta._dd_trace_context→activate_distributed_headers(...).DD_MCP_DISTRIBUTED_TRACING(defaulttrue)._metais the MCP-spec-sanctioned place for this (https://modelcontextprotocol.io/specification/2025-06-18/basic#meta).Proposal for dd-trace-js
Protocol.request(the single chokepoint every client request flows through; the symmetric point to py'sBaseSession.send_request): setrequest.params._meta._dd_trace_contextfrom a TEXT_MAP injection of the active span. No-op without an active span; best-effort.Protocol._onrequest: preferparams._meta._dd_trace_contextas the parent when present, falling back to the current same-process ALS context.DD_MCP_DISTRIBUTED_TRACINGenv name for parity with dd-trace-py.The original instrumentation (#7845) explicitly reserved this:
Repro
dd-trace@5.106.0,@langchain/mcp-adapters,@modelcontextprotocol/sdk@1.29.0.ddtrace@4.10.5(defaultDD_MCP_DISTRIBUTED_TRACING).mcp.tools_callspan isparent_id:0(root); downstream gRPC/Bedrock/Postgres spans nest under that detached root, not the client trace. The Python server's extract works — there's simply no_meta._dd_trace_contextto read because the JS client never injects it.Willing to contribute
Happy to open the PR (client inject + server extract). See related work in #8916 / #8921.