Extensão Tasks do MCP: tarefas assíncronas e em segundo plano explicadas
Chamadas de ferramentas longas expiram, perdem conexões e desperdiçam trabalho. A extensão Tasks do MCP resolve isso com um taskId durável, polling via tasks/get, pausas input_required e cancelamento cooperativo. Veja o ciclo de vida, payloads em JSON, um exemplo de servidor FastMCP e hábitos de cliente que sobrevivem a quedas.
Seu agente chama uma ferramenta, a ferramenta precisa de quarenta minutos e, por volta do segundo minuto, um proxy fecha a conexão. O job pode continuar rodando no servidor, mas ninguém consegue mais alcançá-lo, e o modelo fica com um erro no lugar de uma resposta. É exatamente essa lacuna que a extensão Tasks do MCP foi criada para fechar. Em vez de manter uma requisição aberta até o trabalho terminar, o servidor devolve de imediato um taskId durável, e o cliente consulta quando quiser.
Este artigo mostra como funcionam as tarefas assíncronas e em segundo plano no Model Context Protocol: o que é a extensão, o que significa cada status, como são os payloads, como construir um servidor com suporte a tarefas e quais hábitos do cliente mantêm jobs longos seguros. Os nomes de campos abaixo vêm da especificação publicada da extensão (io.modelcontextprotocol/tasks, SEP-2663) e da documentação do FastMCP.
Por que as chamadas bloqueantes quebram
Uma chamada tools/call padrão do MCP se comporta como um cliente parado no balcão esperando o prato. A requisição sai, a conexão continua aberta e a resposta volta pela mesma linha. Para uma consulta de clima, isso é perfeito. Para um pipeline de CI, uma importação em massa ou um treinamento de modelo, é um mau negócio.
Um restaurante resolve isso de outro jeito. Ninguém fica parado no passe olhando para o chef. O garçom prende uma comanda de papel no trilho, e essa comanda é a alça do pedido. As tarefas dão ao MCP esse mesmo trilho de comandas.
O problema do timeout
Muitos clientes e intermediários de transporte impõem timeouts que tornam inviável manter uma requisição aberta por mais de alguns segundos. Load balancers, proxies corporativos e gateways serverless derrubam conexões silenciosas. Quando isso acontece, quem chama vê uma falha, mesmo que o servidor ainda esteja ocupado, e a reação natural é tentar de novo e disparar o mesmo job caro duas vezes.
Trabalho perdido após uma desconexão
Uma chamada bloqueante prende o resultado à conexão. A tampa do notebook fecha, um cliente móvel perde o wifi ou o processo host reinicia, e a resposta não tem para onde ir. Com uma tarefa, o ID é uma alça durável: o cliente reconecta, chama tasks/get com o mesmo ID e retoma exatamente de onde parou.
💡 Regra prática: se uma operação costuma levar mais do que alguns segundos, ou pausa para uma decisão humana, ela deve ficar atrás de uma tarefa.
O que a extensão Tasks acrescenta
Da especificação central à extensão
As tarefas começaram como um recurso experimental na especificação central do MCP. Desde então, o protocolo as tirou do núcleo e as colocou numa extensão opcional identificada como io.modelcontextprotocol/tasks, documentada na SEP-2663. Ficar fora do núcleo mantém o protocolo base enxuto, enquanto servidores e clientes que precisam de trabalho de longa duração optam por usá-la de forma deliberada. A documentação oficial descreve o resultado como execução assíncrona de tarefas para operações longas do MCP, e a especificação completa está no repositório ext-tasks.
Uma mudança merece atenção. Descrições antigas do recurso mencionam uma chamada tasks/result separada. Na extensão, a saída final chega dentro da resposta tasks/get, o que reduz o loop do cliente a um único método de polling.
O identificador da extensão e a adesão
O suporte é negociado, nunca presumido:
O cliente lista io.modelcontextprotocol/tasks nas capacidades por requisição, dentro de _meta, em io.modelcontextprotocol/clientCapabilities.
O servidor anuncia a mesma extensão nas capacidades que devolve aos clientes.
Se um servidor exige suporte a tarefas e o cliente nunca o declarou, o servidor responde com o código de erro -32003 e a mensagem Missing required client capability.
A decisão sobre quando criar uma tarefa cabe ao servidor. Não existe flag por ferramenta do lado do cliente. O cliente adere uma vez e precisa estar preparado para dois formatos de resultado: o resultado normal ou um identificador de tarefa. Hoje, tools/call é o único tipo de requisição que pode gerar uma tarefa.
Lado
O que deve fazer
Por que importa
Cliente
Declarar a extensão, tratar dois formatos de resultado
Um servidor nunca devolve uma tarefa a um cliente que não aderiu
Servidor
Anunciar a extensão, criar a tarefa antes de responder
Uma queda logo após a resposta não pode deixar o ID órfão
Ambos
Tratar taskId como a única alça
Reconexões e reinicializações deixam de ser problema
O ciclo de vida de uma tarefa
Obter a alça e fazer polling
O fluxo tem cinco etapas:
O cliente envia tools/call com a capacidade de tarefas anexada.
O servidor decide que o trabalho é longo e devolve um CreateTaskResult marcado como resultType: "task".
A tarefa é criada de forma durável antes que essa resposta saia do servidor.
O cliente chama tasks/get com o taskId, esperando pelo menos pollIntervalMs entre as chamadas.
Cada resposta traz o status atual e, quando a tarefa chega a um estado final, o resultado ou o erro.
Aqui está uma visão simplificada de uma tarefa recém-criada. O envelope exato está definido na especificação, então trate o exemplo como uma ilustração dos campos:
O servidor precisa de uma entrada do cliente, veja inputRequests
Não
completed
A operação terminou, o campo result contém a saída
Sim
failed
Ocorreu um erro JSON-RPC, o campo error traz os detalhes
Sim
cancelled
Interrompida por solicitação, embora nem sempre seja atendida
Sim
Quando uma tarefa atinge um status final, seu estado nunca mais muda. tasks/get é idempotente, então fazer polling dez vezes é tão seguro quanto fazer uma vez.
Pausar para a entrada humana
Alguns jobs chegam a um ponto de decisão no meio do caminho: aprovar um deploy, confirmar uma compra, escolher uma entre três opções. A tarefa passa para input_required, e a próxima resposta de tasks/get inclui um mapa inputRequests com elicitações ou outras requisições do servidor.
O cliente mostra essas requisições a um usuário ou a um modelo e responde com tasks/update, enviando inputResponses que correspondem às requisições pendentes. O servidor confirma com um resultado vazio e ignora respostas para entradas desconhecidas ou já atendidas. Quando todas as requisições têm resposta, o servidor segue em frente.
💡 Por que isso é prático: não há segunda conexão nem mensagem não solicitada do servidor para o cliente. A etapa humana usa o mesmo loop de polling de todo o resto.
Finalizar, falhar e cancelar
Quando a tarefa termina com sucesso, o campo result contém o que a requisição original teria devolvido de forma síncrona. Para uma chamada de ferramenta, isso significa os mesmos blocos de conteúdo que uma chamada bloqueante produziria. Quando o status é failed, o campo error traz o erro JSON-RPC.
O cancelamento usa tasks/cancel. O servidor confirma com um resultado vazio, mas o cancelamento é cooperativo. O trabalho pode já ter passado do ponto sem volta, então a tarefa ainda pode terminar em outro status final.
Os servidores também podem enviar atualizações por meio de notifications/tasks. Os clientes aderem por meio de subscriptions/listen, e cada notificação traz o estado completo da tarefa, no mesmo formato que uma resposta de tasks/get devolveria.
Como construir um servidor de tarefas
Uma ferramenta mínima com FastMCP
O FastMCP 4.0 adicionou suporte à extensão. Você instala fastmcp-tasks, registra TasksExtension e marca a ferramenta como compatível com tarefas:
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"
Dois detalhes importam aqui. Tarefas em segundo plano exigem funções assíncronas, e usar task=True em uma função síncrona gera um ValueError no momento do registro. E task=True apenas sinaliza que a ferramenta pode rodar em segundo plano. Se ela de fato roda depende de o cliente aderir e do modo de execução do servidor. A documentação observa que o Docket alimenta o agendador distribuído, que é o que torna a configuração pronta para produção.
Progresso e modos de execução
As ferramentas informam o progresso por meio de uma dependência injetada Progress, e é daí que vem o statusMessage que seus clientes exibem:
Para um controle mais fino, substitua o booleano por um TaskConfig. Três modos definem como a ferramenta se comporta:
Modo
Comportamento
optional
Roda de forma síncrona para clientes legados e em segundo plano para clientes com suporte a tarefas
required
Gera erro se o cliente não tiver suporte a tarefas, caso contrário roda em segundo plano
forbidden
Sempre síncrono, nunca em segundo plano
Os atalhos correspondem diretamente: task=True equivale a optional, e task=False equivale a forbidden. Você também pode sugerir um intervalo de polling com poll_interval=timedelta(seconds=2).
Padrões de cliente que se sustentam
Faça polling com moderação, persista tudo
Um cliente que conversa com servidores com suporte a tarefas precisa de cinco hábitos:
Declarar a extensão nas capacidades por requisição.
Tratar resultados polimórficos, já que um tools/call pode devolver um resultado normal ou uma tarefa.
Respeitar pollIntervalMs, porque o servidor pode alterá-lo entre as respostas.
Responder a inputRequests por meio de tasks/update em vez de ignorá-las.
Armazenar os IDs de tarefa de forma durável para que o polling possa ser retomado após uma queda ou reinicialização.
O loop abaixo é um pseudocódigo, não preso a um SDK específico:
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")
O passageiro no trem é o modelo mental. A conexão cai em cada túnel, mas o bilhete no bolso continua válido. Um cliente construído assim reconecta depois do túnel e segue em frente.
Notificações em vez de polling
O polling é o padrão e funciona em qualquer lugar. Se um servidor oferece suporte a notifications/tasks, um cliente pode se inscrever uma vez e dispensar a maior parte das idas e vindas de tasks/get, já que cada notificação já contém o estado completo da tarefa. Mantenha o polling como alternativa para servidores que não enviam atualizações.
Erros a evitar
Os identificadores de tarefa se comportam como encomendas num depósito de correio. Deixe uma encomenda parada tempo demais e ela é descartada. Estas são as armadilhas que mais aparecem:
Erro
O que dá errado
Correção
Ignorar ttlMs
A tarefa expira antes que um cliente lento leia o resultado
Leia os resultados logo e defina um TTL no servidor que caiba no comportamento real dos clientes
Fazer polling mais rápido que pollIntervalMs
Requisições desperdiçadas e carga evitável
Aguarde o intervalo sugerido
Tratar o cancelamento como instantâneo
A interface diz que o job parou enquanto ele continua rodando
Aguarde um status final antes de informar isso
Devolver uma tarefa a um cliente que nunca aderiu
O cliente não consegue ler a resposta
Verifique antes as capacidades declaradas
Colocar toda ferramenta em uma tarefa
Chamadas rápidas ganham latência sem motivo
Deixe as operações rápidas retornarem normalmente
Compartilhar IDs de tarefa sem cuidado
Outro chamador poderia ler a saída de outra pessoa
Trate o ID como uma alça e vincule-o ao chamador autenticado (boa prática, além do que a especificação lista)
Um ttlMs de null significa ilimitado, o que parece amigável até o armazenamento encher de jobs finalizados que ninguém recolhe.
Combinando tarefas com o PicassoIA
Mídia generativa é o exemplo clássico de job longo, e por isso os padrões de tarefas combinam naturalmente com ferramentas de imagem e vídeo. Dois modelos do PicassoIA se encaixam direto em um fluxo de tarefas.
O Claude Sonnet 5 lida com programação em várias etapas e trabalho com uso de ferramentas, então é um bom parceiro de programação para o código dos handlers deste artigo. Outras opções na mesma categoria incluem o GPT 5.6 Sol, se você quiser uma segunda opinião sobre o mesmo código.
Cole a assinatura da sua ferramenta e os campos de tarefa deste artigo na caixa Prompt, depois peça um handler assíncrono com mensagens de status.
Defina Effort como high para máquinas de estado complicadas, ou deixe em low para edições rápidas.
Adicione um System Prompt como "Escreva em Python, apenas async, sem chamadas bloqueantes" para que todas as respostas mantenham o mesmo estilo.
Aumente Max Tokens acima do padrão de 8.192 se quiser que os testes sejam gerados na mesma resposta.
Execute, leia o resultado e cole o handler no seu projeto.
💡 Dica: anexe uma captura de tela de um erro no campo Image. O modelo a lê como contexto.
Recortes limpos para diagramas
A documentação sobre fluxos de tarefas costuma precisar de imagens limpas: a foto de um dispositivo para um cartão de status, um logotipo para um slide de arquitetura. O Remove Background devolve um PNG transparente em segundos, e a configuração Preserve Partial Alpha mantém as bordas suaves naturais. Desative-a quando quiser bordas definidas e totalmente opacas para fotos de produto.
Para imagens de cena novas, o Flux 2 Pro e o P Image transformam um prompt escrito em uma foto adequada para o cabeçalho de um blog.
Crie suas próprias imagens a seguir
Agora você tem o quadro completo: uma alça no lugar de uma conexão presa, cinco status, três métodos e uma lista curta de hábitos que impedem que jobs longos desapareçam. A melhor forma de fixar isso é construir algo pequeno. Escreva uma ferramenta que leve dez segundos, marque-a como task=True e acompanhe o status passando de working até um estado final.
Depois, dê um rosto ao projeto. Abra o Picasso IA, escolha um modelo de texto para imagem e gere uma imagem de cabeçalho para o seu texto. Experimente ângulos de câmera, luz e detalhes de lente nos seus prompts, remova um fundo para ter um logotipo limpo e veja como uma ideia vira um visual pronto em pouco tempo. O resultado da sua próxima tarefa merece uma imagem que valha a pena compartilhar.