Skip to content

Commit d75e313

Browse files
fix: handle MCP list tools spans with GenAI semantics
- Add GenAIOperationName.LIST_TOOLS constant - Implement _get_attributes_from_mcp_list_tools_span_data to extract operation name, tool definitions, and output type - Update _extract_genai_attributes dispatch for MCPListToolsSpanData - Add test coverage for MCP list tools attribute extraction - Update CHANGELOG Fixes #4197
1 parent 804db2a commit d75e313

3 files changed

Lines changed: 43 additions & 8 deletions

File tree

instrumentation-genai/opentelemetry-instrumentation-openai-agents-v2/CHANGELOG.md

Lines changed: 1 addition & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -7,19 +7,14 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
77

88
## Unreleased
99

10-
### Added
11-
1210
- Align AgentSpanData test stubs and span processor with real OpenAI Agents SDK;
1311
remove non-existent `operation`, `description`, `agent_id`, and `model` fields.
1412
([#4229](https://github.com/open-telemetry/opentelemetry-python-contrib/pull/4229))
1513
- Document official package metadata and README for the OpenAI Agents instrumentation.
1614
([#3859](https://github.com/open-telemetry/opentelemetry-python-contrib/pull/3859))
1715
- Populate instructions and tool definitions from Response obj.
1816
([#4196](https://github.com/open-telemetry/opentelemetry-python-contrib/pull/4196))
19-
20-
### Fixed
21-
22-
- `opentelemetry-instrumentation-openai-agents`: Handle MCP list tools spans correctly.
17+
- `opentelemetry-instrumentation-openai-agents-v2`: Handle MCP list tools spans correctly.
2318
([#4508](https://github.com/open-telemetry/opentelemetry-python-contrib/pull/4508))
2419

2520
## Version 0.1.0 (2025-10-15)

instrumentation-genai/opentelemetry-instrumentation-openai-agents-v2/src/opentelemetry/instrumentation/openai_agents/span_processor.py

Lines changed: 23 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -122,6 +122,7 @@ class GenAIOperationName:
122122
SPEECH = "speech_generation"
123123
GUARDRAIL = "guardrail_check"
124124
HANDOFF = "agent_handoff"
125+
LIST_TOOLS = "list_tools"
125126
RESPONSE = "response" # internal aggregator in current processor
126127

127128
CLASS_FALLBACK = {
@@ -1159,6 +1160,8 @@ def _infer_output_type(self, span_data: Any) -> str:
11591160
if _is_instance_of(span_data, FunctionSpanData):
11601161
# Tool results are typically JSON
11611162
return GenAIOutputType.JSON
1163+
if _is_instance_of(span_data, MCPListToolsSpanData):
1164+
return GenAIOutputType.JSON
11621165
if _is_instance_of(span_data, TranscriptionSpanData):
11631166
return GenAIOutputType.TEXT
11641167
if _is_instance_of(span_data, SpeechSpanData):
@@ -1548,7 +1551,7 @@ def _get_operation_name(self, span_data: Any) -> str:
15481551
if _is_instance_of(span_data, HandoffSpanData):
15491552
return GenAIOperationName.HANDOFF
15501553
if _is_instance_of(span_data, MCPListToolsSpanData):
1551-
return "list_tools"
1554+
return GenAIOperationName.LIST_TOOLS
15521555
return "unknown"
15531556

15541557
def _extract_genai_attributes(
@@ -1609,6 +1612,10 @@ def _extract_genai_attributes(
16091612
yield from self._get_attributes_from_guardrail_span_data(span_data)
16101613
elif _is_instance_of(span_data, HandoffSpanData):
16111614
yield from self._get_attributes_from_handoff_span_data(span_data)
1615+
elif _is_instance_of(span_data, MCPListToolsSpanData):
1616+
yield from self._get_attributes_from_mcp_list_tools_span_data(
1617+
span_data
1618+
)
16121619

16131620
def _get_attributes_from_generation_span_data(
16141621
self, span_data: GenerationSpanData, payload: ContentPayload
@@ -2174,6 +2181,21 @@ def _get_attributes_from_handoff_span_data(
21742181
normalize_output_type(self._infer_output_type(span_data)),
21752182
)
21762183

2184+
def _get_attributes_from_mcp_list_tools_span_data(
2185+
self, span_data: MCPListToolsSpanData
2186+
) -> Iterator[tuple[str, AttributeValue]]:
2187+
"""Extract attributes from MCP list-tools spans."""
2188+
yield GEN_AI_OPERATION_NAME, GenAIOperationName.LIST_TOOLS
2189+
2190+
tools = getattr(span_data, "tools", None)
2191+
if self._capture_tool_definitions and tools:
2192+
yield GEN_AI_TOOL_DEFINITIONS, safe_json_dumps(tools)
2193+
2194+
yield (
2195+
GEN_AI_OUTPUT_TYPE,
2196+
normalize_output_type(self._infer_output_type(span_data)),
2197+
)
2198+
21772199
def _cleanup_spans_for_trace(self, trace_id: str) -> None:
21782200
"""Clean up spans for a trace to prevent memory leaks."""
21792201
spans_to_remove = [

instrumentation-genai/opentelemetry-instrumentation-openai-agents-v2/tests/test_z_span_processor_unit.py

Lines changed: 19 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -183,7 +183,10 @@ def test_operation_and_span_naming(processor_setup):
183183
)
184184

185185
mcp_data = MCPListToolsSpanData()
186-
assert processor._get_operation_name(mcp_data) == "list_tools"
186+
assert (
187+
processor._get_operation_name(mcp_data)
188+
== sp.GenAIOperationName.LIST_TOOLS
189+
)
187190

188191
class UnknownSpanData:
189192
pass
@@ -362,6 +365,21 @@ def __init__(self) -> None:
362365
assert function_attrs[sp.GEN_AI_TOOL_CALL_RESULT] == {"temperature": 70}
363366
assert function_attrs[sp.GEN_AI_OUTPUT_TYPE] == sp.GenAIOutputType.JSON
364367

368+
mcp_span = MCPListToolsSpanData(
369+
tools=[{"type": "function", "name": "lookup_weather"}]
370+
)
371+
mcp_attrs = _collect(
372+
processor._get_attributes_from_mcp_list_tools_span_data(mcp_span)
373+
)
374+
assert (
375+
mcp_attrs[sp.GEN_AI_OPERATION_NAME]
376+
== sp.GenAIOperationName.LIST_TOOLS
377+
)
378+
assert json.loads(mcp_attrs[sp.GEN_AI_TOOL_DEFINITIONS]) == [
379+
{"type": "function", "name": "lookup_weather"}
380+
]
381+
assert mcp_attrs[sp.GEN_AI_OUTPUT_TYPE] == sp.GenAIOutputType.JSON
382+
365383

366384
def test_extract_genai_attributes_unknown_type(processor_setup):
367385
processor, _ = processor_setup

0 commit comments

Comments
 (0)