Skip to content

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

@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 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.

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type
    No fields configured for issues without a type.

    Projects

    Status
    New issues

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions