Skip to content

fix(mcp): flag failed tool calls with isError - #43374

Open
AurimasNav wants to merge 3 commits into
apache:masterfrom
AurimasNav:fix/mcp-tool-error-iserror-flag
Open

fix(mcp): flag failed tool calls with isError#43374
AurimasNav wants to merge 3 commits into
apache:masterfrom
AurimasNav:fix/mcp-tool-error-iserror-flag

Conversation

@AurimasNav

Copy link
Copy Markdown
Contributor

Drafted with AI assistance.

Fixes #43358, which @sadpandajoe invited a PR for.

The problem

StructuredContentStripperMiddleware.on_call_tool catches every exception and returns a ToolResult carrying the error text, but never sets is_error. It defaults to False, so the call serializes as isError: false — a permission denial or an unhandled crash is indistinguishable from a successful call to any client that inspects the flag rather than parsing message text.

The fix

Set is_error=True on that last-resort result. The catch-all itself stays exactly as it is — letting exceptions reach the MCP SDK produces CallToolResult(isError=True) responses that some transports can't encode, which is the whole reason the handler exists. is_error rides along in the serialized result as a plain boolean, so this restores protocol conformance without reintroducing the unencodable response.

This is option 1 from the issue discussion, which @dosu also identified as the lowest-risk path. One correction to the snippet suggested there: the field is is_error, not isError — the latter raises TypeError.

The PR also preserves the flag through the structured_content strip. That path rebuilds the ToolResult and dropped is_error, so a tool reporting failure alongside structured output came back looking successful.

Verification

Against fastmcp 3.4.7, the pinned version:

signature: (self, content=None, structured_content=None, meta=None, is_error: bool = False)
wire JSON: {"content": [{"type": "text", "text": "Error: Permission denied"}], "isError": true}
default is_error = False

The default staying False is what keeps successful calls unaffected.

Four tests cover it: a ToolError denial and an unexpected exception are both flagged, a successful result is not, and the flag survives structured-content stripping.

On the encoding concern raised in the original workaround comment — a ToolResult with is_error=True serializes as a plain dict with a boolean field, not as the exception object that triggered the bridge failure. I don't have access to the Claude.ai bridge to test that leg directly, so if a maintainer can confirm against it before merging, that would close the last open question.

@dosubot dosubot Bot added the change:backend Requires changing the backend label Aug 21, 2026
@bito-code-review

bito-code-review Bot commented Aug 21, 2026

Copy link
Copy Markdown
Contributor

Code Review Agent Run #d444fe

Actionable Suggestions - 0
Review Details
  • Files reviewed - 2 · Commit Range: 25e1208..25e1208
    • superset/mcp_service/middleware.py
    • tests/unit_tests/mcp_service/test_middleware.py
  • Files skipped - 0
  • Tools
    • MyPy (Static Code Analysis) - ✔︎ Successful
    • Astral Ruff (Static Code Analysis) - ✔︎ Successful
    • Whispers (Secret Scanner) - ✔︎ Successful
    • Detect-secrets (Secret Scanner) - ✔︎ Successful

Bito Usage Guide

Commands

Type the following command in the pull request comment and save the comment.

  • /review - Manually triggers a full AI review.

  • /pause - Pauses automatic reviews on this pull request.

  • /resume - Resumes automatic reviews.

  • /resolve - Marks all Bito-posted review comments as resolved.

  • /abort - Cancels all in-progress reviews.

Refer to the documentation for additional commands.

Configuration

This repository uses Superset You can customize the agent settings here or contact your Bito workspace admin at evan@preset.io.

Documentation & Help

AI Code Review powered by Bito Logo

@dosubot

dosubot Bot commented Aug 21, 2026

Copy link
Copy Markdown

The fix looks correct and well-reasoned.

The core change is minimal and safe. Adding is_error=True to the catch-all ToolResult restores MCP protocol conformance without touching the catch-all structure itself [1] — the right call given the Claude.ai bridge encoding issue that motivated the workaround in the first place [2].

The structured_content stripping fix is a nice catch. Preserving result.is_error when rebuilding the ToolResult to drop structured_content is easy to miss and correct to include. Without it, a tool that sets is_error=True alongside structured output would silently come back looking successful after the strip [1].

Test coverage is thorough. The four cases (ToolError flagged, unexpected exception flagged, success path untouched, flag survives stripping) cover the relevant branches cleanly.

One open question you've already flagged: confirming the serialized ToolResult(is_error=True) doesn't trigger the same Claude.ai bridge encoding failure. Your reasoning — that it serializes to a plain dict with a boolean field, not as the exception object that caused the bridge failure — is sound and consistent with the issue discussion, but a maintainer with access to that bridge should verify before merging.

The field name correction (is_error vs isError) is also worth calling out, which you've already done — the issue discussion had a snippet with the wrong name that raises TypeError [2].


Leave Feedback Ask Dosu about superset Add Dosu to your team


assert result.structured_content is None
assert result.is_error is True
assert "[REDACTED]" in text

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Suggestion: This assertion was left at the end of the new test, but text is local to the preceding test_client_facing_text_is_sanitized method and is undefined here. The test therefore raises NameError after its intended assertions pass; remove the misplaced assertion or move it back to the sanitization test. [possible bug]

Severity Level: Major ⚠️
- ❌ MCP middleware regression test fails with NameError.
- ❌ CI cannot pass the affected unit-test module.
- ⚠️ The production middleware behavior remains unaffected by this assertion.

Use CodeAnt Skill

Prompt for AI Agent 🤖
This is a comment left during a code review.

**Path:** tests/unit_tests/mcp_service/test_middleware.py
**Line:** 2180:2180
**Comment:**
	*Possible Bug: This assertion was left at the end of the new test, but `text` is local to the preceding `test_client_facing_text_is_sanitized` method and is undefined here. The test therefore raises `NameError` after its intended assertions pass; remove the misplaced assertion or move it back to the sanitization test.

Validate the correctness of the flagged issue. If correct, How can I resolve this? If you propose a fix, implement it and please make it concise.
Once fix is implemented, also check other comments on the same PR, and ask user if the user wants to fix the rest of the comments as well. if said yes, then fetch all the comments validate the correctness and implement a minimal fix
👍 | 👎

@bito-code-review

Copy link
Copy Markdown
Contributor

The flagged issue is correct. In the test test_is_error_survives_structured_content_stripping, the assertion assert "[REDACTED]" in text at line 131 references the variable text, which is not defined in this test method. This will indeed cause a NameError.

To resolve this, you should remove the misplaced assertion, as it appears to be a copy-paste error from the preceding test_client_facing_text_is_sanitized test.

        assert result.structured_content is None
        assert result.is_error is True

I have validated the issue and proposed the fix. Would you like me to check the other comments on this PR and implement fixes for them as well?

tests/unit_tests/mcp_service/test_middleware.py

assert result.structured_content is None
        assert result.is_error is True

@codecov

codecov Bot commented Aug 21, 2026

Copy link
Copy Markdown

Codecov Report

❌ Patch coverage is 0% with 1 line in your changes missing coverage. Please review.
✅ Project coverage is 66.77%. Comparing base (fd7095d) to head (2f727c6).

Files with missing lines Patch % Lines
superset/mcp_service/middleware.py 0.00% 1 Missing ⚠️
Additional details and impacted files
@@            Coverage Diff             @@
##           master   #43374      +/-   ##
==========================================
- Coverage   66.81%   66.77%   -0.05%     
==========================================
  Files        2876     2876              
  Lines      164243   164196      -47     
  Branches    37921    37896      -25     
==========================================
- Hits       109744   109639     -105     
- Misses      52314    52372      +58     
  Partials     2185     2185              
Flag Coverage Δ
hive 38.09% <0.00%> (ø)
mysql 57.79% <0.00%> (ø)
postgres 57.82% <0.00%> (ø)
presto 40.03% <0.00%> (ø)
python 59.25% <0.00%> (ø)
sqlite 57.51% <0.00%> (ø)
unit 100.00% <ø> (ø)

Flags with carried forward coverage won't be shown. Click here to find out more.

☔ View full report in Codecov by Harness.
📢 Have feedback on the report? Share it here.

🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.
  • 📦 JS Bundle Analysis: Save yourself from yourself by tracking and limiting bundle sizes in JS merges.

StructuredContentStripperMiddleware catches every exception and returns a
ToolResult carrying the error text, but never sets is_error. Since is_error
defaults to False, the call serializes as isError: false — so a permission
denial or an unhandled crash is indistinguishable from a successful call to any
client that inspects the flag rather than parsing the message text.

The catch-all itself has to stay: letting exceptions reach the MCP SDK produces
CallToolResult(isError=True) responses that some transports cannot encode. But
is_error rides along in the serialized result as a plain boolean, so setting it
restores protocol conformance without reintroducing the unencodable response.

Also preserve the flag when structured_content is stripped. That path rebuilds
the ToolResult and previously dropped is_error, so a tool reporting failure
alongside structured output came back looking successful.

Verified against fastmcp 3.4.7, the pinned version: the result serializes to
{"content": [...], "isError": true}, and is_error still defaults to False so
successful calls are unaffected.

Closes apache#43358
@AurimasNav
AurimasNav force-pushed the fix/mcp-tool-error-iserror-flag branch from 25e1208 to c1575b3 Compare August 21, 2026 07:08
@bito-code-review

bito-code-review Bot commented Aug 21, 2026

Copy link
Copy Markdown
Contributor

Code Review Agent Run #a0b088

Actionable Suggestions - 0
Review Details
  • Files reviewed - 2 · Commit Range: c1575b3..7c9fca6
    • superset/mcp_service/middleware.py
    • tests/unit_tests/mcp_service/test_middleware.py
  • Files skipped - 0
  • Tools
    • MyPy (Static Code Analysis) - ✔︎ Successful
    • Astral Ruff (Static Code Analysis) - ✔︎ Successful
    • Whispers (Secret Scanner) - ✔︎ Successful
    • Detect-secrets (Secret Scanner) - ✔︎ Successful

Bito Usage Guide

Commands

Type the following command in the pull request comment and save the comment.

  • /review - Manually triggers an incremental AI Review.

  • /review full - Manually triggers a full AI Review.

  • /pause - Pauses automatic reviews on this pull request.

  • /resume - Resumes automatic reviews.

  • /resolve - Marks all Bito-posted review comments as resolved.

  • /abort - Cancels all in-progress reviews.

Refer to the documentation for additional commands.

Configuration

This repository uses Superset You can customize the agent settings here or contact your Bito workspace admin at evan@preset.io.

Documentation & Help

AI Code Review powered by Bito Logo

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

Labels

change:backend Requires changing the backend size/L

Projects

None yet

Development

Successfully merging this pull request may close these issues.

MCP: failed tool calls return isError: false, so denials look like successes

1 participant