Which component is this bug for?
Traceloop SDK
📜 Description
McpInstrumentor.patch_mcp_client() (which wraps mcp.shared.session.BaseSession.send_request) is supposed to inject a traceparent into the outgoing JSON-RPC request's _meta field so that the MCP server can continue the client's trace. In practice this never happens for a normal tool call, because the injection is gated behind a check that only runs when params.meta is already non-None:
if params:
if hasattr(args[0].root.params, "meta"):
meta = args[0].root.params.meta
if meta and len(args) > 0:
carrier = {}
TraceContextTextMapPropagator().inject(carrier)
meta.traceparent = carrier["traceparent"]
args[0].root.params.meta = meta
RequestParams.meta (aliased to _meta) defaults to None and is only populated by the mcp SDK itself when a caller explicitly requests a progress token (progress_callback argument). Nothing in the standard mcp.ClientSession.call_tool() path, nor langchain_mcp_adapters.tools.load_mcp_tools(), sets a progress token by default. So for an ordinary tool call, params.meta is None, if meta: is False, and the traceparent is silently never attached. The MCP server therefore starts a brand new trace instead of continuing the caller's trace.
This is a regression introduced by #3179 ("fix(mcp): do not override meta pydantic types"). Before that PR, the code always created a Meta() object when one was missing:
if params is None:
args[0].root.params = mcp.types.RequestParams()
meta = mcp.types.RequestParams.Meta()
else:
if hasattr(args[0].root.params, "meta"):
meta = args[0].root.params.meta
if meta is None:
meta = mcp.types.RequestParams.Meta()
...
meta.traceparent = parent_span["traceparent"]
args[0].root.params.meta = meta
#3179's stated goal was to stop clobbering an already-present _meta (e.g. when a progress token was set) — but the fix went further than needed and removed the "create an empty Meta() when absent" fallback entirely, instead of only skipping the overwrite case. That collapsed the common case (no pre-existing _meta) into a no-op.
👟 Reproduction steps
- Run an MCP client and server both instrumented via
Traceloop.init() (which enables Instruments.MCP by default), talking over the Streamable HTTP transport.
- From within a traced request/span, call any tool via the standard
ClientSession.call_tool(...) (or a langchain_mcp_adapters-generated tool wrapper) — i.e. a call that does not pass a progress_callback.
- Inspect the outgoing JSON-RPC request body:
params._meta is absent.
- On the server, the resulting
tools/call span has no parent — it starts a new trace, instead of continuing the trace of the request that triggered the tool call.
Reproduced against traceloop-sdk==0.62.1 / opentelemetry-instrumentation-mcp==0.62.1 (current latest), mcp==1.28.1, fastmcp==3.4.4. Confirmed via two traces from a real deployment: the caller's trace ends at a {tool_name}.tool span with no HTTP/transport child span, and the MCP server's trace root is tools/call {tool_name} with no parent context.
This can also be reproduced in the OpenTelemetry Demo:
- Go to
.env and update MCP_ENABLED=True.
- Run
make start-agentic
- Navigate to
localhost:8080/chatbot
- Click on one of the suggested questions
- Navigate to Jaeger
localhost:8080/jaeger
- Check that
agent and mcp have 2 disconnected traces.
👍 Expected behavior
traceparent (and any other configured propagator fields) should be injected into params._meta for every tool call made while a span is active, regardless of whether _meta was already present — merging into existing _meta rather than skipping injection when it's absent.
👎 Actual Behavior with Screenshots
For any tool call that doesn't already carry a _meta object (the default case), no trace context is attached to the outgoing request, and the client/server traces are permanently disconnected.
🤖 Python Version
3.14
📃 Provide any additional context for the Bug.
Suggested fix: create an empty Meta()/{} when meta is None (restoring the pre-#3179 fallback) and only avoid overwriting an existing traceparent/_meta value that's already set, rather than skipping the whole block. Something like:
if params:
meta = getattr(args[0].root.params, "meta", None)
if meta is None and params is not None:
meta = mcp.types.RequestParams.Meta()
args[0].root.params.meta = meta
if meta is not None and len(args) > 0:
carrier = {}
TraceContextTextMapPropagator().inject(carrier)
meta.traceparent = carrier["traceparent"]
Happy to open a PR with this fix if maintainers confirm the approach.
👀 Have you spent some time to check if this bug has been raised before?
Are you willing to submit PR?
Yes I am willing to submit a PR!
Which component is this bug for?
Traceloop SDK
📜 Description
McpInstrumentor.patch_mcp_client()(which wrapsmcp.shared.session.BaseSession.send_request) is supposed to inject atraceparentinto the outgoing JSON-RPC request's_metafield so that the MCP server can continue the client's trace. In practice this never happens for a normal tool call, because the injection is gated behind a check that only runs whenparams.metais already non-None:RequestParams.meta(aliased to_meta) defaults toNoneand is only populated by themcpSDK itself when a caller explicitly requests a progress token (progress_callbackargument). Nothing in the standardmcp.ClientSession.call_tool()path, norlangchain_mcp_adapters.tools.load_mcp_tools(), sets a progress token by default. So for an ordinary tool call,params.metaisNone,if meta:isFalse, and thetraceparentis silently never attached. The MCP server therefore starts a brand new trace instead of continuing the caller's trace.This is a regression introduced by #3179 ("fix(mcp): do not override meta pydantic types"). Before that PR, the code always created a
Meta()object when one was missing:#3179's stated goal was to stop clobbering an already-present
_meta(e.g. when a progress token was set) — but the fix went further than needed and removed the "create an emptyMeta()when absent" fallback entirely, instead of only skipping the overwrite case. That collapsed the common case (no pre-existing_meta) into a no-op.👟 Reproduction steps
Traceloop.init()(which enablesInstruments.MCPby default), talking over the Streamable HTTP transport.ClientSession.call_tool(...)(or alangchain_mcp_adapters-generated tool wrapper) — i.e. a call that does not pass aprogress_callback.params._metais absent.tools/callspan has no parent — it starts a new trace, instead of continuing the trace of the request that triggered the tool call.Reproduced against
traceloop-sdk==0.62.1/opentelemetry-instrumentation-mcp==0.62.1(current latest),mcp==1.28.1,fastmcp==3.4.4. Confirmed via two traces from a real deployment: the caller's trace ends at a{tool_name}.toolspan with no HTTP/transport child span, and the MCP server's trace root istools/call {tool_name}with no parent context.This can also be reproduced in the OpenTelemetry Demo:
.envand updateMCP_ENABLED=True.make start-agenticlocalhost:8080/chatbotlocalhost:8080/jaegeragentandmcphave 2 disconnected traces.👍 Expected behavior
traceparent(and any other configured propagator fields) should be injected intoparams._metafor every tool call made while a span is active, regardless of whether_metawas already present — merging into existing_metarather than skipping injection when it's absent.👎 Actual Behavior with Screenshots
For any tool call that doesn't already carry a
_metaobject (the default case), no trace context is attached to the outgoing request, and the client/server traces are permanently disconnected.🤖 Python Version
3.14
📃 Provide any additional context for the Bug.
Suggested fix: create an empty
Meta()/{}whenmetaisNone(restoring the pre-#3179 fallback) and only avoid overwriting an existingtraceparent/_metavalue that's already set, rather than skipping the whole block. Something like:Happy to open a PR with this fix if maintainers confirm the approach.
👀 Have you spent some time to check if this bug has been raised before?
Are you willing to submit PR?
Yes I am willing to submit a PR!