Skip to content

feat(mcp): report every tool failure as an MCP error - #12429

Draft
puchy22 wants to merge 2 commits into
feat/mcp-error-renderingfrom
feat/mcp-tool-error-contract
Draft

feat(mcp): report every tool failure as an MCP error#12429
puchy22 wants to merge 2 commits into
feat/mcp-error-renderingfrom
feat/mcp-tool-error-contract

Conversation

@puchy22

@puchy22 puchy22 commented Aug 12, 2026

Copy link
Copy Markdown
Member

Context

Stack 2 of N — based on #12428. Review that one first; this PR's diff is only what sits on top of it.

#12428 added the vocabulary for describing a failure. This PR is what makes every tool use it, and the rulebook the remaining PRs in the stack are checked against.

Description

ProwlerMCP (lib/server.py) is a FastMCP subclass that overrides tool() to wrap whatever it registers in tool_errors. FastMCP.tool() is the single funnel every registration goes through — the @server.tool() and bare @server.tool decorator forms, and the direct mcp.tool(fn) call BaseTool uses to auto-register — so overriding it covers all of them at once. All four servers are now built from it.

It is applied at the base class rather than by hand because forgetting it is silent and expensive: this PR also sets mask_error_details=True everywhere, so an unwrapped tool answers Error calling tool 'x' and nothing else, while its name, schema and description all still look correct. ToolError is a FastMCPError and FastMCP._call_tool re-raises those untouched, so a rendered message reaches the client verbatim, past every mount and past the masking. That is what lets the servers mask by default and still say something useful.

tool_errors wraps the callable handed to mcp.tool(), not the class attribute — so only the MCP boundary is normalised. A tool calling another tool internally still sees the real, typed exception and can branch on it.

Nothing about the tools themselves changes here. Every tool that returns {"error": ...} today keeps doing exactly that; those are converted surface by surface in the PRs above. What changes is that a failure which used to escape as a raw exception is now described by render_tool_error instead.

The rulebook. AGENTS.md gains an ### Errors section and the developer guide's ### Error Handling section is rewritten. Those exist in this PR rather than at the end of the stack for a specific reason: without them the guide would spend the next several PRs telling contributors to "return structured error responses instead of raising exceptions", which is now exactly wrong. The parts of the guide that describe things not yet built (the Hub helpers, ScanCreationResult(status="created_unconfirmed")) are held back.

Steps to review

Start with lib/server.py — it is 37 lines and it is the whole mechanism.

Then mcp_server/tests/lib/test_server.py, which covers the four ways a tool can be registered and asserts the wrapper is invisible to FastMCP's schema generation. A wrapper that loses the signature takes the parameters with it, leaving a tool that looks registered and is unusable; test_wrapping_does_not_disturb_the_published_tool is the guard.

tests/test_server.py gains test_no_tool_disappears_between_registration_and_the_client, which compares each sub-server's tool count against what the mount actually publishes. That is the check that would catch a wrapper silently dropping a tool.

Worth knowing before changing tool_errors:

  • Never register its result with exclude_args=. That path rebuilds the function from __code__, which on a wrapper is the wrapper's own. Nothing passes it today.
  • Use inspect.iscoroutinefunction, never asyncio.iscoroutinefunction. The asyncio one is deprecated from Python 3.14 and removed in 3.16; since requires-python has no upper bound and the suite runs with filterwarnings = ["error"], swapping it in would fail the tests on any 3.14 interpreter — which CI does not currently cover.
cd mcp_server && uv run pytest -q     # 155 passed

Checklist

MCP Server

  • All issue/task requirements work as expected on the MCP Server
  • Changelog fragment: not applicable — no tool changes its output in this PR. The entry describing the new contract lands with the final PR of the stack, once it is true of every tool. Labelled no-changelog.

License

By submitting this pull request, I confirm that my contribution is made under the terms of the Apache 2.0 license.

Adds ProwlerMCP, the FastMCP subclass every sub-server is now built
from. Its tool() wraps whatever it registers -- the decorator forms and
the direct call BaseTool uses -- so a failure leaves any tool as a
ToolError, which the client reads as isError: true.

Applied at the base class rather than by hand because forgetting it is
silent: every server now sets mask_error_details=True, so an unwrapped
tool would answer "Error calling tool 'x'" and nothing else. ToolError
bypasses that masking, which is what lets the servers mask by default
and still say something useful.

No tool changes yet. Tools that still return {"error": ...} keep working
exactly as before; they are converted surface by surface in the PRs
above this one. What changes here is that a failure which used to escape
as a raw exception is now described by render_tool_error.

The rules this establishes are in AGENTS.md and the developer guide,
so the conversions have something to be checked against.
@coderabbitai

coderabbitai Bot commented Aug 12, 2026

Copy link
Copy Markdown
Contributor

Important

Review skipped

Draft detected.

Please check the settings in the CodeRabbit UI or the .coderabbit.yaml file in this repository. To trigger a single review, invoke the @coderabbitai review command.

⚙️ Run configuration

Configuration used: Organization UI

Review profile: ASSERTIVE

Plan: Pro Plus

Run ID: e2f59b70-77e9-419e-a16a-20ca0b64008e

You can disable this status message by setting the reviews.review_status to false in the CodeRabbit configuration file.

Use the checkbox below for a quick retry:

  • 🔍 Trigger review

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.

@github-actions

Copy link
Copy Markdown
Contributor

⚠️ Changes detected in the following folders without a changelog fragment:

  • mcp_server

A changelog fragment is a small Markdown file named <slug>.<type>.md under <component>/changelog.d/, where <type> is one of added, changed, deprecated, removed, fixed or security. Its content is the changelog entry text, without the PR link (added automatically at release time) and without a trailing period. For example:

echo 'Entry text describing the change' > <component>/changelog.d/mcp-tool-error-contract.fixed.md

If this PR does not need a changelog entry, add the no-changelog label instead.

@github-actions

Copy link
Copy Markdown
Contributor

No Conflicts

No conflict markers, and the branch merges cleanly into its base.

@codecov

codecov Bot commented Aug 12, 2026

Copy link
Copy Markdown

Codecov Report

❌ Patch coverage is 98.00000% with 1 line in your changes missing coverage. Please review.
✅ Project coverage is 92.17%. Comparing base (c37e2b9) to head (cbc5f58).

Additional details and impacted files
@@                    Coverage Diff                    @@
##           feat/mcp-error-rendering   #12429   +/-   ##
=========================================================
  Coverage                     92.16%   92.17%           
=========================================================
  Files                           311      312    +1     
  Lines                         44952    44994   +42     
=========================================================
+ Hits                          41431    41472   +41     
- Misses                         3521     3522    +1     
Flag Coverage Δ
mcp 55.56% <98.00%> (+0.65%) ⬆️

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

Components Coverage Δ
prowler ∅ <ø> (∅)
api 94.56% <ø> (ø)
mcp_server 55.56% <98.00%> (+0.65%) ⬆️
🚀 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.

@github-actions

github-actions Bot commented Aug 12, 2026

Copy link
Copy Markdown
Contributor

🔒 Container Security Scan

Image: prowler-mcp:fe6c6f7
Last scan: 2026-08-13 13:43:22 UTC

✅ No Vulnerabilities Detected

The container image passed all security checks. No known CVEs were found.

📋 Resources:

@puchy22 puchy22 added the no-changelog Skip including change in changelog/release notes label Aug 12, 2026
@github-actions

github-actions Bot commented Aug 12, 2026

Copy link
Copy Markdown
Contributor

🔎 Container Security Scan (Grype)

Image: prowler-mcp:fe6c6f7
Last scan: 2026-08-13 13:44:32 UTC

✅ Nothing Blocking

No findings at critical or high severity.

Not blocking at this cutoff — medium: 5, low: 2.

5 finding(s) excluded by .grype.yaml, each with a documented reason.


📋 Resources:

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

Labels

component/mcp-server documentation no-changelog Skip including change in changelog/release notes

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant