Document how a handler deals with cancellation
Merged
MA
14d ago+348+348+012345678901234567890123456789-2-2-0123456789
Fixes #3389.
This documents how a handler deals with cancellation using public API. It does not add a cancel_requested event to Context. That event is only set when a notifications/cancelled message arrives, and on 2026-07-28 Streamable HTTP cancelling means closing the request's stream, so a property backed by it would never fire there. Cancelling the handler itself already works on both.
What was added
docs/handlers/cancellation.md, a short page under "Inside your handler", after Progress- When the client gives up on a call (it cancels, or its timeout runs out), the SDK cancels the handler. Tools, prompts and resource functions alike.
- An
async deftool cleans up in afinally, and cleanup that has toawaitgoes insideanyio.move_on_after(seconds, shield=True). - A plain
deftool runs in a thread that nothing can interrupt, so it callsanyio.from_thread.check_cancelled()between units of work, and cleans up in a plainfinally. One that never checks runs to the end and its result is dropped. - A warning for the two Streamable HTTP configurations where the handler is not cancelled today:
json_response=Trueon a 2026-07-28 connection, andstateless_http=Trueon a legacy one.
docs_src/cancellation/tutorial001.pyandtutorial002.py, the two examples on the page- Links to the page from the section index, the end of the Progress page, and the "
async def" section of Tools tests/docs_src/test_cancellation.py
Nothing changes under src/, and the translated pages aren't regenerated here.
How it was checked
- The tests run both examples against a client that abandons the call, in process and over an in-memory JSON-RPC stream. The async one is also run over Streamable HTTP at both protocol eras, and against a client timeout.
- The async example's cleanup runs to its last line.
- The
defexample stops at its next check (three books requested, one indexed) and itsfinallyputs search back online. Left alone, it indexes all three and cleans up the same way. - A
deftool that never checks finishes anyway. - A prompt and a resource function are cancelled too.
- Under the two configurations in the warning the tool keeps running.
- Removing
shield=True, thecheck_cancelled()call, or thedefexample'sfinallymakes the tests fail. - Both examples were also run against a real stdio subprocess and a real uvicorn server, at both protocol eras. The hold was released about 100 ms after the client gave up, which is the example's own simulated round trip, and the index rebuild stopped after the book it was on.
./scripts/testpasses with 100% coverage. ruff, pyright, markdownlint and the strict docs build are clean.