MCP Tasks Extension: tareas asíncronas y en segundo plano explicadas
Las llamadas largas a herramientas caducan por tiempo de espera, se cortan y pierden trabajo. La extensión MCP Tasks lo soluciona con un taskId duradero, sondeo con tasks/get, pausas input_required y cancelación cooperativa. Incluye el ciclo de vida, los payloads JSON, un ejemplo de servidor FastMCP y hábitos de cliente que sobreviven a los fallos.
Tu agente llama a una herramienta, la herramienta necesita cuarenta minutos y, hacia el minuto dos, un proxy cierra la conexión. El trabajo puede seguir ejecutándose en el servidor, pero nadie puede acceder ya a él, y el modelo se queda con un error en lugar de una respuesta. Esa brecha es justo lo que la extensión MCP Tasks pretende cerrar. En lugar de mantener abierta una solicitud hasta que termina el trabajo, el servidor devuelve de inmediato un taskId duradero, y el cliente consulta cuando quiera.
Este artículo explica cómo funcionan las tareas asíncronas y en segundo plano en el Model Context Protocol: qué es la extensión, qué significa cada estado, cómo son los payloads, cómo construir un servidor con soporte de tareas y qué hábitos del cliente mantienen seguros los trabajos largos. Los nombres de campo que aparecen abajo provienen de la especificación publicada de la extensión (io.modelcontextprotocol/tasks, SEP-2663) y de la documentación de FastMCP.
Por qué fallan las llamadas bloqueantes
Una tools/call MCP estándar se comporta como un cliente de pie en un mostrador esperando su plato. La solicitud sale, la conexión se mantiene abierta y la respuesta vuelve por la misma línea. Para una consulta del tiempo, es perfecto. Para una canalización de CI, una importación masiva o un entrenamiento de modelo, es un mal negocio.
Un restaurante lo resuelve de otra forma. Nadie se queda junto al pase de cocina mirando al chef. El camarero engancha un ticket de papel en la barra, y ese ticket es el asidero del pedido. Las tareas dan a MCP la misma barra de tickets.
El problema del tiempo de espera
Muchos clientes e intermediarios de transporte imponen tiempos de espera que hacen impracticable mantener una solicitud abierta más de unos segundos. Los balanceadores de carga, los proxies corporativos y las pasarelas serverless cortan las conexiones silenciosas. Cuando eso ocurre, quien llama ve un fallo aunque el servidor siga ocupado, y la reacción natural es reintentar y lanzar dos veces el mismo trabajo costoso.
Trabajo perdido tras una desconexión
Una llamada bloqueante ata el resultado a la conexión. Se cierra la tapa del equipo portátil, un cliente en dispositivo móvil pierde el wifi o el proceso anfitrión se reinicia, y la respuesta no tiene adónde ir. Con una tarea, el ID es un asidero duradero: el cliente se reconecta, llama a tasks/get con el mismo ID y retoma exactamente donde se quedó.
💡 Regla general: si una operación tarda habitualmente más de unos segundos, o se detiene para una decisión humana, debe ir dentro de una tarea.
Qué añade la extensión Tasks
De la especificación base a la extensión
Las tareas empezaron como una función experimental en la especificación base de MCP. Desde entonces, el protocolo las ha sacado del núcleo y las ha colocado en una extensión opcional identificada como io.modelcontextprotocol/tasks, documentada en SEP-2663. Estar fuera del núcleo mantiene pequeño el protocolo base, mientras que los servidores y clientes que necesitan trabajos de larga duración se suman de forma deliberada. La documentación oficial describe el resultado como ejecución asíncrona de tareas para operaciones MCP de larga duración, y la especificación completa vive en el repositorio ext-tasks.
Un cambio merece atención. Las descripciones antiguas de la función mencionan una llamada tasks/result aparte. En la extensión, la salida final llega dentro de la respuesta tasks/get, lo que reduce el bucle del cliente a un único método de sondeo.
El identificador de la extensión y cómo activarla
El soporte se negocia, nunca se asume:
El cliente lista io.modelcontextprotocol/tasks en sus capacidades por solicitud, dentro de _meta bajo io.modelcontextprotocol/clientCapabilities.
El servidor anuncia la misma extensión en las capacidades que devuelve a los clientes.
Si un servidor exige soporte de tareas y el cliente nunca lo declaró, el servidor responde con el código de error -32003 y el mensaje Missing required client capability.
La decisión sobre cuándo crear una tarea corresponde al servidor. No existe un indicador por herramienta en el lado del cliente. El cliente activa la extensión una vez y debe estar preparado para dos formas de resultado: el resultado normal o un manejador de tarea. Hoy, tools/call es el único tipo de solicitud que puede producir una tarea.
Lado
Qué debe hacer
Por qué importa
Cliente
Declarar la extensión, gestionar dos formas de resultado
Un servidor nunca devuelve una tarea a un cliente que no se adhirió
Servidor
Anunciar la extensión, crear la tarea antes de responder
Un fallo justo después de la respuesta no puede dejar huérfano el ID
Ambos
Tratar taskId como el único manejador
Las reconexiones y los reinicios pasan a ser inofensivos
El ciclo de vida de la tarea
Obtener un manejador y sondear
El flujo tiene cinco pasos:
El cliente envía tools/call con la capacidad de tareas incluida.
El servidor decide que el trabajo es largo y devuelve un CreateTaskResult marcado como resultType: "task".
La tarea se crea de forma duradera antes de que esa respuesta salga del servidor.
El cliente llama a tasks/get con el taskId, esperando al menos pollIntervalMs entre llamadas.
Cada respuesta incluye el estado actual y, una vez que la tarea es terminal, el resultado o el error.
Aquí tienes una vista simplificada de una tarea recién creada. El sobre exacto está definido en la especificación, así que tómalo como una ilustración de los campos:
El servidor necesita una entrada del cliente, consulta inputRequests
No
completed
La operación terminó, el campo result contiene la salida
Sí
failed
Se produjo un error JSON-RPC, el campo error tiene los detalles
Sí
cancelled
Detenida por solicitud, aunque no siempre se respeta
Sí
Una vez que una tarea alcanza un estado terminal, su estado ya no cambia. tasks/get es idempotente, así que sondear diez veces es exactamente tan seguro como sondear una.
Pausar para la entrada humana
Algunos trabajos llegan a un punto de decisión a mitad de camino: aprobar un despliegue, confirmar una compra, elegir una de tres opciones. La tarea pasa a input_required, y la siguiente respuesta tasks/get incluye un mapa inputRequests con las elicitaciones u otras solicitudes del servidor.
El cliente muestra esas solicitudes a una persona o a un modelo, y luego responde con tasks/update, enviando inputResponses que correspondan a las solicitudes pendientes. El servidor confirma con un resultado vacío e ignora las respuestas de entradas desconocidas o ya satisfechas. Cuando todas las solicitudes tienen respuesta, el servidor continúa.
💡 Por qué es elegante: no hace falta una segunda conexión ni un mensaje no solicitado del servidor al cliente. El paso humano viaja por el mismo bucle de sondeo que todo lo demás.
Terminar, fallar y cancelar
Cuando la tarea termina con éxito, el campo result contiene lo que la solicitud original habría devuelto de forma síncrona. En una llamada a herramienta, eso significa los mismos bloques de contenido que habría producido una llamada bloqueante. Cuando el estado es failed, el campo error contiene el error JSON-RPC.
La cancelación usa tasks/cancel. El servidor confirma con un resultado vacío, pero la cancelación es cooperativa. El trabajo puede haber superado ya el punto de no retorno, así que una tarea puede acabar en un estado terminal diferente.
Los servidores también pueden enviar actualizaciones mediante notifications/tasks. Los clientes se suscriben mediante subscriptions/listen, y cada notificación lleva el estado completo de la tarea, con la misma forma que devolvería una respuesta tasks/get.
Cómo crear un servidor de tareas
Una herramienta FastMCP mínima
FastMCP 4.0 añadió soporte para la extensión. Instalas fastmcp-tasks, registras TasksExtension y marcas la herramienta como compatible con tareas:
import asyncio
from fastmcp import FastMCP
from fastmcp_tasks import TasksExtension
mcp = FastMCP("ReportServer")
mcp.add_extension(TasksExtension())
@mcp.tool(task=True)
async def slow_computation(duration: int) -> str:
"""A long-running operation."""
for i in range(duration):
await asyncio.sleep(1)
return f"Finished in {duration} seconds"
Dos detalles importan aquí. Las tareas en segundo plano requieren funciones asíncronas, y usar task=True en una función síncrona provoca un ValueError en el momento del registro. Y task=True solo indica que la herramienta puede ejecutarse en segundo plano. Si lo hace realmente depende de que el cliente active la extensión y del modo de ejecución del servidor. La documentación señala que Docket impulsa el planificador distribuido, que es lo que hace la configuración lista para producción.
Progreso y modos de ejecución
Las herramientas informan del progreso mediante una dependencia Progress inyectada, y de ahí sale el statusMessage que muestran tus clientes:
Para un control más fino, sustituye el booleano por un TaskConfig. Tres modos deciden cómo se comporta la herramienta:
Modo
Comportamiento
optional
Se ejecuta de forma síncrona para clientes heredados y en segundo plano para los que soportan tareas
required
Da error si el cliente no tiene soporte de tareas; en otro caso se ejecuta en segundo plano
forbidden
Siempre síncrono, nunca en segundo plano
Los atajos se corresponden de forma clara: task=True equivale a optional, y task=False equivale a forbidden. También puedes sugerir una cadencia de sondeo con poll_interval=timedelta(seconds=2).
Patrones de cliente que aguantan
Sondea con cortesía, guarda todo
Un cliente que habla con servidores con soporte de tareas necesita cinco hábitos:
Declara la extensión en las capacidades por solicitud.
Gestiona los resultados polimórficos, ya que un tools/call puede devolver un resultado normal o una tarea.
Respeta pollIntervalMs, porque el servidor puede cambiarlo entre respuestas.
Responde a inputRequests mediante tasks/update en lugar de ignorarlas.
Guarda los IDs de tarea de forma duradera para que el sondeo pueda reanudarse tras un fallo o un reinicio.
El bucle siguiente es pseudocódigo, no está ligado a ningún SDK concreto:
async def run_tool(session, name, args):
reply = await session.call_tool(name, args)
if reply.get("resultType") != "task":
return reply # ordinary synchronous result
task = reply
store.save(task["taskId"]) # survive a crash
while task["status"] in ("working", "input_required"):
if task["status"] == "input_required":
answers = await ask_user(task["inputRequests"])
await session.request("tasks/update", {
"taskId": task["taskId"],
"inputResponses": answers,
})
await asyncio.sleep(task["pollIntervalMs"] / 1000)
task = await session.request("tasks/get", {"taskId": task["taskId"]})
if task["status"] == "failed":
raise RuntimeError(task["error"])
return task.get("result")
El viajero en un tren es el modelo mental. La conexión se corta en cada túnel, pero el billete que lleva en el bolsillo sigue siendo válido. Un cliente construido así se reconecta tras el túnel y sigue adelante.
Notificaciones en lugar de sondeo
El sondeo es la opción por defecto, y funciona en todas partes. Si un servidor soporta notifications/tasks, un cliente puede suscribirse una vez y evitar la mayoría de los viajes de ida y vuelta de tasks/get, ya que cada notificación ya contiene el estado completo de la tarea. Mantén el sondeo como alternativa para los servidores que no envían notificaciones.
Errores que conviene evitar
Los manejadores de tareas se comportan como paquetes en la trastienda de una oficina de correos. Si se dejan demasiado tiempo, se retiran. Estas son las trampas que aparecen con más frecuencia:
Error
Qué sale mal
Solución
Ignorar ttlMs
La tarea caduca antes de que un cliente lento lea el resultado
Lee los resultados pronto y da al servidor un TTL acorde al comportamiento real de los clientes
Sondear más rápido que pollIntervalMs
Peticiones desperdiciadas y carga evitable
Espera el intervalo sugerido
Tratar la cancelación como instantánea
La interfaz dice que el trabajo se detuvo mientras sigue ejecutándose
Espera a un estado terminal antes de informarlo
Devolver una tarea a un cliente que nunca se adhirió
El cliente no puede leer la respuesta
Comprueba antes las capacidades declaradas
Envolver cada herramienta en una tarea
Las llamadas rápidas ganan latencia sin motivo
Deja que las operaciones rápidas devuelvan el resultado normal
Compartir los IDs de tarea sin control
Otro llamante podría leer la salida de otra persona
Trata el ID como un manejador y vincúlalo al llamante autenticado (buena práctica, más allá de lo que enumera la especificación)
Un ttlMs de null significa ilimitado, lo que suena amable hasta que el almacenamiento se llena de trabajos terminados que nadie recoge.
Combinar las tareas con PicassoIA
Los medios generativos son el ejemplo típico de trabajo de larga duración, por eso los patrones de tareas encajan de forma natural junto a las herramientas de imagen y video. Dos modelos de PicassoIA encajan directamente en un flujo de tareas.
Claude Sonnet 5 se encarga de la programación en varios pasos y del uso de herramientas, así que es un buen compañero de programación para el código del manejador de este artículo. Otras opciones de la misma categoría incluyen GPT 5.6 Sol, si quieres una segunda opinión sobre el mismo código.
Pega la firma de tu herramienta y los campos de tarea de este artículo en el cuadro Prompt, y pide un manejador asíncrono con mensajes de estado.
Fija Effort en high para máquinas de estados complicadas, o déjalo en low para ediciones rápidas.
Añade un System Prompt como "Escribe en Python, solo asíncrono, sin llamadas bloqueantes" para que todas las respuestas mantengan el mismo estilo.
Sube Max Tokens por encima del valor predeterminado de 8.192 si quieres que los tests se generen en la misma respuesta.
Ejecútalo, lee el resultado y pega el manejador en tu proyecto.
💡 Consejo: adjunta una captura de pantalla de un error con el campo Image. El modelo la lee como contexto.
Recortes limpios para diagramas
La documentación sobre flujos de tareas suele necesitar visuales limpios: una foto de un dispositivo para una tarjeta de estado, un logotipo para una diapositiva de arquitectura. Remove Background devuelve un PNG transparente en segundos, y su ajuste Preserve Partial Alpha mantiene naturales los bordes suaves. Desactívalo cuando quieras bordes duros y totalmente opacos para fotos de producto.
Para imágenes de escena nuevas, Flux 2 Pro y P Image convierten un prompt escrito en una foto apta para la cabecera de un blog.
Tu siguiente paso: crea tus propias imágenes
Ya tienes el panorama completo: un manejador en lugar de una conexión retenida, cinco estados, tres métodos y una lista corta de hábitos que evitan que los trabajos largos desaparezcan. La mejor forma de que se quede es construir algo pequeño. Escribe una herramienta que tarde diez segundos, márcala como task=True y observa cómo el estado pasa de working a un estado terminal.
Después, ponle imagen al proyecto. Abre Picasso IA, elige un modelo de texto a imagen y genera una imagen de cabecera para tu artículo. Prueba con ángulos de cámara, luz y detalles de lente en tus prompts, elimina un fondo para lograr un logotipo limpio y comprueba lo rápido que una idea se convierte en un recurso visual terminado. Tu próximo resultado de tarea merece una imagen que valga la pena compartir.