Python: feat(python): Add MCP client OTel spans per GenAI semantic conventions#6349
Conversation
Python Test Coverage Report •
Python Unit Test Overview
|
|||||||||||||||||||||||||||||||||||
There was a problem hiding this comment.
Pull request overview
Adds Python MCP client tracing that follows the OpenTelemetry GenAI MCP semantic conventions, aiming to consistently emit (or enrich) CLIENT spans for MCP operations and preserve MCP-specific error typing (e.g., error.type="tool_error").
Changes:
- Introduces MCP span helper utilities in
observability.py(create/enrich/error helpers + active tool span contextvar). - Instruments MCP client operations in
_mcp.py(initialize,tools/list,prompts/list,tools/call,prompts/get) including transport attributes and retries fortools/call. - Updates tool invocation tracing in
_tools.pyto expose the activeexecute_toolspan for MCP enrichment and to preserve MCP error typing; adds a dedicated MCP observability test suite.
Reviewed changes
Copilot reviewed 4 out of 4 changed files in this pull request and generated 6 comments.
| File | Description |
|---|---|
| python/packages/core/agent_framework/observability.py | Adds MCP tracing helpers and MCP semantic convention attributes. |
| python/packages/core/agent_framework/_mcp.py | Instruments MCP operations with CLIENT spans, enrichment logic, transport attrs, and error helpers. |
| python/packages/core/agent_framework/_tools.py | Publishes the active execute_tool span via contextvar and preserves MCP-specific error.type. |
| python/packages/core/tests/core/test_mcp_observability.py | Adds tests validating span creation/enrichment, error typing, and transport attributes. |
There was a problem hiding this comment.
Automated Code Review
Reviewers: 3 | Confidence: 86%
✓ Security Reliability
The PR is well-structured from a security and reliability standpoint. The contextvar
_ACTIVE_TOOL_EXECUTION_SPANlifecycle is properly managed (set after span creation, reset infinally), URL parsing is defensively wrapped in try/except, span creation is gated behindOBSERVABILITY_SETTINGS.ENABLED, and the_otel_error_typepropagation mechanism correctly preserves MCP-specific error types throughFunctionTool.invoke()despitecapture_exceptionoverwriting. No injection risks, secrets, unsafe deserialization, or resource leaks were identified. There is one minor observability gap: reconnection failure paths in both_call_tool_with_retriesandget_promptdo not callset_mcp_span_error, meaning spans created via the new-span path (direct calls without FunctionTool wrapper) will end without error status when reconnection fails, sinceset_status_on_exception=False.
✓ Test Coverage
The PR adds 19 well-structured tests covering the happy paths for all 5 MCP span operations, span enrichment vs creation, transport attributes, and the observability-disabled gate. The test assertions are meaningful (checking span kind, attributes, status codes) and mocks are appropriate. However, there are notable test coverage gaps: the
get_prompterror paths (3 explicitset_mcp_span_errorcalls) have zero test coverage, and the_call_tool_with_retriesnon-tool_error error paths (generic exceptions, exhausted retries) are also untested. Since thetools/calltool_error path IS tested, the asymetry withprompts/geterror handling is a meaningful gap.
✓ Design Approach
The MCP span coverage looks broadly aligned with the new tests, but there is one design regression in the
tools/callenrichment path: when an MCP tool is exposed through a prefixed/localFunctionToolname, the new code overwrites the existingexecute_toolspan'sgen_ai.tool.namewith the remote MCP name, so the span no longer identifies the tool the framework actually executed.
Automated review by TaoChenOSU's agents
Implement MCP client spans per the OTel GenAI Semantic Conventions for MCP (https://opentelemetry.io/docs/specs/semconv/gen-ai/mcp/#client). Operations instrumented: - initialize: CLIENT span capturing MCP session setup - tools/list: CLIENT span for tool listing (per-page) - prompts/list: CLIENT span for prompt listing (per-page) - tools/call: CLIENT span (nested under execute_tool when called via FunctionTool) - prompts/get: CLIENT span Span attributes follow the MCP semantic conventions: - Required: mcp.method.name - Conditional: error.type, gen_ai.tool.name, gen_ai.prompt.name - Recommended: gen_ai.operation.name, mcp.protocol.version, mcp.session.id, network.transport, server.address, server.port Transport-specific attributes per subclass: - MCPStdioTool: network.transport=pipe - MCPStreamableHTTPTool: network.transport=tcp, network.protocol.name=http - MCPWebsocketTool: network.transport=tcp, network.protocol.name=websocket All span creation gated behind OBSERVABILITY_SETTINGS.ENABLED. Closes #3624 Closes #4697 Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
4bd2fff to
df32fc0
Compare
…ersion caching - Always create nested CLIENT spans for tools/call instead of enriching the parent execute_tool span - Remove _ACTIVE_TOOL_EXECUTION_SPAN contextvar (no longer needed) - Remove enrich_span_with_mcp_attributes() helper - Remove _otel_error_type preservation in FunctionTool.invoke() - Remove _mcp_protocol_version instance variable; protocol version is only set on the initialize span where it is available Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Remove record_exception=False and set_status_on_exception=False from create_mcp_client_span. Let OTel handle exception recording and status setting automatically. The manual set_mcp_span_error calls for tools/call still correctly set error.type (which OTel's automatic handling doesn't touch), so tool_error is preserved. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
- Fix initialize test to call mocked session.initialize() and read protocolVersion from the result instead of hardcoding it - Add tools/call McpError error-path test - Add prompts/get McpError error-path test Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Motivation and Context
Implements MCP client spans per the OTel GenAI Semantic Conventions for MCP, giving users visibility into MCP operations (initialize, tool listing, tool calls, prompt operations) through OpenTelemetry tracing.
Closes #3624
Closes #4697
Description
Adds CLIENT spans for five MCP operations:
initializeinitializemcp.protocol.versionfrom resulttools/listtools/listprompts/listprompts/listtools/calltools/call {tool_name}execute_toolwhen called viaFunctionTool; setserror.type="tool_error"forisErrorresults per specprompts/getprompts/get {prompt_name}Span attributes follow the MCP semantic conventions:
mcp.method.nameerror.type,gen_ai.tool.name,gen_ai.prompt.namegen_ai.operation.name,mcp.protocol.version,mcp.session.id,network.transport,server.address,server.portTransport-specific attributes per subclass:
network.transportnetwork.protocol.nameserver.address/portMCPStdioToolpipeMCPStreamableHTTPTooltcphttpMCPWebsocketTooltcpwebsocketAll span creation is gated behind
OBSERVABILITY_SETTINGS.ENABLED.Files changed:
observability.py— NewOtelAttrentries for MCP attributes,mcp_tracer(),create_mcp_client_span(),set_mcp_span_error()helpers_mcp.py— Instrumented all 5 operations,_mcp_base_span_attributes()with transport overrides, extracted_call_tool_with_retries()_tools.py— No changes (reverted earlier modifications)test_mcp_observability.py— 15 new tests covering all MCP span operationsContribution Checklist