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.client span / SpanKind.CLIENT; is_client=False → mcp.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
-
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?
-
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?
-
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.
What problem do you want to solve?
opentelemetry-util-genaiprovides reusable invocation types (ToolInvocation,AgentInvocation,WorkflowInvocation, etc.) and aTelemetryHandlerwith 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) andmcp.server(SpanKind: SERVER) — along with four metrics histograms. Without MCP types inopentelemetry-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"inopentelemetry-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
MCPInvocationtype toopentelemetry-util-genai, wired intoTelemetryHandleras a factory method and context manager — following the same pattern established forToolInvocationandAgentInvocation.The two MCP span kinds (
mcp.client/mcp.server) share all attributes exceptserver.address/server.port(client-side) vsclient.address/client.port(server-side). A single type with anis_client: boolfield (defaultTrue) is sufficient to driveSpanKind.CLIENTvsSpanKind.SERVERselection, keeping the API surface minimal.MVP scope (PR 1 — spans)
New file:
_mcp_invocation.pyOne class inheriting
GenAIInvocation:MCPInvocation—is_client=True→mcp.clientspan /SpanKind.CLIENT;is_client=False→mcp.serverspan /SpanKind.SERVERAccepts
mcp_method_nameat construction time (used for the span name andmcp.method.nameattribute) plus optional fields populated after start:Span name:
{mcp.method.name} {target}where target isgen_ai.tool.nameorgen_ai.prompt.namewhen applicable; plain{mcp.method.name}otherwise.gen_ai.operation.name: set toexecute_toolonly whenmcp_method_name == "tools/call", SHOULD NOT be set for other operations.Attributes covered (from
spans.yaml+common.yaml):mcp.method.namegen_ai.tool.namegen_ai.operation.nameexecute_toolfortools/call)gen_ai.prompt.nameerror.typemcp.protocol.versionrpc.response.status_codemcp.session.idmcp.resource.urijsonrpc.request.idnetwork.transport/network.protocol.*jsonrpc.protocol.version2.0)server.address/server.portclient.address/client.portgen_ai.tool.call.argumentsgen_ai.tool.call.resultTelemetryHandleradditions:invocation.py: exportMCPInvocationin__all__.Tests (
test_handler_mcp.py): span name format,SpanKinddriven byis_client, required attributes,execute_toolonly ontools/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:mcp.client.operation.durationsmcp.server.operation.durationsmcp.client.session.durations(recorded oninitializecompletion)mcp.server.session.durations(recorded oninitializecompletion)This requires extending
InvocationMetricsRecorderwith four new histograms ininstruments.py, following the same pattern as PR open-telemetry/opentelemetry-python-contrib#4443 (which added metrics toToolInvocation).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
model/mcp/spans.yamlmodel/mcp/common.yamlmodel/mcp/metrics.yamlmcp.method.nameenum with 27 values):model/mcp/registry.yamlWould you like to implement this?
Yes — we plan to open PR 1 (spans) once we have alignment on the questions above.