Phase 13: Tools & Protocols

Extension des tâches du MCP: travail durable sur un noyau sans État

Le MCP sans État ne signifie pas que chaque opération doit être terminée dans une seule demande. L'extension officielle Tasks donne à un travail de longue durée une poignée durable explicite. Un serveur peut retourner cette poignée à partir de tools/call, n' importe quelle instance peut répondre tasks/get, et les données du client arrivent par le biais de tasks/updatesans réanimer les sessions de protocole.

Type: Build

Languages: Python

Prerequisites: Phase 13 · 09 (transports), Phase 13 · 11 (stateless MRTR), Phase 13 · 12 (elicitation)

Time: ~90 minutes

Objectifs d'apprentissage

  • Distinguer le transport de protocole sans état de l'état de tâche d'application durable.
  • - On négocieio.modelcontextprotocol/tasksl' extension des capacités par demande et server/discover- Je suis désolé .
  • Retourner une adresse serveur CreateTaskResultavec resultType: "task"seulement après une création durable.
  • Enquête avec tasks/get, remplir les tâches de commande avec tasks/update, et demander l' annulation de la coopération avec tasks/cancel- Je suis désolé .
  • Retirez les plus âgés .tasks/status- Je suis là .tasks/result, et tasks/list- les hypothèses.
  • Abonnez-vous aux notifications de tâches facultatives via subscriptions/listensur un flux SSE de réponse POST.
  • Expiration de la tâche modèle, redémarrage de la récupération, déduplication de la clé d'entrée et erreurs d'exécution correctement.

Pourquoi les tâches sont une extension

Les tâches ont été introduites pour la première fois comme une fonction de base expérimentale en 2025-11-25.io.modelcontextprotocol/tasksL'extension afin que les clients et les serveurs puissent opter pour le cycle de vie supplémentaire sans étendre le protocole de base pour tout le monde.

La spécification d'extension reste une surface de projet même si c'est la maison officielle actuelle de Tasks.

Utiliser une tâche lorsque l'opération présente une ou plusieurs des propriétés suivantes:

  • Il peut survivre à une période de temps ordinaire de demande.
  • Une file d'attente ou un système d'emploi externe possède déjà l'exécution.
  • Le client doit se remettre après son propre redémarrage.
  • L'opération s'arrête pour l'entrée de l'utilisateur ou du modèle pendant l'exécution.
  • L'annulation et la récupération durable des résultats sont des exigences du produit.

Ne créez pas une tâche pour une recherche déterministe bon marché. Une manche, la persistance, les sondages, l'expiration et l'annulation sont une vraie complexité.

Le code de base des statuts, application de l'État

Le MCP 2026-07-28 est retiré initialize- Je suis là .notifications/initialized, les sessions de protocole, et Mcp-Session-Id- Ce n'est pas interdit.

Un id de tâche est l'état explicite de l'application:

  • Le serveur le persiste avant de le renvoyer.
  • Le client peut le stocker et le refaire après le redémarrage.
  • L'identifiant peut être redirigé vers n'importe quelle copie soutenue par le même magasin durable.
  • L'autorisation est vérifiée sur chaque méthode de tâche.
  • L'expiration et la suppression sont définies par des champs de tâches, et non par une durée de vie du transport.

Cela est fonctionnellement différent de l'état caché attaché à une connexion.

Gardez quatre vies séparées:

StateLifetimeWhere it belongs
Protocol metadataOne requestparams._meta, validated again on every call
Transport workOne stdio request or HTTP responseIn-flight coordinator with a bounded deadline
MRTR continuationOne retry sequenceIntegrity-protected requestState, plus replay controls when needed
Durable taskAcross requests, replicas, restarts, and reconnectsShared application store keyed by an authorized taskId

Le déplacement d'un enregistrement de tâche dans la mémoire de processus ne rend pas MCP état.tasks/getPersistez avant de retourner la poignée, puis faites en sorte que chaque méthode de tâche résolve le même enregistrement partagé sous les contrôles du locataire et du principal.

Les négociations sur la capacité

Le client annonce un soutien à chaque demande éligible:

json{
  "_meta": {
    "io.modelcontextprotocol/protocolVersion": "2026-07-28",
    "io.modelcontextprotocol/clientCapabilities": {
      "extensions": {
        "io.modelcontextprotocol/tasks": {}
      }
    },
    "io.modelcontextprotocol/clientInfo": {
      "name": "lesson-client",
      "version": "1.0.0"
    }
  }
}

Le serveur retourne exact supportedVersions, les capacités,ttlMs, et cacheScopede server/discoverEn effet, elle annonce des outils, elle implique aussi des obligations obligatoires.tools/listCe résultat renvoie une déterministegenerate_reportdescripteur, objet valide inputSchema- Je suis là .resultType: "complete", les métadonnées d'identité du serveur, et les indices de cache public.

Une méthode de tâche d' un client qui n' a pas déclaré les retours d' extension -32021, la capacité requise du client manquant, avec data.requiredCapabilitiesà {"extensions":{"io.modelcontextprotocol/tasks":{}}}Une chaîne de protocole non prise en charge est retournée .-32022avec exactitude supportedet requesteddonnées; une version manquante ou non à chaîne est renvoyée -32602- Je suis désolé .

Une enveloppe sans JSON-RPC idest une notification. Le récepteur peut la traiter, mais elle n'émite pas de résultat JSON-RPC ou d'erreur. Un adaptateur HTTP par flux renvoie 202 Acceptedsans organisme pour une notification acceptée.

Pour le moment, seulement tools/callConceptez votre abstraction interne afin que les types de requêtes futurs ne nécessitent pas de réécriture de stockage.

Création de tâches dirigées par le serveur

Le vieux drapeau du client .params._meta.task.requiredLe client déclare le support d'extension, puis le serveur décide si une certainetools/calldevient une tâche.

La demande:

json{
  "jsonrpc": "2.0",
  "id": 1,
  "method": "tools/call",
  "params": {
    "name": "generate_report",
    "arguments": {"size": "large"},
    "_meta": {
      "io.modelcontextprotocol/protocolVersion": "2026-07-28",
      "io.modelcontextprotocol/clientCapabilities": {
        "extensions": {
          "io.modelcontextprotocol/tasks": {}
        }
      }
    }
  }
}

Réponse:

json{
  "jsonrpc": "2.0",
  "id": 1,
  "result": {
    "resultType": "task",
    "taskId": "tsk_786512e29e0d",
    "status": "working",
    "statusMessage": "Preparing report outline.",
    "createdAt": "2026-08-21T10:30:00Z",
    "lastUpdatedAt": "2026-08-21T10:30:00Z",
    "ttlMs": 900000,
    "pollIntervalMs": 1000
  }
}

Le serveur ne doit pas retourner cette poignée avant un tasks/getDans un magasin éventuellement cohérent, attendez la visibilité de lecture avant de répondre. sinon un client peut recevoir un id valide et obtenir immédiatement " pas trouvé ".

Une réponse à la tâche n'est pas demandée dans le sens où le client ne demande pas le mode de tâche.

La forme de la tâche

Chaque tâche comporte:

  • taskId: identifiant stable généré par le serveur;
  • statusLe numéro de la liste:working- Je suis là .input_required- Je suis là .completed- Je suis là .cancelledou failedLe dépôt de la commission
  • createdAtet lastUpdatedAt: timestamps ISO 8601;
  • ttlMs: durée d'expiration à compter de la création, ou nullpour une limite non annoncée;
  • optionnel pollIntervalMs: la cadence minimale suggérée des sondages du serveur;
  • optionnel statusMessage: contexte d'utilisation ou de modèle.

Les champs spécifiques à l'état ne sont affichés que lorsqu'ils sont pertinents:

  • input_requiredinclut inputRequests- Je suis désolé .
  • completedinclut les documents de la demande initiale resultla forme.
  • failedinclut un JSON-RPC error- Je suis désolé.

Le client doit honorer .pollIntervalMsUn serveur peut limiter les taux de sondage plus agressif et modifier l'intervalle au cours de la durée de la tâche.

Enquête avec tasks/get

Le client demande une photo d' instant:

httpPOST /mcp HTTP/1.1
Content-Type: application/json
MCP-Protocol-Version: 2026-07-28
Mcp-Method: tasks/get
Mcp-Name: tsk_786512e29e0d
json{
  "jsonrpc": "2.0",
  "id": 2,
  "method": "tasks/get",
  "params": {
    "taskId": "tsk_786512e29e0d",
    "_meta": {
      "io.modelcontextprotocol/protocolVersion": "2026-07-28",
      "io.modelcontextprotocol/clientCapabilities": {
        "extensions": {
          "io.modelcontextprotocol/tasks": {}
        }
      }
    }
  }
}

tasks/getIl est donc toujours le résultat.resultType: "complete"La tâche enlisée peut encore avoirstatus: "working"ou status: "input_required"- Je suis désolé .

Cette distinction empêche un bug parseur commun:

textresult.resultType = complete    means the tasks/get RPC finished
result.status = working        means the represented job is still running

Il n' y a pas detasks/resultQuand la tâche est terminée, la prochainetasks/getLa réponse est conforme à l'original CallToolResultsous result- Le numéro de la liste:

json{
  "resultType": "complete",
  "taskId": "tsk_786512e29e0d",
  "status": "completed",
  "createdAt": "2026-08-21T10:30:00Z",
  "lastUpdatedAt": "2026-08-21T10:34:12Z",
  "ttlMs": 900000,
  "result": {
    "resultType": "complete",
    "content": [
      {"type": "text", "text": "Generated large report with approved outline."}
    ],
    "structuredContent": {"size": "large", "approved": true},
    "isError": false,
    "_meta": {
      "io.modelcontextprotocol/serverInfo": {
        "name": "tasks-demo",
        "version": "1.0.0"
      }
    }
  },
  "_meta": {
    "io.modelcontextprotocol/serverInfo": {
      "name": "tasks-demo",
      "version": "1.0.0"
    }
  }
}

L' extérieur .resultTypedit le tasks/getRPC terminé.result.resultTypeIl faut un discriminateur niché.CallToolResultIl doit aussi porter sa propre .io.modelcontextprotocol/serverInfoCette leçon l'inclut au lieu de stocker une charge utile non typiée.

Il n' y a pas detasks/listLes serveurs sans session ne peuvent pas en toute sécurité déduire quelles tâches appartiennent à une liste de connexion.

Entrée lors de l'exécution de tâches

L'entrée de tâche et le MRTR de base sont similaires, mais utilisent des continuations différentes.

Entrée nécessaire avant la création de tâches

Le noyau de retour resultType: "input_required"à partir de l' original tools/callLe client le remplit et tente à nouveau l'appel original.

Entrée nécessaire après création de tâche

Définissez la tâche à input_required- Je suis là .tasks/getdévoile les faits remarquables inputRequests, et le client envoie des réponses par l' intermédiaire de tasks/updateLe client ne réessaye pas l' original .tools/call- Je suis désolé .

Une photo:

json{
  "resultType": "complete",
  "taskId": "tsk_786512e29e0d",
  "status": "input_required",
  "createdAt": "2026-08-21T10:30:00Z",
  "lastUpdatedAt": "2026-08-21T10:31:00Z",
  "ttlMs": 900000,
  "inputRequests": {
    "approve_outline": {
      "method": "elicitation/create",
      "params": {
        "mode": "form",
        "message": "Approve the generated report outline?",
        "requestedSchema": {
          "type": "object",
          "properties": {"approved": {"type": "boolean"}},
          "required": ["approved"]
        }
      }
    }
  }
}

Mise à jour:

httpPOST /mcp HTTP/1.1
Content-Type: application/json
MCP-Protocol-Version: 2026-07-28
Mcp-Method: tasks/update
Mcp-Name: tsk_786512e29e0d
json{
  "jsonrpc": "2.0",
  "id": 4,
  "method": "tasks/update",
  "params": {
    "taskId": "tsk_786512e29e0d",
    "inputResponses": {
      "approve_outline": {
        "action": "accept",
        "content": {"approved": true}
      }
    },
    "_meta": {
      "io.modelcontextprotocol/protocolVersion": "2026-07-28",
      "io.modelcontextprotocol/clientCapabilities": {
        "extensions": {
          "io.modelcontextprotocol/tasks": {}
        }
      }
    }
  }
}

La réponse à la réussite est une reconnaissance vide plus resultType: "complete"Le changement d'état peut être cohérent, donc le client continue à faire des sondages ou à écouter.

Chacun d' eux .inputRequestsLa clé doit être unique pour toute la durée de vie de la tâche.tasks/getLes instantanés peuvent afficher la même clé en suspens; les clients déduplicent l'interface utilisateur et les serveurs ignorent les réponses pour les clés inconnues, remplacées ou déjà remplies.input_requiredjusqu'à ce que toutes les clés requises soient répondues.

L'annulation est coopérative

tasks/cancelLes travailleurs qui ont été arrêtés ne peuvent pas être autorisés à effectuer des travaux avant la fin de leur travail, à ne pas tenir compte de l'annulation ou de la transition.

httpPOST /mcp HTTP/1.1
Content-Type: application/json
MCP-Protocol-Version: 2026-07-28
Mcp-Method: tasks/cancel
Mcp-Name: tsk_786512e29e0d
json{
  "jsonrpc": "2.0",
  "id": 5,
  "method": "tasks/cancel",
  "params": {
    "taskId": "tsk_786512e29e0d",
    "_meta": {
      "io.modelcontextprotocol/protocolVersion": "2026-07-28",
      "io.modelcontextprotocol/clientCapabilities": {
        "extensions": {
          "io.modelcontextprotocol/tasks": {}
        }
      }
    }
  }
}

Pour les trois méthodes de tâche,Mcp-NameLes miroirsparams.taskId. Il ne répète pas le nom de la méthode JSON-RPC. code/main.pycentraliser cette règle en make_http_request- Je suis désolé .

Le travailleur de leçon honore immédiatement l'annulation, faisant des appels répétés idempotents.

Ne pas utiliser notifications/cancelledCette notification appartient à la demande d'annulation, pas à des tâches durables.

La distinction est importante à la limite de routage. L'annulation de requête cible une opération JSON-RPC en vol ou sa réponse HTTP à la demande.tools/callIl est déjà revenu .resultType: "task"La demande est complète et la fermeture de son transport ne peut pas nommer ou arrêter le travail durable. tasks/cancelIl est un nouveau RPC autorisé.params.taskId, reflète cette identité dansMcp-Name, résolve le backend de la tâche, enregistre l'intention de l'annulation de la coopérative et renvoie un avis de confirmation sans prétendre que le travailleur s'est arrêté.

Une passerelle doit donc conserver les coordonnées des requêtes et les routes des tâches dans différentes tables. La table des requêtes peut disparaître lorsque la réponse est terminée. La route des tâches doit survivre jusqu'à l'expiration de l'état terminal et de la conservation. Lesson 29: MCP Reliability, Cancellation, and Flow Controlconstruit la course, le temps d'arrêt, l'idempotence, la contrainte et réessayez les règles pour les deux chemins.

Notifications facultatives

Les sondages sont la base. Un client qui veut des mises à jour push envoiesubscriptions/listenPour Streamable HTTP, il s'agit d'un POST dont la réponse est un flux SSE à échelle de requête. Il n'y a pas de flux d'événements GET indépendant et aucune session de protocole à maintenir en vie.

Le serveur reconnaît les identifiants acceptés avec notifications/subscriptions/acknowledgedet peut ensuite envoyer des instantanés complets à travers notifications/tasks- Le relevé et chaque notification de tâcheio.modelcontextprotocol/subscriptionIddans _meta, égale à la subscriptions/listenchaque notification de tâche est équivalente à ce que tasks/getIl reviendrait à ce moment-là.

Les clients doivent toujours déclarer l'extension Tasks. Ils doivent se reconnecter et reprendre à partir d'id de tâche durables plutôt que de dépendre de la répétition d'événements ou Last-Event-ID- Je suis désolé .

Sémantique de l'échec

Utilisez les deux couches d'erreur correctement.

Erreur de protocole

Paramètres de méthode invalide ou un id de tâche inconnu renvoyer une erreur JSON-RPC, communément -32602. Des retours de soutien à l' extension manquants -32021avec l'objet de capacité requis.

Résultat de l'exécution des tâches

  • Un résultat normal avec isError: trueest toujours une completedla tâche parce que l'appel à l'outil a produit son résultat défini.
  • Une erreur JSON-RPC lors de l' exécution différée fait la tâche failedet stocke cette erreur JSON-RPC dans error- Je suis désolé .
  • Le refus de l'utilisateur peut entraîner cancelled, un résultat de refus complet, ou un autre résultat sûr spécifique au domaine.

Durable, expirant et propriétaire

Persistez au moins l'identifiant de tâche, le statut, les timestamps, ttl, l'intervalle de sondage, la propriété d'exploitation originale, le résultat ou l'erreur, les demandes de saisie en suspens et toutes les clés d'entrée émises.

La clé de stockage doit inclure ou résoudre un locataire et un principal autorisé.tasks/get- Je suis là .tasks/update- Je suis là .tasks/cancel, et souscription.

ttlMsUn client peut le traiter comme un backstop lorsqu'une tâche a cessé de produire des mises à jour observables. Un serveur peut échouer et supprimer une tâche expirée plus tard. Ne le décris pas comme une promesse de conserver un résultat fini pendant plusieurs millisecondes après sa réalisation.

Utilisez des écritures ou des transactions atomiques. La leçon écrit un fichier temporaire et le renomme atomquement. Un service multi-replica devrait utiliser un magasin durable partagé et un bail ou un contrôle de concurrences équivalent.

Faites-le

code/main.pymet en œuvre un service de tâches déterministe:

  • server/discoverRetour supportedVersions, des indices de cache, et l'extension des tâches.
  • tools/listrenvoie une définition déterministe, cacheable generate_reportDescripteur avec un schéma d'entrée valide.
  • tools/callcrée et persiste la tâche avant de revenir resultType: "task"- Je suis désolé .
  • Une nouvelle instance de service recharge la même tâche, démontrant le redémarrage de la récupération.
  • tasks/getrenvoie des instantanés complets de la tâche.
  • Le travailleur déménage de workingà input_required- Je suis désolé .
  • tasks/updateaccepte une réponse du formulaire et renvoie une confirmation complète vide.
  • Le travailleur stocke un nidCallToolResultavec ses propres resultTypeet l'identité du serveur, puis des transitions vers completed- Je suis désolé .
  • tasks/cancelest idempotent dans cette mise en œuvre.
  • Les ensembles de constructeur HTTP Mcp-Nameà params.taskIdpour tasks/get- Je suis là .tasks/update, et tasks/cancel- Je suis désolé .
  • Les aides à la notification utilisent notifications/subscriptions/acknowledgedet notifications/tasks, tous deux marqués avec l'identifiant de demande d'écoute.
  • Les notifications sans ID ne produisent aucune réponse JSON-RPC.

Le travailleur avance explicitement au lieu de dormir dans un fil de fond. Cela rend chaque transition d'état déterministe et garde l'exemple de protocole séparé de la mécanique de file d'attente.

Utilisez-le

À partir de la racine du référentiel:

bashcd phases/13-tools-and-protocols/13-mcp-async-tasks/code
python3 main.py
python3 -m unittest discover tests -v

Sequence de résultats attendue:

textid=0 resultType=complete status=ack
id=1 resultType=task status=working
id=2 resultType=complete status=working
id=3 resultType=complete status=input_required
id=4 resultType=complete status=ack
id=5 resultType=complete status=completed

Vérifiez aussi que tasks/status- Je suis là .tasks/result, et tasks/listméthode de retour non trouvée dans le service moderne.

Vérifiez ça .tools/listest déterministe et chaque méthode de tâche HTTP actuelle reflète son id de tâche à travers Mcp-Name- Je suis désolé .

La faire partir

outputs/skill-task-store-designer.mdIl produit maintenant une conception consciente de l'extension: négociation de capacités, création durable avant retour, méthodes actuelles, flux de mise à jour des entrées, propriété, expiration, annulation, abonnement et migration des méthodes expérimentales supprimées.

Exercices

  1. Ajoutez une deuxième clé d'entrée en attente. Envoyez une partie tasks/updateet prouver que la tâche reste input_requiredjusqu'à ce que les deux clés soient répondues.
  2. Ajouter la propriété du locataire au magasin et rejeter un identifiant de tâche valide présenté par le principal authentifié incorrectement.
  3. Ajouter un contrat de location de travailleurs à expiration.
  4. Implementer un adaptateur SSE de réponse POST pour subscriptions/listen. Ne pas ajouter GET, Last-Event-ID, ou un en-tête de session.
  5. Ajouter le nettoyage d'expiration. Sélectionner une tâche expirée d'un identifiant de tâche malformé sans fuite d'existence entre les locataires.

Les termes clés

TermMeaning in the current extension
Tasks extensionOptional io.modelcontextprotocol/tasks capability for durable async work
CreateTaskResultServer-directed resultType: "task" response to an eligible request
tasks/getPoll a full current task snapshot, including terminal result or pending input
tasks/updateSubmit responses to a task's outstanding inputRequests
tasks/cancelAcknowledge cooperative cancellation intent
input_requiredTask status indicating client input is outstanding
pollIntervalMsServer-suggested minimum delay before another poll
ttlMsExpiry duration measured from task creation
Durable-before-returnRule that the task id must resolve before its handle is sent
notifications/tasksOptional full task snapshot delivered on a subscribed SSE response

Compatibilité avec l'héritage

La surface expérimentale de 2025-11-25 utilisait l'augmentation des tâches demandées par le client, tasks/status- Je suis là .tasks/result, et facultatif tasks/listUn client actuel utilise la capacité d'extension, accepte les poignées dirigées par le serveur, les sondagestasks/get, fournit des informations avec tasks/update, et lit le résultat final de l'instantané de la tâche.

Pour en savoir plus

This free lesson is part of the AI Engineering from Scratch curriculum. Read the full explanation, run the lesson code, and verify the result in the interactive reader or from the repository source.

Browse the complete course catalog or open this lesson on GitHub.