open-telemetry / open-telemetry/opentelemetry-python-genai
opentelemetry-util-genai: add MCP invocation types and TelemetryHandler support
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:
MCPInvocation—is_client=True→mcp.clientspan /SpanKind.CLIENT;is_client=False→mcp.serverspan /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
-
Semconv constants:
mcp.*andjsonrpc.*attributes are not yet in theopentelemetry-semantic-conventionsPython package. The established pattern (see_GEN_AI_AGENT_VERSIONin_agent_invocation.py) is to use module-level string literals with a# TODO: Migrate once in semconv packagecomment. Is that acceptable here? -
Session duration trigger:
mcp.client/server.session.durationshould be recorded when the session ends. ShouldMCPInvocationdetectmcp_method_name == "initialize"and record session duration automatically on stop, or should this be left to the caller viametric_attributes? -
mcp.resource.urion 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 viametric_attributes?
References
- Semconv spans:
model/mcp/spans.yaml - Semconv common attrs:
model/mcp/common.yaml - Semconv metrics:
model/mcp/metrics.yaml - Semconv registry (
mcp.method.nameenum with 27 values):model/mcp/registry.yaml - Related bug: open-telemetry/opentelemetry-python-contrib#4197
- Related workaround: open-telemetry/opentelemetry-python-contrib#4629
Would you like to implement this?
Yes — we plan to open PR 1 (spans) once we have alignment on the questions above.
Contributor guide
First steps
- Read the whole issue, then the project's contributing guide.
- Comment on the issue to say you are picking it up — it saves two people doing the same work.
- Fork the repository and make your change on a branch.
- 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