chore: import upstream snapshot with attribution
Deploy Docs / deploy-docs (push) Failing after 1s
Conformance Tests / client-conformance (push) Failing after 3s
Conformance Tests / server-conformance (push) Failing after 1s
GitHub Actions Security Analysis / zizmor (push) Failing after 1s
CI / checks (push) Failing after 59m20s
CI / all-green (push) Has been cancelled
Deploy Docs / deploy-docs (push) Failing after 1s
Conformance Tests / client-conformance (push) Failing after 3s
Conformance Tests / server-conformance (push) Failing after 1s
GitHub Actions Security Analysis / zizmor (push) Failing after 1s
CI / checks (push) Failing after 59m20s
CI / all-green (push) Has been cancelled
This commit is contained in:
@@ -0,0 +1,52 @@
|
||||
# error-handling
|
||||
|
||||
Tool *execution* failures travel as a successful `CallToolResult` with
|
||||
`is_error=True` so the LLM can read the message and self-correct.
|
||||
*Protocol* failures travel as a JSON-RPC error that the client catches as
|
||||
`MCPError`. This story shows how to produce each from a tool body — `raise
|
||||
ToolError(...)` vs `raise MCPError(...)` on `MCPServer`; an explicit
|
||||
`is_error=True` return vs `raise MCPError` on `lowlevel.Server` — and how a
|
||||
client tells them apart.
|
||||
|
||||
## Run it
|
||||
|
||||
```bash
|
||||
# stdio (default — the client spawns the server as a subprocess)
|
||||
uv run python -m stories.error_handling.client
|
||||
|
||||
# HTTP — the client self-hosts the server on a free port, runs, then tears it down
|
||||
uv run python -m stories.error_handling.client --http
|
||||
# same, against the lowlevel-API server variant
|
||||
uv run python -m stories.error_handling.client --http --server server_lowlevel
|
||||
```
|
||||
|
||||
## What to look at
|
||||
|
||||
- `client.py` `main` — opens with `async with Client(target, mode=mode) as
|
||||
client:`. Inside it, `await` returns for `is_error` results and
|
||||
`except MCPError` catches protocol errors; the client never auto-raises on
|
||||
`is_error`.
|
||||
- `server.py` — `raise ToolError(...)` vs `raise MCPError(...)`: same `raise`
|
||||
keyword, opposite wire channel. The tool wrapper re-raises `MCPError`
|
||||
verbatim and wraps everything else as an `is_error` result.
|
||||
- `server_lowlevel.py` — no wrapper: you build `CallToolResult(is_error=True)`
|
||||
yourself, and `MCPError` is the only way to pick a JSON-RPC error code.
|
||||
|
||||
## Caveats
|
||||
|
||||
- The "any other exception → `is_error` result" contract on `MCPServer` and the
|
||||
"uncaught exception → `code=0`" behaviour on `lowlevel.Server` are **not
|
||||
shown** — the contract is under design and the legacy code is a known spec
|
||||
divergence. This story will grow those cases once the contract lands.
|
||||
- `MCPServer` prefixes the execution-error message with
|
||||
`"Error executing tool {name}: "`; build a `CallToolResult` directly from a
|
||||
lowlevel handler if you need verbatim control.
|
||||
|
||||
## Spec
|
||||
|
||||
[Tools — error handling](https://modelcontextprotocol.io/specification/2025-11-25/server/tools#error-handling)
|
||||
|
||||
## See also
|
||||
|
||||
`tools/` (the happy path), `streaming/` (cancellation as a third error-adjacent
|
||||
surface).
|
||||
Reference in New Issue
Block a user