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
{{ message }}
Repository navigation
Commit b6ed5bc
Browse filesBrowse the repository at this point in the historyBrowse files
A client can give up on a call: the user pressed stop, or a timeout ran out.
4
+
5
+
When it does, the SDK **cancels your handler**. The `await` it is waiting on raises, the function unwinds, and nothing it returns is sent. Most handlers need to do nothing about that.
6
+
7
+
Two kinds do: a handler with something to clean up, and a handler that is a plain `def`.
8
+
9
+
## Clean up in an `async def` tool
10
+
11
+
Put the cleanup in a `finally`:
12
+
13
+
```python title="server.py" hl_lines="23 26-28"
14
+
--8<--"docs_src/cancellation/tutorial001.py"
15
+
```
16
+
17
+
* The `finally` runs however the tool ends: it returned, it raised, or it was cancelled.
18
+
* Cleanup that has to `await` needs `shield=True`. In a cancelled handler every further `await` raises too, so without the shield `release_hold` would stop at its first line.
19
+
* Nothing can cancel a shielded block, so give it a time limit. Here that is `5` seconds.
20
+
21
+
!!! tip
22
+
Reach for `finally`, not `except`. The cancellation has to keep travelling up once your cleanup
23
+
is done, and a `finally` lets it.
24
+
25
+
## Stop early in a plain `def` tool
26
+
27
+
A plain `def` tool runs in a thread, and nothing can interrupt a thread from outside. The tool has to ask:
28
+
29
+
```python title="server.py" hl_lines="22 25-26"
30
+
--8<--"docs_src/cancellation/tutorial002.py"
31
+
```
32
+
33
+
*`anyio.from_thread.check_cancelled()` does nothing while the call is live, and raises once it has been cancelled. Call it between units of work.
34
+
* Cleanup goes in a `finally` here too. Nothing in a thread awaits, so it needs no shield.
35
+
* A `def` tool that never asks runs to the end, and its result is thrown away.
36
+
37
+
## Where it applies
38
+
39
+
Prompt and resource functions are cancelled exactly like tools.
40
+
41
+
It works the same over stdio and Streamable HTTP. With this SDK's `Client`, giving up means cancelling the task that awaits `call_tool`, or letting its `read_timeout_seconds` run out.
42
+
43
+
!!! warning
44
+
Two Streamable HTTP options keep the news from your handler: `json_response=True` on a
45
+
`2026-07-28` connection, and `stateless_http=True` on a legacy one. There the handler runs to
46
+
the end whatever the client did.
47
+
48
+
## Recap
49
+
50
+
* When the client gives up on a call, the SDK cancels the handler: tool, prompt or resource.
51
+
*`async def`: clean up in a `finally`, and put cleanup that awaits inside `anyio.move_on_after(seconds, shield=True)`.
52
+
* Plain `def`: call `anyio.from_thread.check_cancelled()` between units of work, or the tool runs to the end. A plain `finally` cleans up.
53
+
*`json_response=True` (modern connections) and `stateless_http=True` (legacy ones) switch cancellation off.
54
+
55
+
Progress and cancellation are between a running tool and its *caller*. The lines it logs for *you*, the person operating the server, are a different channel: **[Logging](logging.md)**.
Copy file name to clipboardExpand all lines: docs/handlers/progress.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
@@ -115,4 +115,4 @@ The callback receives `total=None`. A client can still show *activity* ("3 impor
115
115
* No callback on the call means `report_progress` does nothing. Report unconditionally.
116
116
* Omit `total` when you don't know it; the callback gets `None`.
117
117
118
-
Progress is what a running tool shows the *user*. The lines it logs for *you*, the person operating the server, are a different channel: **[Logging](logging.md)**.
118
+
Progress is for a client that is still waiting. What your tool sees when the client stops waiting is **[Cancellation](cancellation.md)**.
Copy file name to clipboardExpand all lines: docs/migration.md
+4-20Lines changed: 4 additions & 20 deletions
Display the source diff
Display the rich diff
Original file line number
Diff line number
Diff line change
@@ -97,7 +97,7 @@ dependencies = [
97
97
]
98
98
```
99
99
100
-
Relax or bump any conflicting pins when upgrading. sse-starlette jumps two majors, so a project that imports `sse_starlette` itself must also work through that library's own breaking changes to co-install with mcp v2. `opentelemetry-api` is a new hard dependency because every outbound request now carries a `_meta` envelope used for OpenTelemetry trace propagation; see [Every outbound request now carries a `_meta` envelope](#every-outbound-request-now-carries-a-_meta-envelope-opentelemetry-is-on-by-default). `mcp-types` is exact-pinned to the SDK version; nothing in a v1 tree can conflict with it, but do not pin `mcp-types` independently of `mcp`.
100
+
Relax or bump any conflicting pins when upgrading. sse-starlette jumps two majors, so a project that imports `sse_starlette` itself must also work through that library's own breaking changes to co-install with mcp v2. `opentelemetry-api` is a new hard dependency because OpenTelemetry trace propagation now ships enabled; see [OpenTelemetry is on by default](#opentelemetry-is-on-by-default). `mcp-types` is exact-pinned to the SDK version; nothing in a v1 tree can conflict with it, but do not pin `mcp-types` independently of `mcp`.
101
101
102
102
### `httpx` and `httpx-sse` replaced by `httpx2`
103
103
@@ -1789,7 +1789,7 @@ Positional callers (`session.elicit_form(message, schema)`) are unaffected, and
1789
1789
1790
1790
### `Client` defaults to `mode='auto'`
1791
1791
1792
-
In v1, connecting to a server always performed the `initialize` handshake. In v2, `Client` defaults to `mode='auto'`: on enter it probes `server/discover` and, if the server doesn't support it, falls back to the `initialize` handshake. Pass `mode='legacy'` to force the initialize handshake and reproduce v1's pre-2026 connection sequence (the per-request wire shape still differs from v1; see [Every outbound request now carries a `_meta` envelope](#every-outbound-request-now-carries-a-_meta-envelope-opentelemetry-is-on-by-default)), or pass a modern protocol-version string (e.g. `mode='2026-07-28'`) to pin a version without probing.
1792
+
In v1, connecting to a server always performed the `initialize` handshake. In v2, `Client` defaults to `mode='auto'`: on enter it probes `server/discover` and, if the server doesn't support it, falls back to the `initialize` handshake. Pass `mode='legacy'` to force the initialize handshake and reproduce v1's pre-2026 connection sequence, or pass a modern protocol-version string (e.g. `mode='2026-07-28'`) to pin a version without probing.
1793
1793
1794
1794
The probe is transport-independent: v2 servers answer it over stdio (and any other stream-pair transport) as well as streamable HTTP, so `mode='auto'` lands on `2026-07-28` against a v2 server on every transport. If your stdio workflow relies on server-initiated requests (sampling, push elicitation, roots), pass `mode='legacy'` — a 2026-07-28 connection refuses them on every transport with `NoBackChannelError` (see [Server-initiated sampling, elicitation, and roots raise `NoBackChannelError`](#server-initiated-sampling-elicitation-and-roots-raise-nobackchannelerror)).
1795
1795
@@ -2659,25 +2659,9 @@ Validation runs when the result is serialized onto the wire, not when the model
2659
2659
2660
2660
In v1, a request for a method the SDK didn't recognize failed request-union validation and was answered with `-32602` (`"Invalid request parameters"`, empty `data`). Any method the receiver doesn't serve — unrecognized on either side, or a spec method the server has no registered handler for — is now answered with the JSON-RPC-specified `-32601` (`"Method not found"`), with the method name in `data`, in every initialization state. Clients still decline sampling, elicitation, and roots requests with `-32600` when no callback is registered, as in v1. Update anything that matched on the old code for this case.
2661
2661
2662
-
### Every outbound request now carries a `_meta` envelope; OpenTelemetry is on by default
2662
+
### OpenTelemetry is on by default
2663
2663
2664
-
v2 sends `"_meta": {}` in the params of every request it emits, at every negotiated protocol version. Requests that had no params in v1, such as `ping` and `tools/list`, now carry `"params": {"_meta": {}}`; server-initiated requests get the same envelope. This is spec-valid and accepted by all peers, but wire traffic differs from v1 on every call, and no configuration restores the v1 wire shape. Update any test or tooling that asserts on raw outbound request bytes.
2665
-
2666
-
**Before (v1):** same client code, 2025-11-25 peer:
The envelope exists for OpenTelemetry trace propagation ([SEP-414](https://github.com/modelcontextprotocol/modelcontextprotocol/pull/414)), which now ships enabled: every server installs a tracing middleware and the client opens a span per outbound request. With no OpenTelemetry SDK configured these are no-ops and only the empty envelope is visible. If your application already configures a global tracer provider, it starts recording MCP client and server spans with no code change, and a W3C `traceparent` field is injected into outbound `_meta`, propagating your trace ids to the servers you call. To suppress the spans, filter the `mcp-python-sdk` tracer in your pipeline; [OpenTelemetry](run/opentelemetry.md) has the recipe for removing the server middleware. There is no public switch for the client-side span and `traceparent` injection.
2664
+
OpenTelemetry trace propagation ([SEP-414](https://github.com/modelcontextprotocol/modelcontextprotocol/pull/414)) now ships enabled: every server installs a tracing middleware and the client opens a span per outbound request. With no OpenTelemetry SDK configured these are no-ops and nothing is added to outbound requests. If your application already configures a global tracer provider, it starts recording MCP client and server spans with no code change, and a W3C `traceparent` field is injected into outbound `_meta`, propagating your trace ids to the servers you call. To suppress the spans, filter the `mcp-python-sdk` tracer in your pipeline; [OpenTelemetry](run/opentelemetry.md) has the recipe for removing the server middleware. There is no public switch for the client-side span and `traceparent` injection.
2681
2665
2682
2666
The SDK's new `opentelemetry-api` runtime dependency is covered under [Packaging, dependencies, and CLI](#packaging-dependencies-and-cli).
Copy file name to clipboardExpand all lines: docs/servers/tools.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
@@ -136,7 +136,7 @@ You can mix and match: plain parameters next to model parameters, nested models,
136
136
137
137
If a tool does I/O (calls an API, reads a file, queries a database), declare it `async def` and `await` inside it. The SDK awaits it.
138
138
139
-
A plain `def` tool works too: the SDK runs it in a thread so it never blocks the server.
139
+
A plain `def` tool works too: the SDK runs it in a thread so it never blocks the server. A long one can check whether the client is still waiting; see **[Cancellation](../handlers/cancellation.md)**.
0 commit comments