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
Log at the two handler sites directly instead of through a shared
helper: the tool site checks for ToolError, the resource site only has
to ask whether it caught an UnexpectedResourceError.
Drop the three transport-matrix logging tests and their requirement
ids from the interaction suite, which is for wire behaviour; the same
properties are covered next to MCPServer in test_server.py.
Shorten the logging docs to a pointer, reword the handling-errors
section plainly, and drop the recap bullet and prompt caveats.
Copy file name to clipboardExpand all lines: docs/handlers/logging.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
@@ -70,7 +70,7 @@ 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.)
73
+
You also don't need a `try`/`except` in every handler just to record failures. When a tool or resource function raises, the SDK logs it for you. **[Handling errors](../servers/handling-errors.md#what-the-server-logs)**explains what gets logged and at which level.
Copy file name to clipboardExpand all lines: docs/servers/handling-errors.md
+8-11Lines changed: 8 additions & 11 deletions
Display the source diff
Display the rich diff
Original file line number
Diff line number
Diff line change
@@ -118,26 +118,24 @@ It means a whole class of `raise` statements you don't write: don't re-validate
118
118
Everything so far is what a **client** sees, and the in-memory `Client` you'll write tests
119
119
with sees exactly the same thing. Even `raise_exceptions=True` doesn't hand a failing tool's
120
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 tracebackis in the server's log (next
122
-
section), which pytest's `caplog` captures. **[Testing](../get-started/testing.md)** covers the pattern.
121
+
`is_error=True` result. Assert on the result. If you need the traceback, it is in the server's
122
+
log (next section), and pytest's `caplog` captures it. **[Testing](../get-started/testing.md)** covers the pattern.
123
123
124
-
## What lands in your log
124
+
## What the server logs
125
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.
126
+
The server also logs these failures, and how it logs them depends on whether you anticipated the failure.
127
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'`.
128
+
`get_author` raised a plain `ValueError`. The model got the message, but the SDK can't tell that you raised it on purpose, so it treats the call as a crash and logs it at `ERROR` with the full traceback. That is what you want on the day the exception is a `KeyError` from deep inside a library and the result text says only `'id'`.
129
129
130
130
When the failure is one you planned for, say so with `ToolError`:
131
131
132
132
```python title="server.py" hl_lines="2 12-13"
133
133
--8<--"docs_src/handling_errors/tutorial004.py"
134
134
```
135
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.
136
+
`ToolError` comes from `mcp.server.mcpserver.exceptions`. The model reads exactly what it read before. The difference is in your log, where a `ToolError` is a single `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 logged at `INFO` too, because those are the caller's mistakes rather than yours.
137
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.
138
+
Resources work the same way. A crashing resource handler is logged at `ERROR` with its traceback, which matters more here because the `-32603` the client receives names only the URI. `ResourceNotFoundError` is an `INFO` line.
141
139
142
140
## Recap
143
141
@@ -146,8 +144,7 @@ Prompts aren't split yet: any failure in a prompt function, including an unknown
146
144
* The deciding question: *could a smarter model have avoided this?* Yes -> exception. No -> `MCPError`.
147
145
*`ResourceNotFoundError` from a resource handler -> the protocol's `-32602`, with the URI in `data`.
148
146
* Bad arguments are rejected against the schema before your function runs; you don't `raise` for those.
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`.
147
+
*`from mcp import MCPError`; the error-code constants come from `mcp.types`.
151
148
152
149
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`.
95
+
If `<message>` alone doesn't tell you what broke, look in the **server's log**. Unless the tool raised `ToolError`, the exception is logged there at `ERROR` with its traceback, as `Tool '<name>' raised an unexpected exception`.
96
96
97
97
## `TypeError: The @tool decorator was used incorrectly. Did you forget to call it? Use @tool() instead of @tool`
0 commit comments