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.

MCP Tasks Extension: tareas asíncronas y en segundo plano explicadas
Cristian Da Conceicao
Fundador de Picasso IA

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

Tickets de pedido de papel sujetos a un riel de acero en la mesa de pase de una cocina de restaurante concurrida

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

La mano de un cliente recibiendo un ticket de reclamación de papel numerado sobre el mostrador de un taller de reparaciones de madera

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.

LadoQué debe hacerPor qué importa
ClienteDeclarar la extensión, gestionar dos formas de resultadoUn servidor nunca devuelve una tarea a un cliente que no se adhirió
ServidorAnunciar la extensión, crear la tarea antes de responderUn fallo justo después de la respuesta no puede dejar huérfano el ID
AmbosTratar taskId como el único manejadorLas reconexiones y los reinicios pasan a ser inofensivos

El ciclo de vida de la tarea

Obtener un manejador y sondear

Vista por encima del hombro de un desarrollador revisando una terminal en un equipo portátil en un despacho en casa con luz de sol

El flujo tiene cinco pasos:

  1. El cliente envía tools/call con la capacidad de tareas incluida.
  2. El servidor decide que el trabajo es largo y devuelve un CreateTaskResult marcado como resultType: "task".
  3. La tarea se crea de forma duradera antes de que esa respuesta salga del servidor.
  4. El cliente llama a tasks/get con el taskId, esperando al menos pollIntervalMs entre llamadas.
  5. 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:

{
  "resultType": "task",
  "taskId": "tsk_8f3a91c2",
  "status": "working",
  "statusMessage": "Rendering 120 pages",
  "createdAt": "2026-10-06T09:00:00Z",
  "lastUpdatedAt": "2026-10-06T09:00:04Z",
  "ttlMs": 3600000,
  "pollIntervalMs": 2000
}

Cinco estados describen cualquier tarea:

EstadoSignificado¿Terminal?
workingLa operación está en cursoNo
input_requiredEl servidor necesita una entrada del cliente, consulta inputRequestsNo
completedLa operación terminó, el campo result contiene la salidaSí
failedSe produjo un error JSON-RPC, el campo error tiene los detallesSí
cancelledDetenida por solicitud, aunque no siempre se respetaSí

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

Vista cenital de las manos de un gerente estampando una aprobación sobre una pila de formularios impresos

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

Perfil lateral de un programador escribiendo en un despacho en casa con poca luz al atardecer

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:

@mcp.tool(task=True)
async def process_files(
    files: list[str],
    progress: Progress = Progress()
) -> str:
    await progress.set_total(len(files))
    for file in files:
        await progress.set_message(f"Processing {file}")
        await progress.increment()
    return f"Processed {len(files)} files"

Para un control más fino, sustituye el booleano por un TaskConfig. Tres modos deciden cómo se comporta la herramienta:

ModoComportamiento
optionalSe ejecuta de forma síncrona para clientes heredados y en segundo plano para los que soportan tareas
requiredDa error si el cliente no tiene soporte de tareas; en otro caso se ejecuta en segundo plano
forbiddenSiempre 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).

Pasillo simétrico entre filas de racks de servidores negros dentro de un centro de datos

Patrones de cliente que aguantan

Sondea con cortesía, guarda todo

Viajero con un teléfono en un tren de la mañana lluviosa

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

Estantería de paquetes de cartón marrón con etiquetas escritas a mano en la trastienda de una oficina de correos

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:

ErrorQué sale malSolución
Ignorar ttlMsLa tarea caduca antes de que un cliente lento lea el resultadoLee los resultados pronto y da al servidor un TTL acorde al comportamiento real de los clientes
Sondear más rápido que pollIntervalMsPeticiones desperdiciadas y carga evitableEspera el intervalo sugerido
Tratar la cancelación como instantáneaLa interfaz dice que el trabajo se detuvo mientras sigue ejecutándoseEspera a un estado terminal antes de informarlo
Devolver una tarea a un cliente que nunca se adhirióEl cliente no puede leer la respuestaComprueba antes las capacidades declaradas
Envolver cada herramienta en una tareaLas llamadas rápidas ganan latencia sin motivoDeja que las operaciones rápidas devuelvan el resultado normal
Compartir los IDs de tarea sin controlOtro llamante podría leer la salida de otra personaTrata 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.

Cómo usar Claude Sonnet 5

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.

  1. Abre la página de Claude Sonnet 5 en Picasso IA.
  2. 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.
  3. Fija Effort en high para máquinas de estados complicadas, o déjalo en low para ediciones rápidas.
  4. Añade un System Prompt como "Escribe en Python, solo asíncrono, sin llamadas bloqueantes" para que todas las respuestas mantengan el mismo estilo.
  5. Sube Max Tokens por encima del valor predeterminado de 8.192 si quieres que los tests se generen en la misma respuesta.
  6. 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

Mesa de estudio de un fotógrafo de producto con una taza de cerámica sobre papel blanco y un equipo portátil que muestra el recorte

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.

Compartir este artículo

Elige tu idioma