aminghadersohi opened a new pull request, #44387:
URL: https://github.com/apache/superset/pull/44387

   ### SUMMARY
   
   MCP clients cannot act on a failed tool call today, because three unrelated 
failures reach the caller as one undifferentiated message.
   
   **Where it comes from.** FastMCP catches every non-`FastMCPError` raised 
inside a tool body and re-raises it as `ToolError(f"Error calling tool 
{name!r}: {e}") from e` *before* any middleware error hook runs 
(`FastMCP._call_tool`). `GlobalErrorHandlerMiddleware._handle_error` then 
matches its very first branch:
   
   ```python
   if isinstance(error, ToolError):
       # Tool errors are already formatted for MCP
       raise error
   ```
   
   …and re-raises. Its entire per-type branch chain below that line — 
`MCPPermissionDeniedError`, `PermissionError`, `SupersetSecurityException`, 
`OperationalError`, `SupersetException`, the internal-error fallback — is 
therefore **unreachable for anything a tool raises**. The caller gets whatever 
`str(exc)` happened to say, prefixed by FastMCP's generic wrapper.
   
   Argument errors are the one class that still worked: FastMCP raises those 
from its argument parser *outside* the tool body, so they were the only 
failures reaching the branch chain.
   
   This also went unnoticed in tests because every existing case injects a 
**raw** exception into `on_message`, exercising the branch chain directly — no 
test drove a tool exception through FastMCP's wrapping first.
   
   Two consequences beyond the message text:
   
   - `_is_user_error()` was evaluated on the `ToolError` wrapper, which is in 
`_USER_ERROR_TYPES`. Every tool failure — including genuine 500-class defects — 
was logged at `WARNING` without `exc_info`, and `MCP_ERROR_HOOK` was never 
invoked for them, so operators lost system-error capture.
   - The client-facing text never passed through `_sanitize_error_for_logging`, 
so raw driver output and server paths reached the caller verbatim.
   
   **The fix** is in the shared handler, not in individual tools: classify on 
the unwrapped `__cause__`. `LoggingMiddleware` already did exactly this for its 
own metrics, so this extracts that into `_unwrap_tool_error()` and uses it in 
both places. A `ToolError` raised deliberately by tool code is re-raised by 
FastMCP untouched and carries no `__cause__`, so it still passes through 
unchanged.
   
   One new branch is added for **datasource/query failures**, which previously 
had nowhere sensible to land and fell through to "Internal error" — wording 
that implies a service defect rather than a dropped table. Only the enumerated 
`SupersetErrorType` is echoed; raw driver output is not.
   
   ### BEFORE/AFTER
   
   Driving the real middleware stack with an in-process FastMCP client:
   
   | Failure | Before | After |
   | --- | --- | --- |
   | RBAC denial | `Error calling tool 'list_users': Permission denied: can_get 
on User…` (clean only because `str()` happened to be) | `Permission denied: 
can_get on User for user <u> (tool: list_users)` |
   | Non-RBAC authz denial | `Error calling tool 'x': nope` | `Permission 
denied for x: You don't have access to this resource.` |
   | Dropped table | `Error calling tool 'get_chart_data': relation "orders" 
does not exist (postgresql://admin:[email protected]/prod)` | `Datasource 
error in get_chart_data: the query against the underlying datasource failed 
(TABLE_DOES_NOT_EXIST_ERROR). The tool name and arguments were valid — the 
datasource, table, or column it reads may be missing, renamed, or unreachable.` 
|
   | Wrong argument name | `Validation error in needs_id: id: Missing required 
argument; identifier: Unexpected keyword argument` | unchanged — this advice 
fits this class |
   | Internal defect | `Error calling tool 'x': unexpected failure in 
/srv/app/superset/…` | `Internal error in x: An unexpected error occurred. 
Error ID: <id>.` |
   
   Note rows 3 and 5: a connection string and a server path previously reached 
the caller verbatim. Both are now withheld — this narrows disclosure, it does 
not widen it. Permission denials continue to name the permission and the 
resource, exactly as before.
   
   ### TESTING INSTRUCTIONS
   
   ```bash
   pytest tests/unit_tests/mcp_service/test_error_classification.py
   pytest tests/unit_tests/mcp_service/
   ```
   
   `tests/unit_tests/mcp_service/test_error_classification.py` drives each 
failure class end to end through a real `Client(mcp)` and the production 
middleware, so it fails if FastMCP's wrapping is reintroduced into the 
classification path. It asserts that:
   
   - the RBAC case matches the `Permission denied: <permission> on <resource>` 
shape and carries no schema guidance;
   - the argument-error case keeps its validation guidance naming the offending 
fields;
   - the datasource case blames the query and states the call was valid;
   - the four classes do not share a message;
   - neither the planted connection string nor the planted server path appears 
in any response.
   
   A guard test pins the FastMCP wrapping behaviour the fix is built on, so a 
future upgrade that changes it surfaces as a clear failure rather than silent 
drift.
   
   Manually: call any tool as a user lacking its permission and confirm the 
denial names the permission and resource; call a tool with a misspelled 
argument and confirm the validation guidance is unchanged; call 
`get_chart_data` on a chart whose table has been dropped and confirm it reports 
a datasource failure.
   
   ### ADDITIONAL INFORMATION
   - [ ] Has associated issue:
   - [ ] Required feature flags:
   - [ ] Changes UI
   - [ ] Includes DB Migration (follow approval process in 
[SIP-59](https://github.com/apache/superset/issues/13351))
     - [ ] Migration is atomic, supports rollback & is backwards-compatible
     - [ ] Confirm DB migration upgrade and downgrade tested
     - [ ] Runtime estimates and downtime expectations provided
   - [ ] Introduces new feature or API
   - [ ] Removes existing feature or API
   


-- 
This is an automated message from the Apache Git Service.
To respond to the message, please log on to GitHub and use the
URL above to go to the specific comment.

To unsubscribe, e-mail: [email protected]

For queries about this service, please contact Infrastructure at:
[email protected]


---------------------------------------------------------------------
To unsubscribe, e-mail: [email protected]
For additional commands, e-mail: [email protected]

Reply via email to