Отмена
Машинный перевод
Эта страница переведена с английской документации автоматически, и основной версией остаётся английская страница. Если что-то читается неправильно, на странице Переводы объясняется, как об этом сообщить.
Клиент может отказаться от вызова: пользователь нажал «стоп» или истёк тайм-аут.
В этом случае SDK отменяет обработчик. Выражение await, на котором он ждёт, выбрасывает исключение, стек функции раскручивается, и то, что она возвращает, никуда не отправляется. Большинству обработчиков ничего с этим делать не нужно.
Исключений два: обработчик, которому есть что за собой убрать, и обработчик, объявленный через обычный def.
Очистка в инструменте async def
Поместите очистку в блок finally:
import anyio
from mcp.server import MCPServer
mcp = MCPServer("Bookshop")
holds: set[str] = set()
async def take_payment(title: str) -> None:
await anyio.sleep(30) # the customer is typing a card number
async def release_hold(title: str) -> None:
await anyio.sleep(0.1) # a round trip to the stock system
holds.discard(title)
@mcp.tool()
async def order_book(title: str) -> str:
"""Hold a copy of a book while the customer pays for it."""
holds.add(title)
try:
await take_payment(title)
return f"Ordered {title!r}."
finally:
with anyio.move_on_after(5, shield=True):
await release_hold(title)
- Блок
finallyвыполняется, как бы ни завершился инструмент: вернул результат, выбросил исключение или был отменён. - Очистке, которой нужен
await, требуетсяshield=True. В отменённом обработчике каждый следующийawaitтоже выбрасывает исключение, поэтому без защиты функцияrelease_holdостановилась бы на первой же строке. - Защищённый блок ничем нельзя отменить, поэтому ограничьте его по времени. Здесь это
5секунд.
Tip
Используйте finally, а не except. После очистки отмена должна распространяться дальше
вверх, и finally ей это позволяет.
Досрочная остановка в обычном инструменте def
Обычный инструмент def выполняется в потоке, а поток нельзя прервать извне. Инструмент должен спросить сам:
import time
import anyio.from_thread
from mcp.server import MCPServer
mcp = MCPServer("Bookshop")
offline: set[str] = set()
def index_book(title: str) -> None:
time.sleep(1) # slow work with nothing to await
@mcp.tool()
def rebuild_index(titles: list[str]) -> str:
"""Take search offline and rebuild its index, one book at a time."""
offline.add("search")
try:
for title in titles:
anyio.from_thread.check_cancelled()
index_book(title)
return f"Indexed {len(titles)} books."
finally:
offline.discard("search")
- Функция
anyio.from_thread.check_cancelled()ничего не делает, пока вызов активен, и выбрасывает исключение, как только он отменён. Вызывайте её между порциями работы. - Очистка и здесь помещается в
finally. В потоке нет асинхронных ожиданий, поэтому защита не нужна. - Инструмент
def, который ни разу не спрашивает, выполняется до конца, а его результат отбрасывается.
Где это работает
Функции промптов и ресурсов отменяются точно так же, как инструменты.
Через stdio и Streamable HTTP всё работает одинаково. Для класса Client из этого SDK отказаться от вызова — значит отменить задачу, которая ожидает call_tool, или дождаться, пока истечёт его тайм-аут read_timeout_seconds.
Warning
При двух параметрах Streamable HTTP обработчик об отмене не узнаёт: json_response=True на
подключении 2026-07-28 и stateless_http=True на подключении старого поколения. В этих
случаях обработчик выполняется до конца, что бы ни сделал клиент.
Итоги
- Когда клиент отказывается от вызова, SDK отменяет обработчик: инструмент, промпт или ресурс.
async def: выполняйте очистку вfinally, а очистку с асинхронным ожиданием помещайте вanyio.move_on_after(seconds, shield=True).- Обычный
def: вызывайтеanyio.from_thread.check_cancelled()между порциями работы, иначе инструмент выполнится до конца. Для очистки достаточно обычногоfinally. json_response=True(современные подключения) иstateless_http=True(подключения старого поколения) отключают отмену.
Ход выполнения и отмена — это дело работающего инструмента и вызывающей стороны. Строки, которые он пишет в лог для вас, того, кто обслуживает сервер, идут по другому каналу: Логирование.