Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -547,6 +547,29 @@ async def __aenter__(self) -> Any:
async def __aexit__(self, exc_type: Any, exc_value: Any, traceback: Any) -> Any:
return await self.__wrapped__.__aexit__(exc_type, exc_value, traceback)

@staticmethod
def _get_value(data: Any, key: str, default: Any = None) -> Any:
if isinstance(data, dict):
return data.get(key, default)
return getattr(data, key, default)

@classmethod
def _extract_result_error(cls, result: Any) -> Tuple[bool, Union[str, None]]:
is_error = cls._get_value(result, "isError", False) is True
if not is_error:
return False, None

content = cls._get_value(result, "content", [])
if not content:
return True, None

first_item = content[0]
message = cls._get_value(first_item, "text")
if message is None:
return True, None

return True, str(message)

@dont_throw
async def send(self, item: Any) -> Any:
from mcp.types import JSONRPCMessage, JSONRPCRequest
Expand All @@ -567,14 +590,9 @@ async def send(self, item: Any) -> Any:
span.set_attribute(
SpanAttributes.MCP_RESPONSE_VALUE, f"{serialize(request.result)}"
)
if "isError" in request.result:
if request.result["isError"] is True:
span.set_status(
Status(
StatusCode.ERROR,
f"{request.result['content'][0]['text']}",
)
)
is_error, error_message = self._extract_result_error(request.result)
if is_error:
span.set_status(Status(StatusCode.ERROR, error_message))
if hasattr(request, "id"):
span.set_attribute(SpanAttributes.MCP_REQUEST_ID, f"{request.id}")

Expand Down
35 changes: 35 additions & 0 deletions packages/opentelemetry-instrumentation-mcp/tests/test_fastmcp.py
Original file line number Diff line number Diff line change
@@ -1,3 +1,8 @@
from types import SimpleNamespace

from opentelemetry.instrumentation.mcp.instrumentation import InstrumentedStreamWriter


async def test_fastmcp_instrumentor(span_exporter, tracer_provider) -> None:
from fastmcp import FastMCP, Client

Expand Down Expand Up @@ -146,3 +151,33 @@ def get_greeting() -> str:
assert workflow_name == 'test-server.mcp', (
f"Expected workflow name 'test-server.mcp' on tool span, got '{workflow_name}'"
)


def test_extract_result_error_with_dict_shape():
result = {"isError": True, "content": [{"text": "dict error"}]}

is_error, message = InstrumentedStreamWriter._extract_result_error(result)

assert is_error is True
assert message == "dict error"


def test_extract_result_error_with_object_shape():
result = SimpleNamespace(
isError=True,
content=[SimpleNamespace(text="object error")],
)

is_error, message = InstrumentedStreamWriter._extract_result_error(result)

assert is_error is True
assert message == "object error"


def test_extract_result_error_with_missing_content():
result = {"isError": True, "content": []}

is_error, message = InstrumentedStreamWriter._extract_result_error(result)

assert is_error is True
assert message is None