Skip to content

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

@etserend

Description

@etserend

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 #4197 reported MCP list-tools spans showing as "unknown" in opentelemetry-instrumentation-openai-agents-v2, and PR #4629 added a per-instrumentation workaround rather than building on a shared foundation.

Describe the solution you'd like

Add MCPClientInvocation and MCPServerInvocation types to opentelemetry-util-genai, with lifecycle methods and context managers on TelemetryHandler, and four MCP-specific metric histograms.

MCPClientInvocation — covers all MCP operations initiated by the client side (mcp.client span type, SpanKind.CLIENT):

invocation = handler.start_mcp_client(
    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.tool_call_arguments = {...}   # opt-in
invocation.stop()

# context manager form
with handler.mcp_client(mcp_method_name="resources/read", mcp_resource_uri="file:///report.pdf") as inv:
    inv.mcp_session_id = "..."

MCPServerInvocation — covers all MCP operations processed on the server side (mcp.server span type, SpanKind.SERVER):

with handler.mcp_server(mcp_method_name="tools/call", tool_name="get_weather") as inv:
    inv.client_address = "192.0.2.1"
    inv.tool_call_result = {...}   # opt-in

Tool call operations (mcp.method.name = "tools/call") are handled by the same MCPClientInvocation / MCPServerInvocation types — the spec explicitly states that mcp.client/mcp.server spans with tools/call are compatible with GenAI execute_tool spans. The invocation type automatically sets gen_ai.operation.name = execute_tool when mcp_method_name == "tools/call", and SHOULD NOT set it for any other method.

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

Attributes (from spans.yaml and common.yaml):

Attribute Requirement level Client Server
mcp.method.name Required
gen_ai.tool.name Cond. req. when tool op
gen_ai.operation.name Recommended (execute_tool for tools/call only)
gen_ai.prompt.name Cond. req. when prompt op
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. for 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_client(self, *, mcp_method_name: str, tool_name=None, prompt_name=None,
                     server_address=None, server_port=None) -> MCPClientInvocation: ...

def mcp_client(self, *, mcp_method_name: str, ...) -> AbstractContextManager[MCPClientInvocation]: ...

def start_mcp_server(self, *, mcp_method_name: str, tool_name=None, prompt_name=None,
                     client_address=None, client_port=None) -> MCPServerInvocation: ...

def mcp_server(self, *, mcp_method_name: str, ...) -> AbstractContextManager[MCPServerInvocation]: ...

Metrics (from metrics.yaml):

Metric Unit Notes
mcp.client.operation.duration s All client-side MCP operations
mcp.server.operation.duration s All server-side MCP operations
mcp.client.session.duration s Recorded on initialize completion
mcp.server.session.duration s Recorded on initialize completion

InvocationMetricsRecorder extended with four new histograms in instruments.py. mcp.resource.uri included as opt-in metric dimension per spec.

Exports: MCPClientInvocation and MCPServerInvocation added to invocation.py __all__.

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 module-level string literals with a # TODO: Migrate once in semconv package comment. Is that acceptable here?

  2. Session duration: mcp.client/server.session.duration should be recorded when the session ends. Should the invocation type detect mcp_method_name == "initialize" and record session duration automatically on stop(), or should this be caller-driven via metric_attributes?

  3. mcp.resource.uri on metrics: Spec marks it as opt-in for operation duration. Should this be 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 a PR once we have alignment on the questions above.

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type
    No fields configured for issues without a type.

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions