Skip to content

fix(mcp): tolerate non-dict result shapes in InstrumentedStreamWriter.send (#4038) - #4396

Open
Anai-Guo wants to merge 1 commit into
traceloop:mainfrom
Anai-Guo:fix/mcp-stream-writer-result-shapes-4038
Open

fix(mcp): tolerate non-dict result shapes in InstrumentedStreamWriter.send (#4038)#4396
Anai-Guo wants to merge 1 commit into
traceloop:mainfrom
Anai-Guo:fix/mcp-stream-writer-result-shapes-4038

Conversation

@Anai-Guo

@Anai-Guo Anai-Guo commented Aug 2, 2026

Copy link
Copy Markdown

Fixes #4038.

The bug

InstrumentedStreamWriter.send() reads request.result as a dict and assumes the
first content block is a text block:

if "isError" in request.result:
    if request.result["isError"] is True:
        span.set_status(Status(StatusCode.ERROR, f"{request.result['content'][0]['text']}"))

send() is decorated with @dont_throw, so anything raised here is swallowed and
logged at DEBUG — which means await self.__wrapped__.send(item) on the last line is
never reached and the MCP message is silently dropped. The span is also left with
the instrumentation's own exception as its status description, masking the real tool
error.

Four shapes hit this, all reachable from a normal MCP server:

request.result raised outcome on main
isError with an image/resource first content block KeyError: 'text' message dropped
isError with an empty content list IndexError message dropped
isError with no content key KeyError: 'content' message dropped
object-shaped result (attribute access) TypeError: ... is not iterable message dropped

CallToolResult.content is a list of ContentBlock, and only TextContent carries
.text — so an error result whose content is an image is enough to lose the response.

The fix

Two small module-level helpers read the result defensively for both dict- and
object-shaped payloads, and the error description falls back to "" rather than
raising. The isError semantics are unchanged (is True, as before).

This also lines the writer path up with _execute_and_handle_result
(instrumentation.py:343, :420), which already uses hasattr(result, "isError")
the inconsistency the issue notes.

Verification

Ran against the released opentelemetry-instrumentation-mcp==0.62.1, which is
byte-identical to this file on main, so old-vs-new is a like-for-like comparison.

New tests/test_stream_writer_result_shapes.py (6 cases: the four shapes above, the
already-working text-block shape, and a non-error result):

  • before: 4 failed, 2 passed — the four broken shapes never forward the message
  • after: 6 passed

Existing MCP package suite (test_error_type.py, test_fastmcp.py,
test_fastmcp_attributes.py, test_fastmcp_server_span.py): 5 passed both before
and after.

uvx ruff@0.4.0 check clean against the package's own pyproject.toml (line-length
120, select = ["E", "F", "W"]).

🤖 Generated with Claude Code

Summary by CodeRabbit

  • Bug Fixes

    • Improved handling of MCP responses across different payload formats.
    • Error messages are now extracted safely, including when content is missing or structured unexpectedly.
    • Preserved response forwarding and return values while accurately recording error status.
    • Non-error responses now retain the expected unset span status.
  • Tests

    • Added coverage for error responses with missing, malformed, or non-dictionary content.

….send (traceloop#4038)

send() read request.result as a dict and assumed content[0] was a text block.
Because send() is wrapped in @dont_throw, any raise there was swallowed and the
wrapped send() was never awaited, so the MCP message was dropped instead of
delivered. Read isError/content defensively for dict- and object-shaped results.
@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

coderabbitai Bot commented Aug 2, 2026

Copy link
Copy Markdown

Review Change Stack

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro Plus

Run ID: bd19d166-2fd5-42e0-8178-8b929e048240

📥 Commits

Reviewing files that changed from the base of the PR and between 93429cf and 3306dcb.

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

📝 Walkthrough

Walkthrough

The MCP instrumentation now supports mapping- and object-shaped response results. It safely extracts error text and sets span status. Tests cover missing, empty, and non-text content, plus non-error responses.

Changes

MCP result compatibility

Layer / File(s) Summary
Result shape handling
packages/opentelemetry-instrumentation-mcp/opentelemetry/instrumentation/mcp/instrumentation.py
Added helpers for mapping- and object-shaped fields. Updated response error handling to extract error descriptions safely.
Stream writer validation
packages/opentelemetry-instrumentation-mcp/tests/test_stream_writer_result_shapes.py
Added coverage for result forwarding, return values, message counts, error span statuses, and unset status for non-error results.

Estimated code review effort: 3 (Moderate) | ~20 minutes

🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title clearly identifies the MCP fix for non-dict result shapes in InstrumentedStreamWriter.send().
Linked Issues check ✅ Passed The changes support mapping and object result shapes, prevent error-handling failures, preserve isError semantics, and add targeted tests for issue #4038.
Out of Scope Changes check ✅ Passed The implementation and tests directly address the linked issue and PR objectives without unrelated code changes.
Docstring Coverage ✅ Passed No functions found in the changed files to evaluate docstring coverage. Skipping docstring coverage check.
✨ 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.

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.

🐛 Bug Report: MCP InstrumentedStreamWriter.send() assumes dict-shaped request.result and can fail for object/model results

2 participants