Skip to content

issue #4038 - fix(mcp): handle dict/object result shapes in stream writer error parsing#4039

Open
Shadow2121 wants to merge 1 commit intotraceloop:mainfrom
Shadow2121:main
Open

issue #4038 - fix(mcp): handle dict/object result shapes in stream writer error parsing#4039
Shadow2121 wants to merge 1 commit intotraceloop:mainfrom
Shadow2121:main

Conversation

@Shadow2121
Copy link
Copy Markdown

@Shadow2121 Shadow2121 commented Apr 21, 2026

  • I have added tests that cover my changes.
  • If adding a new instrumentation or changing an existing one, I've added screenshots from some observability platform showing the change.
  • PR name follows conventional commits format: feat(instrumentation): ... or fix(instrumentation): ....
  • (If applicable) I have updated the documentation accordingly.

Summary by CodeRabbit

Release Notes

  • Bug Fixes
    • Enhanced error handling in MCP stream communication to support diverse response data structures and formats. The instrumentation now properly detects and reports errors whether response data is provided as dictionaries or objects, improving overall reliability and preventing failures when processing different types of error responses from MCP services.

@CLAassistant
Copy link
Copy Markdown

CLA assistant check
Thank you for your submission! We really appreciate it. Like many open source projects, we ask that you sign our Contributor License Agreement before we can accept your contribution.
You have signed the CLA already but the status is still pending? Let us recheck it.

@coderabbitai
Copy link
Copy Markdown

coderabbitai Bot commented Apr 21, 2026

📝 Walkthrough

Walkthrough

Added helper methods to InstrumentedStreamWriter for safely extracting error information from heterogeneous result objects. Updated the send method to use these helpers to set span error status based on extracted error state, replacing direct indexing assumptions about result shape.

Changes

Cohort / File(s) Summary
Error Extraction Helpers
packages/opentelemetry-instrumentation-mcp/opentelemetry/instrumentation/mcp/instrumentation.py
Added _get_value static method for safe nested value extraction and _extract_result_error classmethod to safely determine error state and message from dict-like or attribute-based result objects. Updated send method to use these helpers when setting span error status, removing assumptions about result structure.
Result Error Tests
packages/opentelemetry-instrumentation-mcp/tests/test_fastmcp.py
Added three unit tests validating _extract_result_error behavior: dict-based result with error message, object-based result (via SimpleNamespace) with error message, and empty content returning no error message.

Estimated code review effort

🎯 2 (Simple) | ⏱️ ~12 minutes

Poem

🐰 A careful rabbit, defensive and wise,
Extracted the errors without surprise,
No crashes from dicts that behave differently,
With helpers so gentle and safe as can be,
The errors now whisper their truth to the trace! 🌿

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 0.00% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Title check ✅ Passed The title clearly summarizes the main change: fixing MCP stream writer error parsing to handle dict/object result shapes, which aligns with the actual code modifications.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

Copy link
Copy Markdown

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🧹 Nitpick comments (3)
packages/opentelemetry-instrumentation-mcp/tests/test_fastmcp.py (1)

156-183: Consider adding a negative-path test for isError=False.

The three new tests cover only isError=True branches. Adding a case where isError is absent/False (expecting (False, None)) would lock in the early-return behavior at line 559–560 and prevent regressions if the helper is later refactored.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@packages/opentelemetry-instrumentation-mcp/tests/test_fastmcp.py` around
lines 156 - 183, Add a negative-path unit test for
InstrumentedStreamWriter._extract_result_error that verifies when isError is
False or missing the function returns (False, None); create a new test similar
to the others (e.g., test_extract_result_error_when_not_error) that passes a
dict (and/or SimpleNamespace) with isError=False or without isError and asserts
is_error is False and message is None to lock in the early-return behavior of
_extract_result_error.
packages/opentelemetry-instrumentation-mcp/opentelemetry/instrumentation/mcp/instrumentation.py (2)

342-349: Duplicate error-extraction logic in _execute_and_handle_result.

This block uses the same direct-indexing pattern (result.content[0].text) that the new _extract_result_error helper was introduced to replace. If result arrives in dict shape here (same scenario motivating this PR), this path still breaks. Consider reusing the new helper for consistency and to fully close out issue #4038.

Proposed refactor
-            # Handle errors
-            if hasattr(result, "isError") and result.isError:
-                if len(result.content) > 0:
-                    span.set_status(
-                        Status(StatusCode.ERROR, f"{result.content[0].text}")
-                    )
-            else:
-                span.set_status(Status(StatusCode.OK))
+            # Handle errors
+            is_error, error_message = InstrumentedStreamWriter._extract_result_error(result)
+            if is_error:
+                span.set_status(Status(StatusCode.ERROR, error_message))
+            else:
+                span.set_status(Status(StatusCode.OK))
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In
`@packages/opentelemetry-instrumentation-mcp/opentelemetry/instrumentation/mcp/instrumentation.py`
around lines 342 - 349, Replace the direct-indexing error extraction in
_execute_and_handle_result with the new _extract_result_error helper: when
result has isError, call _extract_result_error(result) to obtain the error text
(falling back appropriately) and pass that string into
span.set_status(Status(StatusCode.ERROR, ...)); remove any use of
result.content[0].text so dict-shaped results are handled consistently with
_extract_result_error.

556-571: Strict is True check may miss truthy error flags; content[0] still assumes subscriptable.

Two minor robustness considerations:

  1. Line 558 uses is True, so truthy-but-non-bool values (e.g., 1, "true") won't trigger the error path. The MCP spec defines isError as a bool, so this is likely intentional, but a simple bool(...) coercion would be more forgiving of non-conforming servers — which is precisely the class of shape-variance this PR is addressing.
  2. Line 566 (content[0]) assumes content is indexable. If a server returns content as a non-list iterable or dict, this raises TypeError/KeyError. It's currently shielded by @dont_throw on send(), but the helper itself isn't, and the new unit tests call it directly — a malformed shape there would surface as a test/production exception rather than the graceful (True, None) fallback the rest of the function aims for.
Suggested hardening
     `@classmethod`
     def _extract_result_error(cls, result: Any) -> Tuple[bool, Union[str, None]]:
-        is_error = cls._get_value(result, "isError", False) is True
+        is_error = bool(cls._get_value(result, "isError", False))
         if not is_error:
             return False, None
 
         content = cls._get_value(result, "content", [])
-        if not content:
+        if not content or not isinstance(content, (list, tuple)):
             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)
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In
`@packages/opentelemetry-instrumentation-mcp/opentelemetry/instrumentation/mcp/instrumentation.py`
around lines 556 - 571, _extract_result_error is fragile: replace the strict "is
True" check on isError with a boolean coercion (e.g., is_error =
bool(cls._get_value(result, "isError", False))) so truthy non-bool values are
handled, and make the content-first-item access robust by not assuming
subscriptability — use a safe extraction strategy (e.g., if isinstance(content,
(list, tuple)) take content[0], else attempt first = next(iter(content)) in a
try/except or handle dict by taking next(iter(content.values()))), and fall back
to returning (True, None) on any extraction failure; keep references to
_extract_result_error, _get_value, is_error, content, and first_item so you
update the correct code paths.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Nitpick comments:
In
`@packages/opentelemetry-instrumentation-mcp/opentelemetry/instrumentation/mcp/instrumentation.py`:
- Around line 342-349: Replace the direct-indexing error extraction in
_execute_and_handle_result with the new _extract_result_error helper: when
result has isError, call _extract_result_error(result) to obtain the error text
(falling back appropriately) and pass that string into
span.set_status(Status(StatusCode.ERROR, ...)); remove any use of
result.content[0].text so dict-shaped results are handled consistently with
_extract_result_error.
- Around line 556-571: _extract_result_error is fragile: replace the strict "is
True" check on isError with a boolean coercion (e.g., is_error =
bool(cls._get_value(result, "isError", False))) so truthy non-bool values are
handled, and make the content-first-item access robust by not assuming
subscriptability — use a safe extraction strategy (e.g., if isinstance(content,
(list, tuple)) take content[0], else attempt first = next(iter(content)) in a
try/except or handle dict by taking next(iter(content.values()))), and fall back
to returning (True, None) on any extraction failure; keep references to
_extract_result_error, _get_value, is_error, content, and first_item so you
update the correct code paths.

In `@packages/opentelemetry-instrumentation-mcp/tests/test_fastmcp.py`:
- Around line 156-183: Add a negative-path unit test for
InstrumentedStreamWriter._extract_result_error that verifies when isError is
False or missing the function returns (False, None); create a new test similar
to the others (e.g., test_extract_result_error_when_not_error) that passes a
dict (and/or SimpleNamespace) with isError=False or without isError and asserts
is_error is False and message is None to lock in the early-return behavior of
_extract_result_error.

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: 863d7ad6-ec74-4b89-b955-175a7a366a25

📥 Commits

Reviewing files that changed from the base of the PR and between 3efa0eb and a2584e1.

📒 Files selected for processing (2)
  • packages/opentelemetry-instrumentation-mcp/opentelemetry/instrumentation/mcp/instrumentation.py
  • packages/opentelemetry-instrumentation-mcp/tests/test_fastmcp.py

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants