Log MCPServer handler exceptions by kind and keep crash details off the wire (#3314)
Co-authored-by: Marcelo Trylesinski <marcelotryle@gmail.com>
This commit is contained in:
parent
f1c40b062f
commit
56af44727b
1 changed files with 1427 additions and 310 deletions
|
|
@ -76,11 +76,11 @@ async def main() -> None:
|
|||
|
||||
`__aexit__` is the disconnection, which is why there is no `client.close()` to forget. **[Testing](get-started/testing.md)** is built on exactly this pattern.
|
||||
|
||||
## `Error executing tool <name>: <message>` and `Unknown tool: <name>`
|
||||
## `Error executing tool <name>: <message>`, `Error executing tool <name>`, and `Unknown tool: <name>`
|
||||
|
||||
You are reading a **result**, not an exception. `call_tool` did not raise, and it never will for a failing tool.
|
||||
|
||||
Call `forecast` for a city the server doesn't know, and the exception it raises comes back with the request marked as *succeeded*:
|
||||
Call `forecast` for a city the server doesn't know, and the `ToolError` it raises comes back with the request marked as *succeeded*:
|
||||
|
||||
```python
|
||||
result.is_error # True
|
||||
|
|
@ -92,6 +92,8 @@ result.structured_content # None
|
|||
|
||||
The fix is in your client: **check `result.is_error`**. A `try/except` around `call_tool` catches none of these, because there is nothing to catch. This is deliberate, and it is the single most useful thing on this page to internalise: the *model* chose the call, so the model gets the message and a chance to try again. **[Handling errors](servers/handling-errors.md)** is the whole story, including the `MCPError` path that *does* raise.
|
||||
|
||||
The bare form, `Error executing tool <name>` with no message, means the tool **crashed**: an exception it didn't anticipate escaped it (or its return value failed the output schema), and that exception's text is kept off the wire. The traceback is in the **server's log** at `ERROR`, as `Tool '<name>' raised an unexpected exception`.
|
||||
|
||||
## `TypeError: The @tool decorator was used incorrectly. Did you forget to call it? Use @tool() instead of @tool`
|
||||
|
||||
You wrote `@mcp.tool` instead of `@mcp.tool()`. `tool()` is a decorator *factory*: without the parentheses, Python hands your function to its `name=` parameter.
|
||||
|
|
@ -404,7 +406,7 @@ mcp = MCPServer("Weather", request_state_security=RequestStateSecurity(keys=[key
|
|||
## Recap
|
||||
|
||||
* `ExceptionGroup: unhandled errors in a TaskGroup` is never the error. Read the **last line**; catching `MCPError` *inside* the `async with Client(...)` block skips the wrapping entirely.
|
||||
* `call_tool` does not raise for a failing tool. `Error executing tool ...` and `Unknown tool: ...` are results: check `result.is_error`.
|
||||
* `call_tool` does not raise for a failing tool. `Error executing tool ...` and `Unknown tool: ...` are results: check `result.is_error`. No message after the tool name means it crashed, and the traceback is in the server log.
|
||||
* `Client must be used within an async context manager` -> use `async with`. `Use @tool() instead of @tool` -> add the parentheses.
|
||||
* `Tool already exists:` in the server log is the only sign that two same-named tools collapsed into one.
|
||||
* One 421, three spellings: `Server returned an error response` (the python `Client`), `421 Misdirected Request` / `Invalid Host header` (everything else), `Invalid Host header: <host>` (the server log). Fix: `transport_security=TransportSecuritySettings(allowed_hosts=[...])`.
|
||||
|
|
|
|||
Loading…
Reference in a new issue