Skip to content
Open
11 changes: 6 additions & 5 deletions docs/client/index.md
Original file line number Diff line number Diff line change
Expand Up @@ -81,7 +81,7 @@ That schema is everything a UI needs to render an argument form, and everything

`call_tool(name, arguments)` runs the tool and gives you back a `CallToolResult`.

```python title="client.py" hl_lines="26-33"
```python title="client.py" hl_lines="27-34"
--8<-- "docs_src/client/tutorial003.py"
```

Expand Down Expand Up @@ -113,17 +113,18 @@ A tool that raises does **not** raise in your client. It comes back as an ordina

!!! check
Ask `lookup_book` for `"Solaris"` (a title that isn't in the catalog) and the function raises
`ValueError`. The call still returns normally:
`ToolError`. The call still returns normally:

```python
result.is_error # True
result.content # [TextContent(type='text', text="Error executing tool lookup_book: No book titled 'Solaris' in the catalog.")]
result.structured_content # None
```

The exception's message landed in `content`, where the **model** can read it and try again. That
is deliberate: a tool error is part of the conversation, not a crash. Always look at `is_error`
before you trust `structured_content`.
The `ToolError`'s message landed in `content`, where the **model** can read it and try again. That
is deliberate: a tool error is part of the conversation, not a crash. (Had the tool crashed with
some other exception, `content` would say only `Error executing tool lookup_book`.) Always look at
`is_error` before you trust `structured_content`.

!!! warning
`is_error=True` covers more than your own `raise`. Ask for a tool the server doesn't even have
Expand Down
16 changes: 13 additions & 3 deletions docs/deprecated.md
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
# Deprecated features

The 2026-07-28 spec retires five things. The SDK still implements every one of them, and every one of them now carries a **deprecation warning**.
The 2026-07-28 spec retires five things. The SDK still implements every one of them, and every one of them now carries a **deprecation warning**. One SDK helper is deprecated on its own account and is listed [at the end](#deprecated-sdk-helpers).

The table below names each deprecated feature, why it is going away, and the replacement to build on.

Expand Down Expand Up @@ -119,22 +119,32 @@ That is the whole API. There is no per-method switch, and you don't want one: th
Run the filter the other way and you get a free regression test. Add
`"error::mcp.MCPDeprecationWarning"` to the `filterwarnings` setting in your pytest
configuration and the deprecated call **raises** instead of warning. A tool named
`old_log` that still calls `ctx.info()` stops passing and starts reporting:
`old_log` that still calls `ctx.info()` stops passing: the call comes back `is_error=True` with
`Error executing tool old_log`, and the captured server log names the culprit:

```text
Error executing tool old_log: The logging capability is deprecated as of 2026-07-28 (SEP-2577).
mcp.shared.exceptions.MCPDeprecationWarning: The logging capability is deprecated as of 2026-07-28 (SEP-2577).
```

One line of pytest configuration, and a deprecated call can never sneak back into your
codebase without failing a test.

## Deprecated SDK helpers

These are not spec changes, only SDK internals with a better replacement. They warn with the same `MCPDeprecationWarning` and will be removed in 3.0.

| Deprecated | What you do instead |
|---|---|
| `FuncMetadata.call_fn_with_arg_validation()` | `FuncMetadata.validate_arguments()` and then `FuncMetadata.call_fn()`. Only code that drives `FuncMetadata` directly (a custom `Tool` subclass, say) ever called it. |

## Recap

* The 2026-07-28 spec deprecates **roots**, server-initiated **sampling**, and protocol **logging** (all [SEP-2577](https://github.com/modelcontextprotocol/modelcontextprotocol/pull/2577)), restricts **progress** to server-to-client, and removes **`ping`**.
* The replacement column points you onward: **[Multi-round-trip requests](handlers/multi-round-trip.md)** for sampling and roots, **[Logging](handlers/logging.md)** for logging, **[Progress](handlers/progress.md)** for progress. `ping` needs nothing at all.
* Deprecated is advisory: no wire changes, everything keeps working against pre-2026 sessions, and you get a visible `MCPDeprecationWarning` (a `UserWarning`, so it is on by default).
* Sampling and roots additionally need a back-channel that a 2026-07-28 session does not have. On a modern connection they warn and then they raise.
* `warnings.filterwarnings("ignore", category=MCPDeprecationWarning)` silences the whole category; `"error::mcp.MCPDeprecationWarning"` in pytest turns it into a test failure.
* One SDK helper, `FuncMetadata.call_fn_with_arg_validation()`, is deprecated separately for removal in 3.0.
* New code should not be built on any of these.

Every other page in these docs teaches the current API.
2 changes: 1 addition & 1 deletion docs/get-started/real-host.md
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ Which means connecting to a host is one act: you tell it **the command that star

## One server, every host

```python title="server.py" hl_lines="3 33-34"
```python title="server.py" hl_lines="4 34-35"
--8<-- "docs_src/real_host/tutorial001.py"
```

Expand Down
4 changes: 2 additions & 2 deletions docs/get-started/testing.md
Original file line number Diff line number Diff line change
Expand Up @@ -79,8 +79,8 @@ There you go! You can now extend your tests to cover more scenarios.
Two different things can go wrong, and this flag only touches one of them.

An exception inside one of **your tools** is not a protocol failure. It becomes a normal result with
`is_error=True`, and the model reads the message. `raise_exceptions` doesn't change that: with or
without it, `call_tool` returns the same `is_error=True` result. There's a whole page on it:
`is_error=True` (and if it was a `ToolError`, the model reads your message). `raise_exceptions` doesn't

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2: When a tool crashes, UnexpectedToolError is a ToolError subclass, but its original message is withheld and the model receives only the generic Error executing tool <name> form. Distinguish deliberate ToolError messages from crash messages here so tests do not expect crash details on the wire.

(Based on your team's feedback about generic unexpected tool error messages.)

View Feedback

Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At docs/get-started/testing.md, line 82:

<comment>When a tool crashes, `UnexpectedToolError` is a `ToolError` subclass, but its original message is withheld and the model receives only the generic `Error executing tool <name>` form. Distinguish deliberate `ToolError` messages from crash messages here so tests do not expect crash details on the wire.

(Based on your team's feedback about generic unexpected tool error messages.) </comment>

<file context>
@@ -79,8 +79,8 @@ There you go! You can now extend your tests to cover more scenarios.
 An exception inside one of **your tools** is not a protocol failure. It becomes a normal result with
-`is_error=True`, and the model reads the message. `raise_exceptions` doesn't change that: with or
-without it, `call_tool` returns the same `is_error=True` result. There's a whole page on it:
+`is_error=True` (and if it was a `ToolError`, the model reads your message). `raise_exceptions` doesn't
+change that: with or without it, `call_tool` returns the same `is_error=True` result. There's a whole page on it:
 **[Handling errors](../servers/handling-errors.md)**.
</file context>

change that: with or without it, `call_tool` returns the same `is_error=True` result. There's a whole page on it:
**[Handling errors](../servers/handling-errors.md)**.

A failure **outside** a tool body is different. On the connection `Client(mcp)` gives you, the
Expand Down
7 changes: 4 additions & 3 deletions docs/handlers/elicitation.md
Original file line number Diff line number Diff line change
Expand Up @@ -84,7 +84,8 @@ That schema is the form. `Field(description=...)` is the label; a default pre-fi
!!! warning
An elicitation schema is not as expressive as a tool's input schema. Flat, primitive fields
only: `str`, `int`, `float`, `bool`, or a `Literal` of strings (it becomes an `enum`).
Put a model inside the model and `ctx.elicit` raises before anything is sent to the client:
Put a model inside the model and `ctx.elicit` raises before anything is sent to the client.
The tool call fails with `Error executing tool <name>`, and your server log has the reason:

```text
TypeError: Elicitation schema field 'address' rendered as {'$ref': '#/$defs/Address'}, which is not a valid PrimitiveSchemaDefinition
Expand All @@ -107,8 +108,8 @@ A refusal is not an error. The tool decides what declining means (here, no booki

!!! tip
The answer is validated against your model before your code sees it. A client that sends
`"maybe"` for a `bool` doesn't corrupt your booking: the call fails with a
schema-mismatch error, your `if` never runs.
`"maybe"` for a `bool` doesn't corrupt your booking: `ctx.elicit` raises `ValueError`, the call
fails, and your `if` never runs.

## Send the user to a URL

Expand Down
2 changes: 2 additions & 0 deletions docs/handlers/logging.md
Original file line number Diff line number Diff line change
Expand Up @@ -49,6 +49,8 @@ The default is `"INFO"`.

`logging.basicConfig()` never replaces handlers that already exist. If you configure logging yourself before creating the server, your configuration wins.

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#any-other-exception)** explains what gets logged and at which level.

## Try it

Run the server with the MCP Inspector:
Expand Down
8 changes: 4 additions & 4 deletions docs/migration.md
Original file line number Diff line number Diff line change
Expand Up @@ -992,8 +992,8 @@ its behavior is unchanged.
`MCPError` carries `ErrorData` and is the SDK's protocol-error type — raise it
when the request itself should be rejected (missing client capability,
elicitation required, invalid parameters). For tool *execution* failures the
calling LLM should see and react to, raise any other exception or return
`CallToolResult(is_error=True, ...)` directly; that path is unchanged.
calling LLM should see and react to, raise `ToolError` or return
`CallToolResult(is_error=True, ...)` directly.

The client sees this change too. `Client.call_tool()` and
`ClientSession.call_tool()` raise on a JSON-RPC error response, so a tool that
Expand All @@ -1016,7 +1016,7 @@ except MCPError as e:

### Resource not found returns `-32602` and resource lookups raise typed exceptions (SEP-2164)

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.
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.

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`).

Expand Down Expand Up @@ -2737,7 +2737,7 @@ One behavioral caveat when moving progress-reporting handlers onto `Client(serve

Every deprecation below is a runtime warning as well as a type-checker one: deprecated methods and helpers emit `mcp.MCPDeprecationWarning` on each call, and the deprecated `Server(...)` constructor parameters (`on_set_logging_level`, `on_roots_list_changed`, `on_progress`) emit it at construction time. The category subclasses `UserWarning`, not `DeprecationWarning`, so it is visible by default; [Deprecated features](deprecated.md) has the full list and each replacement.

Under pytest's `filterwarnings = ["error"]`, that warning becomes an exception at the first deprecated call. Inside an `@mcp.tool()` handler the exception is caught like any other and returned as `CallToolResult(is_error=True)` (`Error executing tool ...: The logging capability is deprecated as of 2026-07-28 (SEP-2577).`), which reads as a failing tool rather than a warning. Keep the warnings visible but non-fatal with:
Under pytest's `filterwarnings = ["error"]`, that warning becomes an exception at the first deprecated call. Inside an `@mcp.tool()` handler the exception is caught like any other and returned as `CallToolResult(is_error=True)` (`Error executing tool ...`, with the `MCPDeprecationWarning` traceback in the server log), which reads as a failing tool rather than a warning. Keep the warnings visible but non-fatal with:

```toml
[tool.pytest.ini_options]
Expand Down
Loading
Loading