diff --git a/packages/opentelemetry-instrumentation-mcp/opentelemetry/instrumentation/mcp/fastmcp_instrumentation.py b/packages/opentelemetry-instrumentation-mcp/opentelemetry/instrumentation/mcp/fastmcp_instrumentation.py index d603a670a..65ff73571 100644 --- a/packages/opentelemetry-instrumentation-mcp/opentelemetry/instrumentation/mcp/fastmcp_instrumentation.py +++ b/packages/opentelemetry-instrumentation-mcp/opentelemetry/instrumentation/mcp/fastmcp_instrumentation.py @@ -89,6 +89,11 @@ async def traced_method(wrapped, instance, args, kwargs): if self._server_name: mcp_span.set_attribute(SpanAttributes.TRACELOOP_WORKFLOW_NAME, self._server_name) + # Add MCP_REQUEST_ID to the parent span + import time + request_id = str(int(time.time() * 1000)) # milliseconds + mcp_span.set_attribute(SpanAttributes.MCP_REQUEST_ID, request_id) + # Create nested tool span span_name = f"{entity_name}.tool" with self._tracer.start_as_current_span(span_name) as tool_span: @@ -112,27 +117,47 @@ async def traced_method(wrapped, instance, args, kwargs): try: result = await wrapped(*args, **kwargs) - # Add output in traceloop format to tool span - if self._should_send_prompts() and result: + # Always add response to MCP span regardless of content tracing setting + if result: try: - # Convert FastMCP Content objects to serializable format - output_data = [] - for item in result: - if hasattr(item, 'text'): - output_data.append({"type": "text", "content": item.text}) - elif hasattr(item, '__dict__'): - output_data.append(item.__dict__) + # Handle FastMCP ToolResult object + if hasattr(result, 'content') and result.content: + # Convert FastMCP Content objects to serializable format + output_data = [] + for item in result.content: + if hasattr(item, 'text'): + output_data.append({"type": "text", "content": item.text}) + elif hasattr(item, '__dict__'): + output_data.append(item.__dict__) + else: + output_data.append(str(item)) + + json_output = json.dumps(output_data, cls=self._get_json_encoder()) + truncated_output = self._truncate_json_if_needed(json_output) + else: + # Handle other result types + if hasattr(result, '__dict__'): + # Convert object to dict + result_dict = {} + for key, value in result.__dict__.items(): + if not key.startswith('_'): + result_dict[key] = str(value) + json_output = json.dumps(result_dict, cls=self._get_json_encoder()) + truncated_output = self._truncate_json_if_needed(json_output) else: - output_data.append(str(item)) - - json_output = json.dumps(output_data, cls=self._get_json_encoder()) - truncated_output = self._truncate_json_if_needed(json_output) - tool_span.set_attribute(SpanAttributes.TRACELOOP_ENTITY_OUTPUT, truncated_output) + # Fallback to string representation + truncated_output = str(result) - # Also add response to MCP span + # Add response to MCP span mcp_span.set_attribute(SpanAttributes.MCP_RESPONSE_VALUE, truncated_output) + + # Also add to tool span if content tracing is enabled + if self._should_send_prompts(): + tool_span.set_attribute(SpanAttributes.TRACELOOP_ENTITY_OUTPUT, truncated_output) + except (TypeError, ValueError): - pass # Skip output logging if serialization fails + # Fallback: add raw result as string + mcp_span.set_attribute(SpanAttributes.MCP_RESPONSE_VALUE, str(result)) tool_span.set_status(Status(StatusCode.OK)) mcp_span.set_status(Status(StatusCode.OK)) diff --git a/packages/opentelemetry-instrumentation-mcp/tests/test_fastmcp.py b/packages/opentelemetry-instrumentation-mcp/tests/test_fastmcp.py index 93595192e..8778f60b2 100644 --- a/packages/opentelemetry-instrumentation-mcp/tests/test_fastmcp.py +++ b/packages/opentelemetry-instrumentation-mcp/tests/test_fastmcp.py @@ -23,8 +23,8 @@ def get_greeting() -> str: # Test tool calling result = await client.call_tool("add_numbers", {"a": 5, "b": 3}) - assert len(result) == 1 - assert result[0].text == "8" + assert len(result.content) == 1 + assert result.content[0].text == "8" # Test resource listing resources_res = await client.list_resources() diff --git a/packages/opentelemetry-instrumentation-mcp/tests/test_fastmcp_server_span.py b/packages/opentelemetry-instrumentation-mcp/tests/test_fastmcp_server_span.py index b1d585856..80876cf5c 100644 --- a/packages/opentelemetry-instrumentation-mcp/tests/test_fastmcp_server_span.py +++ b/packages/opentelemetry-instrumentation-mcp/tests/test_fastmcp_server_span.py @@ -14,8 +14,8 @@ async def test_tool(x: int) -> int: async with Client(server) as client: # Test tool calling result = await client.call_tool("test_tool", {"x": 5}) - assert len(result) == 1 - assert result[0].text == "10" + assert len(result.content) == 1 + assert result.content[0].text == "10" # Get the finished spans spans = span_exporter.get_finished_spans()