open-telemetry / open-telemetry/opentelemetry-python-genai

opentelemetry-util-genai: add MCP invocation types and TelemetryHandler support

Open
#94 2 comments 0 reactions 0 assignees View on GitHub

Nobody has claimed this yet.

Dominant language
Python
Stars
39
Forks
63
Avg merge
1d 15h
Merged PRs (30d)
175

Description

What problem do you want to solve?

opentelemetry-util-genai provides reusable invocation types (ToolInvocation, AgentInvocation, WorkflowInvocation, etc.) and a TelemetryHandler with lifecycle methods and context managers for each. There is currently no equivalent for Model Context Protocol (MCP) operations.

The GenAI semconv MCP spec (status: Development, merged January 2026) defines two canonical span types — mcp.client (SpanKind: CLIENT) and mcp.server (SpanKind: SERVER) — along with four metrics histograms. Without MCP types in opentelemetry-util-genai, each MCP SDK instrumentation must independently re-implement the same span and metric plumbing, risking drift from the semconv.

This was observed in practice: issue open-telemetry/opentelemetry-python-contrib#4197 reported MCP list-tools spans showing as "unknown" in opentelemetry-instrumentation-openai-agents-v2, and PR open-telemetry/opentelemetry-python-contrib#4629 added a per-instrumentation workaround rather than a shared foundation.

Describe the solution you'd like

Add a single MCPInvocation type to opentelemetry-util-genai, wired into TelemetryHandler as a factory method and context manager — following the same pattern established for ToolInvocation and AgentInvocation.

The two MCP span kinds (mcp.client / mcp.server) share all attributes except server.address/server.port (client-side) vs client.address/client.port (server-side). A single type with an is_client: bool field (default True) is sufficient to drive SpanKind.CLIENT vs SpanKind.SERVER selection, keeping the API surface minimal.

MVP scope (PR 1 — spans)

New file: _mcp_invocation.py

One class inheriting GenAIInvocation:

  • MCPInvocationis_client=Truemcp.client span / SpanKind.CLIENT; is_client=Falsemcp.server span / SpanKind.SERVER

Accepts mcp_method_name at construction time (used for the span name and mcp.method.name attribute) plus optional fields populated after start:

# SpanKind.CLIENT (default)
invocation = handler.start_mcp(
    mcp_method_name="tools/call",
    tool_name="get_weather",
    server_address="mcp.example.com",
    server_port=443,
)
invocation.mcp_session_id = "..."
invocation.mcp_protocol_version = "2025-06-18"
invocation.jsonrpc_request_id = "1"
invocation.stop()

# SpanKind.SERVER
invocation = handler.start_mcp(
    mcp_method_name="tools/call",
    tool_name="get_weather",
    is_client=False,
    client_address="10.0.0.1",
    client_port=54321,
)
invocation.stop()

# context manager form
with handler.mcp(mcp_method_name="tools/list") as inv:
    inv.mcp_session_id = "..."

Span name: {mcp.method.name} {target} where target is gen_ai.tool.name or gen_ai.prompt.name when applicable; plain {mcp.method.name} otherwise.

gen_ai.operation.name: set to execute_tool only when mcp_method_name == "tools/call", SHOULD NOT be set for other operations.

Attributes covered (from spans.yaml + common.yaml):

Attribute Level is_client=True is_client=False
mcp.method.name Required
gen_ai.tool.name Cond. req. (tool ops)
gen_ai.operation.name Recommended (execute_tool for tools/call)
gen_ai.prompt.name Cond. req. (prompt ops)
error.type Cond. req. on error
mcp.protocol.version Recommended
rpc.response.status_code Cond. req.
mcp.session.id Recommended
mcp.resource.uri Cond. req. (resource ops)
jsonrpc.request.id Cond. req.
network.transport / network.protocol.* Recommended
jsonrpc.protocol.version Recommended (when ≠ 2.0)
server.address / server.port Recommended
client.address / client.port Recommended
gen_ai.tool.call.arguments Opt-in
gen_ai.tool.call.result Opt-in

TelemetryHandler additions:

def start_mcp(
    self,
    *,
    mcp_method_name: str,
    tool_name: str | None = None,
    prompt_name: str | None = None,
    is_client: bool = True,
    server_address: str | None = None,
    server_port: int | None = None,
    client_address: str | None = None,
    client_port: int | None = None,
) -> MCPInvocation: ...

def mcp(
    self,
    *,
    mcp_method_name: str,
    is_client: bool = True,
    ...
) -> AbstractContextManager[MCPInvocation]: ...

invocation.py: export MCPInvocation in __all__.

Tests (test_handler_mcp.py): span name format, SpanKind driven by is_client, required attributes, execute_tool only on tools/call, all optional attributes, context manager happy and error paths, span status on error.

Deferred to follow-up (PR 2 — metrics)

Four histograms from metrics.yaml:

Metric Unit
mcp.client.operation.duration s
mcp.server.operation.duration s
mcp.client.session.duration s (recorded on initialize completion)
mcp.server.session.duration s (recorded on initialize completion)

This requires extending InvocationMetricsRecorder with four new histograms in instruments.py, following the same pattern as PR open-telemetry/opentelemetry-python-contrib#4443 (which added metrics to ToolInvocation).

Questions for maintainers
  1. Semconv constants: mcp.* and jsonrpc.* attributes are not yet in the opentelemetry-semantic-conventions Python package. The established pattern (see _GEN_AI_AGENT_VERSION in _agent_invocation.py) is to use module-level string literals with a # TODO: Migrate once in semconv package comment. Is that acceptable here?

  2. Session duration trigger: mcp.client/server.session.duration should be recorded when the session ends. Should MCPInvocation detect mcp_method_name == "initialize" and record session duration automatically on stop, or should this be left to the caller via metric_attributes?

  3. mcp.resource.uri on metrics: The spec marks it as opt-in for the operation duration metric. Should this be controlled by a flag on the invocation (e.g. include_resource_uri_in_metrics: bool = False) or left to the caller via metric_attributes?

References
Would you like to implement this?

Yes — we plan to open PR 1 (spans) once we have alignment on the questions above.

Contributor guide

Open the contributing guide

First steps

  1. Read the whole issue, then the project's contributing guide.
  2. Comment on the issue to say you are picking it up — it saves two people doing the same work.
  3. Fork the repository and make your change on a branch.
  4. Open a pull request that references the issue number.

Research direction

Start by reading the existing ToolInvocation and AgentInvocation patterns, then inspect _mcp_invocation.py, TelemetryHandler, and invocation.py. Use the referenced MCP spans.yaml and common.yaml to confirm the span attributes and client/server behavior, and run test_handler_mcp.py while covering the listed span cases. Done for PR 1 means spans and tests are complete; metrics in instruments.py are explicitly deferred.

Written by the indexing model from the issue text.

Assessment

Tech stack
python
Domain
observability-sre
Issue type
Feature
Difficulty
4/5
Estimated time
3-5 days
Activity status
Quiet
Clarity
Mostly clear
Newbie friendliness
55/100

Get new issues in your inbox

A short digest of beginner-friendly GitHub issues.