Skip to content

OTel client spans: add server.address (and other semconv client attributes) so tool calls can be attributed to a server #3836

Open

Description

@mihai-dinculescu

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; tcp for HTTP, pipe for stdio), network.protocol.name / network.protocol.version (Recommended where applicable)
  • mcp.session.id, mcp.protocol.version (Recommended)
  • gen_ai.tool.name (Conditionally Required when the operation relates to a specific tool, i.e. every tools/call) and gen_ai.operation.name (Recommended). Both were added to the server-side OpenTelemetryMiddleware in 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:

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

Activity

  1. added
    enhancementRequest for a new feature that's not currently supported
    on Nov 8, 2026
  2. Pranjulchaurasiya commented on Nov 10, 2026

    @Pranjulchaurasiya

    Hi maintainers,

    I took a look at src/mcp/shared/jsonrpc_dispatcher.py (lines 387–391) and src/mcp/client/session.py. Currently, send_raw_request creates the SpanKind.CLIENT span with only:

    attributes={"mcp.method.name": method, "jsonrpc.request.id": str(request_id)}

    Because JSONRPCDispatcher is 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:

    1. Tool Semantic Conventions (in-place):
      When method == "tools/call" and isinstance(target, str) is present, add "gen_ai.tool.name": target directly to the span attributes (matching the server-side convention in OpenTelemetryMiddleware added in Add GenAI semantic-convention attributes to OpenTelemetryMiddleware #2970).

    2. Server & Transport Attribution (server.address, server.port, network.transport):
      Add an optional otel_attributes: Mapping[str, Any] | None = None parameter to JSONRPCDispatcher.__init__ (copied to an immutable dict), which merges into the base attributes of every outbound client span. Expose this through ClientSession(..., otel_attributes=...).

      • This allows client applications (and transport wrappers with explicit endpoint configurations) to supply server.address, server.port, and connection metadata today.
      • It remains fully forward-compatible with the future outbound middleware seam noted in the TODO(maxisbey) at line 384.
    3. Testing:
      Add unit tests in tests/shared/test_jsonrpc_dispatcher.py verifying that:

      • Span attributes include gen_ai.tool.name on tools/call.
      • Custom otel_attributes passed 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)

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    enhancementRequest for a new feature that's not currently supported

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions