Extension MCP Tasks : tâches asynchrones et en arrière-plan expliquées

Les appels d’outils longs expirent, perdent la connexion et font perdre le travail. L’extension MCP Tasks règle ce problème avec un taskId durable, un polling via tasks/get, des pauses input_required et une annulation coopérative. Découvrez le cycle de vie, les payloads JSON, un exemple de serveur FastMCP et les bonnes habitudes côté client qui tiennent bon face aux plantages.

Extension MCP Tasks : tâches asynchrones et en arrière-plan expliquées
Cristian Da Conceicao
Fondateur de Picasso IA

Votre agent appelle un outil, celui-ci a besoin de quarante minutes et, vers la deuxième minute, un proxy coupe la connexion. Le travail tourne peut-être encore sur le serveur, mais personne ne peut plus l’atteindre, et le modèle se retrouve avec une erreur au lieu d’une réponse. C’est précisément ce manque que l’extension MCP Tasks a été conçue pour combler. Au lieu de garder une requête ouverte jusqu’à la fin du travail, le serveur renvoie immédiatement un taskId durable, et le client revient vérifier quand il le souhaite.

Cet article présente le fonctionnement des tâches asynchrones et en arrière-plan dans le Model Context Protocol : ce qu’est l’extension, ce que signifie chaque statut, à quoi ressemblent les payloads, comment construire un serveur capable de gérer des tâches et quelles habitudes côté client gardent les longs travaux en sécurité. Les noms de champs ci-dessous proviennent de la spécification publiée de l’extension (io.modelcontextprotocol/tasks, SEP-2663) et de la documentation FastMCP.

Pourquoi les appels bloquants posent problème

Tickets de commande en papier fixés sur une tringle en acier dans le passe d’une cuisine de restaurant animée

Un tools/call MCP standard se comporte comme un client debout au comptoir qui attend son plat. La requête part, la connexion reste ouverte et la réponse revient sur la même ligne. Pour une recherche météo, c’est parfait. Pour un pipeline CI, un import en masse ou un entraînement de modèle, c’est une mauvaise affaire.

Un restaurant règle ce problème autrement. Personne ne reste planté devant le passe à fixer le chef. Le serveur accroche un ticket en papier à la tringle, et ce ticket est la poignée de la commande. Les tâches donnent à MCP la même tringle à tickets.

Le problème des délais d’attente

De nombreux clients et intermédiaires de transport imposent des délais d’attente qui rendent impraticable le maintien d’une requête ouverte au-delà de quelques secondes. Les équilibreurs de charge, les proxys d’entreprise et les passerelles serverless coupent les connexions silencieuses. Quand l’un d’eux le fait, l’appelant voit un échec alors que le serveur est toujours occupé, et la réaction naturelle est de relancer deux fois le même travail coûteux.

Travail perdu après une déconnexion

Un appel bloquant lie le résultat à la connexion. Le couvercle de votre ordinateur portable se referme, un client mobile perd le wifi, ou le processus hôte redémarre, et la réponse n’a plus nulle part où aller. Avec une tâche, l’ID est une poignée durable : le client se reconnecte, appelle tasks/get avec le même ID et reprend exactement là où il s’était arrêté.

💡 Règle pratique : si une opération dure régulièrement plus de quelques secondes, ou attend une décision humaine, elle doit passer par une tâche.

Ce qu’ajoute l’extension Tasks

Main d’un client recevant un ticket de prise en charge numéroté en papier au-dessus du comptoir en bois d’un atelier de réparation

De la spécification de base à l’extension

Les tâches ont d’abord été une fonctionnalité expérimentale de la spécification MCP de base. Le protocole les a depuis sorties du cœur pour les placer dans une extension optionnelle identifiée par io.modelcontextprotocol/tasks, décrite dans SEP-2663. Rester hors du cœur garde le protocole de base léger, tandis que les serveurs et clients qui ont besoin de travaux de longue durée s’y inscrivent volontairement. La documentation officielle décrit le résultat comme une exécution asynchrone de tâches pour les opérations MCP de longue durée, et la spécification complète se trouve dans le dépôt ext-tasks.

Un changement mérite attention. Les anciennes descriptions de la fonctionnalité mentionnent un appel tasks/result distinct. Dans l’extension, le résultat final arrive dans la réponse tasks/get, ce qui réduit la boucle côté client à une seule méthode de polling.

L’identifiant de l’extension et l’inscription

La prise en charge se négocie, elle n’est jamais supposée :

  • Le client déclare io.modelcontextprotocol/tasks dans ses capacités par requête, dans _meta sous io.modelcontextprotocol/clientCapabilities.
  • Le serveur annonce la même extension dans les capacités qu’il renvoie aux clients.
  • Si un serveur exige la prise en charge des tâches et que le client ne l’a jamais déclarée, le serveur répond avec le code d’erreur -32003 et le message Missing required client capability.

La décision de quand créer une tâche appartient au serveur. Il n’existe pas d’indicateur par outil côté client. Le client s’inscrit une fois et doit être prêt à deux formes de résultat : le résultat normal ou un descripteur de tâche. Aujourd’hui, tools/call est le seul type de requête capable de produire une tâche.

CôtéCe qu’il doit fairePourquoi c’est important
ClientDéclarer l’extension, gérer deux formes de résultatUn serveur ne renvoie jamais une tâche à un client qui ne s’est pas inscrit
ServeurAnnoncer l’extension, créer la tâche avant de répondreUn plantage juste après la réponse ne peut pas rendre l’ID orphelin
Les deuxTraiter taskId comme seule poignéeReconnexions et redémarrages deviennent sans danger

Le cycle de vie d’une tâche

Obtenir une poignée et interroger

Vue par-dessus l’épaule d’un développeur consultant un terminal sur son portable dans un bureau à domicile ensoleillé

Le flux compte cinq étapes :

  1. Le client envoie tools/call avec la capacité tasks attachée.
  2. Le serveur juge que le travail est long et renvoie un CreateTaskResult marqué resultType: "task".
  3. La tâche est créée de façon durable avant que la réponse ne quitte le serveur.
  4. Le client appelle tasks/get avec le taskId, en attendant au moins pollIntervalMs entre deux appels.
  5. Chaque réponse porte le statut actuel et, une fois la tâche terminée, le résultat ou l’erreur.

Voici une vue simplifiée d’une tâche fraîchement créée. L’enveloppe exacte est définie dans la spécification, considérez donc ceci comme une illustration des champs :

{
  "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
}

Cinq statuts décrivent chaque tâche :

StatutSignificationTerminal ?
workingL’opération est en coursNon
input_requiredLe serveur attend une saisie du client, voir inputRequestsNon
completedL’opération est terminée, le champ result contient le résultatOui
failedUne erreur JSON-RPC s’est produite, le champ error donne les détailsOui
cancelledArrêtée sur demande, bien que pas toujours respectéeOui

Une fois qu’une tâche atteint un statut terminal, son état ne change plus jamais. tasks/get est idempotent : interroger dix fois est donc aussi sûr qu’interroger une seule fois.

Attendre une saisie humaine

Vue de dessus des mains d’un responsable apposant un tampon d’approbation sur une pile de formulaires imprimés

Certains travaux atteignent un point de décision en cours de route : approuver un déploiement, confirmer un achat, choisir l’une de trois options. La tâche passe en input_required, et la réponse tasks/get suivante contient un objet inputRequests qui regroupe les demandes d’élicitation ou autres requêtes du serveur.

Le client présente ces demandes à un utilisateur ou à un modèle, puis répond avec tasks/update, en envoyant inputResponses qui correspondent aux demandes en attente. Le serveur accuse réception par un résultat vide et ignore les réponses pour des entrées inconnues ou déjà satisfaites. Une fois que chaque demande a reçu une réponse, le serveur reprend son travail.

💡 Pourquoi c’est bien pensé : pas de seconde connexion ni de message non sollicité du serveur vers le client. L’étape humaine passe par la même boucle de polling que le reste.

Terminer, échouer et annuler

Quand la tâche se termine avec succès, le champ result contient ce que la requête initiale aurait renvoyé de façon synchrone. Pour un appel d’outil, cela signifie les mêmes blocs de contenu qu’un appel bloquant aurait produits. Quand le statut est failed, le champ error contient l’erreur JSON-RPC.

L’annulation passe par tasks/cancel. Le serveur accuse réception par un résultat vide, mais l’annulation est coopérative. Le travail peut déjà avoir dépassé le point de non-retour, si bien qu’une tâche peut tout de même aboutir à un autre statut terminal.

Les serveurs peuvent aussi envoyer des mises à jour via notifications/tasks. Les clients s’inscrivent avec subscriptions/listen, et chaque notification porte l’état complet de la tâche, dans la même forme que celle que renverrait une réponse tasks/get.

Construire un serveur de tâches

Profil de côté d’un programmeur tapant sur son clavier dans un bureau à domicile sombre au crépuscule

Un outil FastMCP minimal

FastMCP 4.0 a ajouté la prise en charge de l’extension. Vous installez fastmcp-tasks, enregistrez TasksExtension et marquez l’outil comme capable de gérer des tâches :

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"

Deux points comptent ici. Les tâches en arrière-plan exigent des fonctions async, et utiliser task=True sur une fonction synchrone déclenche une ValueError au moment de l’enregistrement. Et task=True signale seulement que l’outil peut tourner en arrière-plan. Qu’il le fasse réellement dépend de l’inscription du client et du mode d’exécution du serveur. La documentation précise que Docket alimente le planificateur distribué, ce qui rend la configuration prête pour la production.

Progression et modes d’exécution

Les outils signalent leur progression via une dépendance injectée Progress, et c’est de là que vient la statusMessage affichée par vos clients :

@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"

Pour un contrôle plus fin, remplacez le booléen par un TaskConfig. Trois modes déterminent le comportement de l’outil :

ModeComportement
optionalS’exécute de façon synchrone pour les anciens clients et en arrière-plan pour ceux qui gèrent les tâches
requiredRenvoie une erreur si le client ne prend pas en charge les tâches, sinon s’exécute en arrière-plan
forbiddenToujours synchrone, jamais en arrière-plan

Les raccourcis se correspondent sans ambiguïté : task=True équivaut à optional, et task=False équivaut à forbidden. Vous pouvez aussi suggérer une cadence de polling avec poll_interval=timedelta(seconds=2).

Allée symétrique entre des rangées de baies serveurs noires dans un centre de données

Des habitudes côté client qui tiennent

Interroger poliment, tout persister

Voyageur tenant un smartphone dans un train du matin sous la pluie

Un client qui dialogue avec des serveurs capables de gérer des tâches a besoin de cinq habitudes :

  • Déclarer l’extension dans les capacités par requête.
  • Gérer des résultats polymorphes, puisqu’un tools/call peut renvoyer un résultat normal ou une tâche.
  • Respecter pollIntervalMs, car le serveur peut le modifier entre deux réponses.
  • Répondre à inputRequests via tasks/update au lieu de les ignorer.
  • Stocker les ID de tâche de façon durable pour que le polling puisse reprendre après un plantage ou un redémarrage.

Le code ci-dessous est du pseudocode, non lié à un SDK précis :

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")

Le voyageur dans un train est le modèle mental. La connexion se coupe à chaque tunnel, mais le billet dans sa poche reste valable. Un client construit ainsi se reconnecte après le tunnel et poursuit.

Des notifications plutôt que du polling

Le polling est la solution par défaut, et il fonctionne partout. Si un serveur prend en charge notifications/tasks, un client peut s’abonner une fois et éviter la plupart des allers-retours tasks/get, puisque chaque notification contient déjà l’état complet de la tâche. Gardez le polling en repli pour les serveurs qui ne poussent pas de mises à jour.

Erreurs à éviter

Étagère de colis en carton brun avec étiquettes manuscrites dans l’arrière-salle d’un bureau de poste

Les descripteurs de tâches se comportent comme des colis dans l’arrière-salle d’un bureau de poste. Laissez-en un trop longtemps et il finit par être débarrassé. Voici les pièges qui reviennent le plus souvent :

ErreurCe qui se passeSolution
Ignorer ttlMsLa tâche expire avant qu’un client lent ne lise le résultatLisez les résultats rapidement, et donnez au serveur une durée de vie (TTL) adaptée au comportement réel des clients
Interroger plus vite que pollIntervalMsRequêtes gaspillées et charge évitableAttendez l’intervalle suggéré
Considérer l’annulation comme instantanéeL’interface annonce l’arrêt du travail alors qu’il continueAttendez un statut terminal avant de le signaler
Renvoyer une tâche à un client qui ne s’est jamais inscritLe client ne peut pas lire la réponseVérifiez d’abord les capacités déclarées
Encapsuler chaque outil dans une tâcheLes appels rapides gagnent en latence sans raisonLaissez les opérations rapides renvoyer un résultat normal
Partager les ID de tâche sans précautionUn autre appelant pourrait lire la sortie de quelqu’un d’autreTraitez l’ID comme une poignée et liez-le à l’appelant authentifié (bonne pratique, au-delà de ce que liste la spécification)

Un ttlMs de null signifie illimité, ce qui semble sympathique, jusqu’à ce que le stockage se remplisse de tâches terminées que personne ne vient récupérer.

Associer les tâches à PicassoIA

La génération de médias est le travail de longue durée type, ce qui explique pourquoi les schémas de tâches s’intègrent naturellement aux outils d’image et de vidéo. Deux modèles de PicassoIA s’insèrent directement dans un flux de tâches.

Comment utiliser Claude Sonnet 5

Claude Sonnet 5 gère le codage en plusieurs étapes et l’utilisation d’outils, c’est donc un bon binôme de programmation pour le code des handlers de cet article. Parmi les autres options de la même catégorie, il y a GPT 5.6 Sol, si vous voulez un second avis sur le même code.

  1. Ouvrez la page Claude Sonnet 5 sur Picasso IA.
  2. Collez la signature de votre outil et les champs de tâche de cet article dans la zone Prompt, puis demandez un handler async avec des messages de statut.
  3. Réglez Effort sur high pour les machines à états délicates, ou laissez-le sur low pour les modifications rapides.
  4. Ajoutez un System Prompt tel que « Write Python, async only, no blocking calls » pour que chaque réponse garde le même style.
  5. Passez Max Tokens au-dessus de la valeur par défaut de 8 192 si vous voulez que les tests soient générés dans la même réponse.
  6. Lancez-le, relisez le résultat et collez le handler dans votre projet.

💡 Astuce : joignez une capture d’écran d’erreur avec le champ Image. Le modèle la lit comme contexte.

Des détourages propres pour les schémas

Table de studio d’un photographe produit avec une tasse en céramique sur papier blanc et un ordinateur affichant le détourage

La documentation sur les flux de tâches a souvent besoin de visuels propres : une photo d’appareil pour une carte de statut, un logo pour une diapositive d’architecture. Remove Background renvoie un PNG transparent en quelques secondes, et son réglage Preserve Partial Alpha conserve des bords doux naturels. Désactivez-le si vous voulez des bords nets et totalement opaques pour les photos produit.

Pour de nouvelles images de scène, Flux 2 Pro et P Image transforment tous deux un prompt écrit en photo adaptée à l’en-tête d’un blog.

Créez ensuite vos propres images

Vous avez maintenant le tableau complet : une poignée à la place d’une connexion maintenue, cinq statuts, trois méthodes et une courte liste d’habitudes qui évitent la disparition des longs travaux. Le meilleur moyen de retenir tout cela est de construire quelque chose de petit. Écrivez un outil qui prend dix secondes, marquez-le task=True et regardez le statut passer de working à un état terminal.

Donnez ensuite un visage à votre projet. Ouvrez Picasso IA, choisissez un modèle de texte vers image et générez une image d’en-tête pour votre article. Essayez différents angles de prise de vue, la lumière et les détails d’objectif dans vos prompts, supprimez un arrière-plan pour un logo propre, et voyez à quelle vitesse une idée devient un visuel fini. Votre prochain résultat de tâche mérite une image qui vaut la peine d’être partagée.

Partager cet article

Choisissez votre langue