Saltar a contenido

Cancelación

Traducción automática

Esta página se tradujo automáticamente a partir de la documentación en inglés, y la página en inglés es la versión de referencia. Si algo no se lee bien, Traducciones explica cómo avisarnos.

Un cliente puede abandonar una llamada: el usuario pulsó detener o se agotó un timeout.

Cuando lo hace, el SDK cancela tu handler. El await en el que está esperando lanza una excepción, la ejecución sale de la función y no se envía nada de lo que devuelva. La mayoría de los handlers no necesita hacer nada al respecto.

Dos tipos sí: un handler que tiene algo que limpiar y un handler que es un def simple.

Limpiar en una herramienta async def

Pon la limpieza en un 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)
  • El finally se ejecuta termine como termine la herramienta: devolvió un valor, lanzó una excepción o se canceló.
  • La limpieza que tiene que hacer await necesita shield=True. En un handler cancelado, cada await posterior también lanza una excepción, así que sin el escudo release_hold se detendría en su primera línea.
  • Nada puede cancelar un bloque protegido con escudo, así que ponle un límite de tiempo. Aquí son 5 segundos.

Tip

Usa finally, no except. La cancelación tiene que seguir propagándose hacia arriba una vez terminada la limpieza, y un finally se lo permite.

Detenerse antes en una herramienta def simple

Una herramienta def simple se ejecuta en un hilo, y nada puede interrumpir un hilo desde fuera. La herramienta tiene que preguntar:

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() no hace nada mientras la llamada sigue activa, y lanza una excepción una vez que se ha cancelado. Llámala entre unidades de trabajo.
  • Aquí la limpieza también va en un finally. En un hilo no hay esperas asíncronas, así que no necesita escudo.
  • Una herramienta def que nunca pregunta se ejecuta hasta el final, y su resultado se descarta.

Dónde se aplica

Las funciones de prompts y de recursos se cancelan exactamente igual que las herramientas.

Funciona igual sobre stdio y Streamable HTTP. Con el Client de este SDK, abandonar significa cancelar la tarea que espera call_tool o dejar que se agote su read_timeout_seconds.

Warning

Dos opciones de Streamable HTTP impiden que la noticia llegue a tu handler: json_response=True en una conexión 2026-07-28 y stateless_http=True en una heredada. Ahí el handler se ejecuta hasta el final, haya hecho lo que haya hecho el cliente.

Resumen

  • Cuando el cliente abandona una llamada, el SDK cancela el handler: herramienta, prompt o recurso.
  • async def: limpia en un finally y pon la limpieza con esperas asíncronas dentro de anyio.move_on_after(seconds, shield=True).
  • def simple: llama a anyio.from_thread.check_cancelled() entre unidades de trabajo, o la herramienta se ejecuta hasta el final. Un finally simple hace la limpieza.
  • json_response=True (conexiones modernas) y stateless_http=True (las heredadas) desactivan la cancelación.

El progreso y la cancelación son cosa de una herramienta en ejecución y de quien la llama. Las líneas que registra para ti, la persona que opera el servidor, son un canal distinto: Registro de logs.