Skip to content

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

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

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions