Repository navigation
Description
Description
The MCP send <method> [<target>] CLIENT span created in JSONRPCDispatcher.send_raw_request only carries mcp.method.name and jsonrpc.request.id:
# src/mcp/shared/jsonrpc_dispatcher.py (v2.3.0 / main)
with otel_span(
span_name,
kind=SpanKind.CLIENT,
attributes={"mcp.method.name": method, "jsonrpc.request.id": str(request_id)},
) as span:A client connected to several MCP servers therefore can't tell from its own telemetry which server a call went to. The span name includes the tool name, but tool names aren't unique across servers, and when the server doesn't emit traces (third-party hosted servers, servers without OTel) the client span is the only record of the call.
Use case. I run Open WebUI with three MCP servers (a Playwright-based fetch server, a smart-home server, and a hosted third-party server). Tempo's span-metrics processor turns the MCP send tools/call <tool> spans into per-tool call counts and latency in Grafana. I'd like to group/filter those by server, but the only option today is guessing from tool-name patterns. Only one of the three servers emits its own SERVER spans, so following child spans doesn't help for the others.
What the semantic conventions say. The GenAI MCP semantic conventions define these on MCP client spans, none of which are currently set:
server.address/server.port(Recommended; for client spans, the address of the server behind any intermediaries, if known)network.transport(Recommended;tcpfor HTTP,pipefor stdio),network.protocol.name/network.protocol.version(Recommendedwhere applicable)mcp.session.id,mcp.protocol.version(Recommended)gen_ai.tool.name(Conditionally Requiredwhen the operation relates to a specific tool, i.e. everytools/call) andgen_ai.operation.name(Recommended). Both were added to the server-sideOpenTelemetryMiddlewarein Add GenAI semantic-convention attributes to OpenTelemetryMiddleware #2970 but are still missing on the client span.
server.address / server.port are the important ones here: with them, a span-metrics or service-graph processor can break tool calls down by server with no client-side changes.
Implementation note. I realise the dispatcher is transport-agnostic and doesn't know the URL. The TODO right there (move the otel span + inject into an outbound middleware once that seam exists) and #3715 point in the same direction. Some options:
- The transport could expose connection metadata (address, port, transport kind) to the session/dispatcher when it's created, and the dispatcher adds it to every outbound span. For Streamable HTTP this comes from the URL; for stdio,
server.addressdoesn't apply butnetwork.transport=pipe(and perhaps the command name) does. - Or this lands as part of the client-side outbound seam proposed in Client: expose an outbound request seam (the send_raw_request TODO) so observers can see a request before it is written #3715.
A smaller stopgap that would also help: an optional, user-supplied set of extra span attributes on Client / ClientSession (e.g. Client(..., otel_attributes={"server.address": ...})), so applications like Open WebUI can tag spans with the connection they already know about.
References
- GenAI MCP semantic conventions, client span attributes: https://github.com/open-telemetry/semantic-conventions-genai/blob/main/docs/gen-ai/mcp.md
- Add richer OTel MCP span attributes #2387: open PR adding
rpc.*,mcp.resource.uri,mcp.session.idto client/server spans (related, but no server identity) - Add GenAI semantic-convention attributes to OpenTelemetryMiddleware #2970: merged, GenAI semconv attributes on the server-side middleware only
- Client: expose an outbound request seam (the send_raw_request TODO) so observers can see a request before it is written #3715: client outbound request seam, the natural home for transport-aware span enrichment
- Middleware Support in MCP #1233: middleware support
Activity
Pranjulchaurasiya commentedon Nov 10, 2026
Hi maintainers,
I took a look at
src/mcp/shared/jsonrpc_dispatcher.py(lines 387–391) andsrc/mcp/client/session.py. Currently,send_raw_requestcreates theSpanKind.CLIENTspan with only:Because
JSONRPCDispatcheris transport-agnostic and outbound middleware (#3715) is still a work-in-progress, here is a minimal, non-invasive approach to resolve the client telemetry gap without pre-empting the architectural middleware seam:Tool Semantic Conventions (in-place):
When
method == "tools/call"andisinstance(target, str)is present, add"gen_ai.tool.name": targetdirectly to the span attributes (matching the server-side convention inOpenTelemetryMiddlewareadded in Add GenAI semantic-convention attributes to OpenTelemetryMiddleware #2970).Server & Transport Attribution (
server.address,server.port,network.transport):Add an optional
otel_attributes: Mapping[str, Any] | None = Noneparameter toJSONRPCDispatcher.__init__(copied to an immutable dict), which merges into the base attributes of every outbound client span. Expose this throughClientSession(..., otel_attributes=...).server.address,server.port, and connection metadata today.TODO(maxisbey)at line 384.Testing:
Add unit tests in
tests/shared/test_jsonrpc_dispatcher.pyverifying that:gen_ai.tool.nameontools/call.otel_attributespassed to the session/dispatcher are correctly merged onto client spans without leaking into unrelated spans.If this bounded approach fits the SDK's direction, I would be glad to be assigned and submit a focused PR with tests.
(Prepared with AI assistance)