Перейти к содержанию

Отмена

Машинный перевод

Эта страница переведена с английской документации автоматически, и основной версией остаётся английская страница. Если что-то читается неправильно, на странице Переводы объясняется, как об этом сообщить.

Клиент может отказаться от вызова: пользователь нажал «стоп» или истёк тайм-аут.

В этом случае SDK отменяет обработчик. Выражение await, на котором он ждёт, выбрасывает исключение, стек функции раскручивается, и то, что она возвращает, никуда не отправляется. Большинству обработчиков ничего с этим делать не нужно.

Исключений два: обработчик, которому есть что за собой убрать, и обработчик, объявленный через обычный def.

Очистка в инструменте async def

Поместите очистку в блок finally:

server.py
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 выполняется в потоке, а поток нельзя прервать извне. Инструмент должен спросить сам:

server.py
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 (подключения старого поколения) отключают отмену.

Ход выполнения и отмена — это дело работающего инструмента и вызывающей стороны. Строки, которые он пишет в лог для вас, того, кто обслуживает сервер, идут по другому каналу: Логирование.