You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
A crashing tool used to leave no server-side trace: _handle_call_tool
turned the exception into an is_error result before the dispatcher
boundary could log it, so a KeyError('id') reached the model as "'id'"
and its traceback existed nowhere. Resources logged once and prompts
twice. Tool.run also re-wrapped a deliberate ToolError, so nothing
downstream could tell an anticipated failure from a crash.
Tool.run now validates arguments first (a schema rejection is a plain
ToolError chained to the ValidationError) and runs the body under an
except ladder that keeps the distinction in the type: a deliberate
ToolError stays a ToolError, anything else becomes the new
UnexpectedToolError. Both keep the "Error executing tool X: " text, so
results are byte-identical. Resources get the matching
UnexpectedResourceError, raised by whichever layer first sees the
foreign exception so __cause__ is always the original.
_log_handler_exception in server.py is the one place tools and
resources are logged: INFO without a traceback for ToolError and
ResourceError (deliberate, unknown name, bad arguments, not found),
ERROR with the traceback for anything else. get_prompt stops logging,
leaving the dispatcher boundary's record as the only one.
ResourceError raised from a static resource now passes through to the
client as it already did from a template.
Copy file name to clipboardExpand all lines: docs/handlers/logging.md
+2Lines changed: 2 additions & 0 deletions
Display the source diff
Display the rich diff
Original file line number
Diff line number
Diff line change
@@ -70,6 +70,8 @@ went to standard error: the terminal, not the wire.
70
70
don't want log lines, you want spans. Your server already emits them: the SDK traces every
71
71
message with OpenTelemetry out of the box. See **[OpenTelemetry](../run/opentelemetry.md)**.
72
72
73
+
You don't have to log your own handlers' crashes either. When a tool or resource function raises something unexpected, the SDK writes the `ERROR` record with the traceback for you, on its own `mcp.*` loggers; a failure you raised deliberately (`ToolError`, `ResourceNotFoundError`) is an `INFO` line instead. A prompt function that raises is an `ERROR` record too, whatever it raised. **[Handling errors](../servers/handling-errors.md#what-lands-in-your-log)** has the split. (In a test using `Client(mcp, raise_exceptions=True)`, a prompt failure is handed to your test as the exception rather than logged.)
74
+
73
75
## Recap
74
76
75
77
* The MCP protocol's logging capability is deprecated by the 2026-07-28 spec and not replaced. Don't build on it.
Copy file name to clipboardExpand all lines: docs/migration.md
+1-1Lines changed: 1 addition & 1 deletion
Display the source diff
Display the rich diff
Original file line number
Diff line number
Diff line change
@@ -1016,7 +1016,7 @@ except MCPError as e:
1016
1016
1017
1017
### Resource not found returns `-32602` and resource lookups raise typed exceptions (SEP-2164)
1018
1018
1019
-
Reading a missing resource now returns JSON-RPC error code `-32602` (invalid params) with the requested URI in `error.data` (`{"uri": ...}`), per [SEP-2164](https://github.com/modelcontextprotocol/modelcontextprotocol/pull/2164). Previously the server returned code `0` with no `data`. Clients can now reliably distinguish not-found from other errors; a template handler that raises `ResourceNotFoundError` (from `mcp.server.mcpserver.exceptions`) produces this same response.
1019
+
Reading a missing resource now returns JSON-RPC error code `-32602` (invalid params) with the requested URI in `error.data` (`{"uri": ...}`), per [SEP-2164](https://github.com/modelcontextprotocol/modelcontextprotocol/pull/2164). Previously the server returned code `0` with no `data`. Clients can now reliably distinguish not-found from other errors; a resource handler (static or template) that raises `ResourceNotFoundError` (from `mcp.server.mcpserver.exceptions`) produces this same response.
1020
1020
1021
1021
The underlying lookups now raise typed exceptions instead of `ValueError`. `ResourceManager.get_resource()` raises `ResourceNotFoundError` when no resource or template matches the URI, and `ResourceTemplate.create_resource()` raises `ResourceError` when the template function fails. Neither subclasses `ValueError`, so callers catching `ValueError` should switch to `ResourceNotFoundError` / `ResourceError` (both importable from `mcp.server.mcpserver.exceptions`; `ResourceNotFoundError` subclasses `ResourceError`).
Copy file name to clipboardExpand all lines: docs/servers/handling-errors.md
+25-5Lines changed: 25 additions & 5 deletions
Display the source diff
Display the rich diff
Original file line number
Diff line number
Diff line change
@@ -115,10 +115,29 @@ Send `get_author` a `title` that isn't a string and the SDK rejects it against t
115
115
It means a whole class of `raise` statements you don't write: don't re-validate your own type hints.
116
116
117
117
!!! info
118
-
Everything on this page is what a **client** sees, and the in-memory `Client` you'll write
119
-
tests with sees exactly the same thing. Even `raise_exceptions=True` doesn't turn a tool error
120
-
back into a traceback: by the time that flag could act, your exception is already the
121
-
`is_error=True` result. Assert on the result. **[Testing](../get-started/testing.md)** covers the pattern.
118
+
Everything so far is what a **client** sees, and the in-memory `Client` you'll write tests
119
+
with sees exactly the same thing. Even `raise_exceptions=True` doesn't hand a failing tool's
120
+
exception back to the caller: by the time that flag could act, your exception is already the
121
+
`is_error=True` result. Assert on the result; the traceback is in the server's log (next
122
+
section), which pytest's `caplog` captures. **[Testing](../get-started/testing.md)** covers the pattern.
123
+
124
+
## What lands in your log
125
+
126
+
Your server keeps its own record of these failures, and it draws one more line: between a failure you anticipated and one you didn't.
127
+
128
+
`get_author` raised a plain `ValueError`. The model got the message, but the SDK can't know you *meant* that exception, so it assumes you didn't: the call is logged at `ERROR` with the full traceback. That is exactly what you want on the day the exception is a `KeyError` from three libraries down and the result text says only `'id'`.
129
+
130
+
When the failure is one you planned for, say so with `ToolError`:
131
+
132
+
```python title="server.py" hl_lines="2 12-13"
133
+
--8<--"docs_src/handling_errors/tutorial004.py"
134
+
```
135
+
136
+
The model reads precisely what it read before. The difference is on your side: a `ToolError` is logged as one `INFO` line with no traceback, so a production log at `WARNING` stays quiet until something is actually broken. Bad arguments and unknown tool names are `INFO` lines too; those are the caller's mistakes, not yours.
137
+
138
+
Resources draw the same line. The `-32603` from a crashing resource handler names only the URI, so the `ERROR` record in your log is the one place the cause and its traceback exist. `ResourceNotFoundError`, including the SDK's own `Unknown resource`, is an `INFO` line. (A template parameter that fails its type annotation, `books://{id}` read with an `id` that isn't an `int`, currently counts as a crash.)
139
+
140
+
Prompts aren't split yet: any failure in a prompt function, including an unknown name or a missing argument, is one `ERROR` record with its traceback, written by the transport layer that turns it into the JSON-RPC error.
122
141
123
142
## Recap
124
143
@@ -127,7 +146,8 @@ It means a whole class of `raise` statements you don't write: don't re-validate
127
146
* The deciding question: *could a smarter model have avoided this?* Yes -> exception. No -> `MCPError`.
128
147
*`ResourceNotFoundError` from a resource handler -> the protocol's `-32602`, with the URI in `data`.
129
148
* Bad arguments are rejected against the schema before your function runs; you don't `raise` for those.
130
-
*`from mcp import MCPError`; the error-code constants come from `mcp.types`.
149
+
* In your log: an exception you didn't raise as `ToolError` is an `ERROR` record with its traceback; `ToolError`, bad tool arguments, unknown tool names, and `ResourceNotFoundError` are one `INFO` line each.
150
+
*`from mcp import MCPError`; `ToolError` and `ResourceNotFoundError` come from `mcp.server.mcpserver.exceptions`; the error-code constants come from `mcp.types`.
131
151
132
152
Errors handled. That is everything a server *exposes*. What every handler can read, and do back to the client while it runs, is the next section: **[Inside your handler](../handlers/index.md)**.
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.
94
94
95
+
If `<message>` alone doesn't tell you what broke, the traceback is in the **server's log**: an exception the tool didn't raise as `ToolError` is logged there at `ERROR`, as `Tool '<name>' raised an unexpected exception`.
96
+
95
97
## `TypeError: The @tool decorator was used incorrectly. Did you forget to call it? Use @tool() instead of @tool`
96
98
97
99
You wrote `@mcp.tool` instead of `@mcp.tool()`. `tool()` is a decorator *factory*: without the parentheses, Python hands your function to its `name=` parameter.
0 commit comments