1. Pourquoi avons-nous besoin des événements MCP
Jusqu’ici, presque toute la communication entre ChatGPT et votre backend ressemblait à du RPC : le modèle appelait un outil, celui‑ci faisait quelque chose, renvoyait un résultat — et c’était terminé. C’est pratique tant que les opérations sont courtes : 200–500 ms, au maximum quelques secondes.
Mais dès qu’apparaît quelque chose de longue durée — analyse d’un gros fichier des préférences des employés pour GiftGenius, agrégation de recommandations depuis une ribambelle d’API externes, recalcul d’un gros flux — tout devient peu agréable. Délais d’expiration HTTP, redémarrages de fonctions, spinners « éternels », et l’utilisateur se demande : « est‑ce encore vivant ou déjà mort ? ».
C’est là que commence le modèle d’événements. Au lieu de garder un long appel d’outil, vous lancez une tâche, récupérez un jobId, puis le serveur envoie de lui‑même des événements : démarré, progression, terminé, tombé en échec. Ces événements dans MCP sont implémentés comme des notifications JSON-RPC — des messages unidirectionnels sans id, pour lesquels aucune réponse n’est attendue.
Il est important de comprendre : un événement n’est pas « un console.log sur le fil ». C’est un message formel du protocole avec un schéma défini, que votre UI (widget) et/ou agent doit savoir traiter avec autant de discipline que le résultat d’un appel d’outil.
Rappel : types de messages dans MCP
Avant d’aller plus loin, rappelons brièvement quels messages existent dans MCP.
En laissant de côté toutes les couches marketing, MCP s’appuie sur JSON-RPC 2.0. On y trouve trois types de messages de base : requêtes, réponses et notifications.
Pour ne pas les énumérer en liste, regardons un petit tableau comparatif :
| Type | Champ id | Qui initie | Réponse attendue ? | Exemple dans MCP |
|---|---|---|---|---|
| Request | présent | Généralement le client (ChatGPT) | Oui | Appel d’outil tools/call |
| Response | présent | Serveur MCP | C’est la réponse elle‑même | Résultat de tools/call |
| Notification | absent | Client ou serveur | Non | notifications/progress, resources/updated, logging/message |
Les événements MCP vivent précisément dans la troisième ligne : ce sont des notifications. Signes distinctifs :
- pas de id au niveau supérieur — aucun result ni error n’arrivera en réponse ;
- l’initiateur n’attend pas d’ACK — « tirer et oublier » au niveau du protocole ;
- la fiabilité ne repose pas sur des accusés de réception, mais sur l’idempotence des gestionnaires et une politique de renvois (retries).
Une contrainte importante : les événements MCP ne « volent » pas n’importe quand quelque part dans l’espace. Ils vivent dans une connexion MCP établie au‑dessus d’un transport concret. Le plus souvent, c’est un flux du type SSE (nous verrons les détails de transport et ses variantes dans un cours séparé).
2. À quoi ressemble « un événement MCP » en pratique
Formellement, un événement MCP est une notification JSON-RPC, c’est‑à‑dire un objet du type :
{
"jsonrpc": "2.0",
"method": "notifications/job/progress",
"params": {
"jobId": "job_123",
"percentage": 30,
"stage": "Nous recherchons des options dans le catalogue",
"eventId": "evt_abc123",
"timestamp": "2025-11-21T10:15:00Z"
}
}
Voici plusieurs points importants :
- Dans le champ method, nous encodons le type d’événement et son « espace de noms ». MCP définit déjà une série de méthodes standard du type notifications/... pour les logs, la progression et les changements de ressources, mais vous pouvez et devez ajouter vos propres méthodes spécifiques métier, comme notifications/job/progress ou notifications/job/completed.
- Toutes les données métier se trouvent dans params. C’est là que nous stockerons aussi les identifiants de tâches (jobId), les identifiants uniques d’événements (eventId), l’horodatage (timestamp), des messages lisibles par l’humain, etc.
- L’absence du champ id au niveau supérieur — c’est précisément ce qui en fait une notification. Le protocole ne prévoit pas de réponse. Si le serveur veut savoir « s’il a été compris », il peut envoyer un autre événement ou attendre des actions réactives du client (par exemple, une nouvelle requête). Mais il n’y a pas d’ACK au sens de JSON-RPC.
Mentalement, on peut raisonner ainsi : l’appel d’outil tools/call est « une lettre à laquelle vous attendez une réponse », et un événement — « une notification d’un bot Slack : « Tâche en arrière‑plan n° 123 terminée » ».
3. Taxonomie des événements : quelles notifications existe‑t‑il
Si l’on autorise simplement « envoyez n’importe quel JSON en notification », au bout de deux semaines le système devient une décharge : les noms d’événements varient, les champs fluctuent, l’UI ne sait pas quoi en faire. Il est donc utile de s’accorder sur une petite taxonomie.
Ci‑dessous — une variante pratique de classification, qui s’aligne bien avec la spec MCP et les cas réels des ChatGPT Apps.
Événements du cycle de vie d’une tâche (Job Lifecycle)
Ce sont les événements qui reflètent les transitions clés d’état d’une tâche. En général, une tâche a un automate d’états du genre pending → running → (completed | failed | canceled).
Événements typiques :
- job.created — la tâche est enregistrée ;
- job.started — un worker a commencé l’exécution ;
- job.completed — la tâche s’est terminée avec succès ;
- job.failed — la tâche a échoué ;
- job.canceled — la tâche a été annulée par l’utilisateur.
Exemple de job.completed pour GiftGenius :
{
"jsonrpc": "2.0",
"method": "notifications/job/completed",
"params": {
"eventId": "evt_gg_100",
"jobId": "giftjob_42",
"timestamp": "2025-11-21T10:20:00Z",
"summary": "La sélection de cadeaux est terminée",
"resultResourceId": "resource:gifts:giftjob_42"
}
}
Ici, resultResourceId peut pointer vers une ressource MCP, qui sera ensuite lue par le widget ou l’agent.
Mises à jour de progression (Progress Updates)
Ce sont des « petites étapes » au sein du cycle de vie : elles ne changent pas le statut final, mais donnent à l’utilisateur la sensation que quelque chose se passe.
Événement typique job.progress :
{
"jsonrpc": "2.0",
"method": "notifications/job/progress",
"params": {
"eventId": "evt_gg_101",
"jobId": "giftjob_42",
"timestamp": "2025-11-21T10:18:30Z",
"percentage": 40,
"stage": "Nous filtrons les cadeaux par budget",
"etaSeconds": 25
}
}
Il est important ici que percentage progresse raisonnablement vers 100, sans faire le yoyo. Choisissez un nom unique pour le champ de progression (par exemple, percentage) et utilisez‑le dans tous les événements. L’outil officiel de progression MCP impose aussi la règle : la progression ne fait que croître.
Événements de mise à jour de données (Resource/Data events)
Parfois, l’utilisateur ne se soucie même pas d’un jobId précis. Ce qui importe, c’est qu’une entité a changé : le flux de produits a été mis à jour, un nouveau snapshot de rapport a été formé, un profil personnel a été régénéré.
MCP dispose déjà de notifications standard de niveau resources/updated, resources/list_changed et similaires, qui signalent au client : « relis la liste des ressources, quelque chose a changé ».
Pour GiftGenius, cela peut ressembler à ceci :
{
"jsonrpc": "2.0",
"method": "resources/updated",
"params": {
"eventId": "evt_feed_17",
"timestamp": "2025-11-21T09:00:00Z",
"resourceId": "resource:product-feed",
"changeType": "snapshot_ready"
}
}
En recevant un tel événement, le widget peut, par exemple, mettre en évidence le bouton « Mettre à jour la liste des cadeaux ».
Événements UX et système
Il existe aussi des événements qui ne sont pas strictement métier, mais importants pour l’UX ou le diagnostic :
- messages de log logging/message — notification MCP standard pour les logs ;
- heartbeat/ping — « je suis vivant » périodiques du serveur ;
- avertissements de dégradation : par exemple, « l’API externe est lente en ce moment, les résultats peuvent arriver plus lentement ».
Ces événements sont utiles pour la supervision et le débogage ; parfois on peut les « casualiser » dans l’UI, en montrant à l’utilisateur que le système n’est pas mort, mais simplement occupé.
4. Structure d’un événement : champs obligatoires et payload
Un événement est un objet d’API au même titre qu’une requête d’outil. Il faut le concevoir. Une bonne habitude consiste à se mettre d’accord sur un jeu de champs de base.
Conceptuellement, il est utile de diviser un événement en trois parties : métadonnées, corrélation et charge utile (payload).
Exemple de forme générale :
{
"jsonrpc": "2.0",
"method": "notifications/job/progress",
"params": {
"eventId": "evt_gg_103",
"type": "job.progress",
"timestamp": "2025-11-21T10:19:00Z",
"jobId": "giftjob_42",
"payload": {
"percentage": 60,
"stage": "Nous comparons les avis",
"etaSeconds": 15
}
}
}
Dans cette structure, on peut distinguer :
- eventId — identifiant unique de l’événement. Nécessaire pour la déduplication côté client ;
- type — nom logique de l’événement (on peut dupliquer/normaliser method) ;
- timestamp — quand l’événement a été généré par le serveur ;
- jobId ou un autre correlation-id — pour comprendre à quoi se rapporte cet événement ;
- payload — les données proprement dites. Sa forme dépend de chaque type d’événement.
Dans un système réel, vous voudrez presque à coup sûr décrire formellement ces structures via JSON Schema ou au moins des types TypeScript, afin que serveur et client valident les messages. Certaines équipes utilisent un format inspiré de CloudEvents : on y trouve aussi des champs standard id, source, type, time, etc.
Mais l’idée clé est simple : un événement doit être lisible par la machine et consistant — sans surprises du type « parfois le champ s’appelle jobId, parfois job_id, parfois il manque ».
Dans les exemples ci‑dessous, pour ne pas surcharger le code, nous utiliserons plus souvent une variante « aplanie » : toutes les données de l’événement se trouvent directement dans params sans payload imbriqué, et le champ type est parfois omis si son rôle est déjà joué par method. Le principe reste le même : chaque événement possède des métadonnées stables (eventId, jobId, timestamp) et une charge utile prévisible.
5. Idempotence des événements : pourquoi et comment
Voici maintenant le mot le plus important de ce cours — l’idempotence.
L’idempotence d’un gestionnaire d’événement signifie que si le même événement est traité une fois ou dix fois, l’état final du système reste correct. Dans des systèmes distribués avec réseau et retries, c’est littéralement une question de vie ou de mort.
Pourquoi le même événement peut‑il arriver plusieurs fois ?
Les raisons sont nombreuses : des coupures de connexion et reconnexions aux retries côté serveur, qui a « par sécurité » renvoyé la notification. Avec des protocoles en flux (par exemple, quand le serveur pousse lui‑même des événements dans une connexion ouverte, comme SSE — nous en parlerons plus en détail dans un cours séparé sur le transport), c’est classique : le client se reconnecte avec Last-Event-ID, le serveur renvoie les événements manqués, et certains d’entre eux seront vus une deuxième fois par le client.
Si votre gestionnaire n’est pas idempotent, les bizarreries commencent :
- l’événement job.completed déclenche un double crédit de bonus ou change deux fois le statut d’une commande ;
- l’événement resource.updated amène le widget à « ajouter » des cartes à chaque fois, en les dupliquant dans l’UI ;
- des job.progress répétés effraient les utilisateurs si la barre de progression se met à avancer puis reculer.
La bonne stratégie fonctionne sur deux couches : la génération des événements côté serveur et leur traitement côté client.
Côté serveur : identifiants stables et automate d’états
Le serveur doit :
- générer un eventId unique pour chaque événement logique ;
- garantir que les événements d’un même jobId forment une séquence d’états valide : vous ne pouvez pas envoyer job.failed après job.completed ni deux job.completed différents avec des résultats différents.
Autrement dit, vous avez en fait un automate d’états de la tâche, et chaque événement est une transition autorisée.
Côté client : déduplication et mises à jour « douces »
Le client (widget, agent ou autre composant) doit :
- conserver l’ensemble des eventId déjà traités, au moins pendant la durée de vie de la connexion/session actuelle ;
- vérifier avant traitement : si un eventId a déjà été vu, simplement l’ignorer ou redessiner l’UI sans effets de bord ;
- lors de la réception d’événements modifiant le statut d’une tâche (job.completed, job.failed), s’assurer que la transition est permise : par exemple, si la tâche est déjà marquée completed, un job.completed répété ne doit rien changer, et un failed devrait être ignoré comme incorrect.
Exemple classique du monde du commerce : le traitement du webhook de confirmation de paiement. Un même order.paid peut facilement arriver deux fois ; c’est pourquoi le backend stocke un paymentId et un drapeau « déjà crédité ». Même si le webhook arrive une seconde fois, l’état de la commande ne change pas. Les événements MCP doivent être conçus avec le même état d’esprit.
6. Exemple : concevoir les événements pour GiftGenius
Appliquons cela à notre projet pédagogique GiftGenius. Imaginons un scénario long : l’utilisateur a chargé un gros CSV avec la liste des employés et leurs centres d’intérêt, et a demandé « propose des idées de cadeaux pour tous ». L’opération peut prendre des dizaines de secondes.
Un modèle d’événements raisonnable peut se décrire ainsi :
- L’utilisateur lance l’outil start_bulk_gift_analysis. L’outil renvoie un jobId : "bulk_2025_001".
- Le serveur MCP crée la tâche et envoie presque immédiatement job.started avec une brève description.
- Au fil de l’exécution, il envoie plusieurs job.progress avec des étapes :
- 10 % — « Nous analysons le fichier et vérifions le format » ;
- 40 % — « Nous extrayons les centres d’intérêt et les départements » ;
- 70 % — « Nous faisons correspondre les cadeaux par catégories » ;
- 100 % — juste avant la fin.
- À la fin, un job.completed arrive avec un lien vers la ressource contenant les recommandations finales.
- Si tout se passe mal — au lieu de completed, un job.failed arrive avec un code d’erreur et, possiblement, un indice sur ce qu’il faut corriger.
Informellement, c’est bien ce qui se passera, mais fixons‑le en JSON Schema pour deux événements clés job.progress et job.completed. Pseudo‑JSON Schema (simplifié) :
{
"job.progress": {
"type": "object",
"properties": {
"eventId": { "type": "string" },
"jobId": { "type": "string" },
"timestamp": { "type": "string", "format": "date-time" },
"percentage": { "type": "number", "minimum": 0, "maximum": 100 },
"stage": { "type": "string" },
"etaSeconds": { "type": "number" }
},
"required": ["eventId", "jobId", "timestamp", "percentage", "stage"]
}
}
{
"job.completed": {
"type": "object",
"properties": {
"eventId": { "type": "string" },
"jobId": { "type": "string" },
"timestamp": { "type": "string", "format": "date-time" },
"summary": { "type": "string" },
"resultResourceId": { "type": "string" }
},
"required": ["eventId", "jobId", "timestamp", "resultResourceId"]
}
}
Vous n’êtes pas obligé de mettre en place une validation complète des schémas tout de suite, mais garder cette structure en tête est utile : cela aide à ne pas « étaler » les champs sur différents formats et à ne pas oublier des métadonnées importantes.
7. Mini‑pratique : un serveur qui envoie des événements MCP
Reliions maintenant la théorie à un petit morceau de pseudo‑code TypeScript. Nous n’allons pas plonger dans les bibliothèques MCP réelles (d’une part elles évoluent encore, d’autre part l’accent est mis ici sur le modèle), mais dessiner un squelette structurel.
Supposons que notre serveur MCP possède une abstraction sendNotification, capable d’envoyer une notification JSON-RPC en retour à ChatGPT. Pseudo‑interface :
// Utilitaire d’envoi d’une notification MCP
async function sendNotification(
method: string,
params: Record<string, unknown>
) {
// Ici vous sérialiseriez le JSON et l’enverriez via la connexion MCP active
}
Implémentons maintenant le gestionnaire de l’outil start_bulk_gift_analysis. Il enregistre une tâche, renvoie un jobId, et « tic‑tac » en arrière‑plan en envoyant la progression. Dans la vraie vie, ce serait un worker et une file, mais limitons‑nous pour l’instant à un minuteur.
type Job = {
id: string;
status: "pending" | "running" | "completed" | "failed";
};
const jobs = new Map<string, Job>();
export async function startBulkGiftAnalysisTool() {
const jobId = `bulk_${Date.now()}`;
jobs.set(jobId, { id: jobId, status: "pending" });
// On envoie immédiatement job.started
await sendNotification("notifications/job/started", {
eventId: `evt_${jobId}_started`,
jobId,
timestamp: new Date().toISOString(),
summary: "Analyse d’une grande liste de cadeaux lancée"
});
simulateJob(jobId); // on "lance" la tâche en arrière-plan
return { jobId };
}
Simulation de la tâche :
async function simulateJob(jobId: string) {
jobs.set(jobId, { id: jobId, status: "running" });
const stages = [
{ percent: 10, stage: "Nous analysons le CSV" },
{ percent: 40, stage: "Nous analysons les centres d’intérêt" },
{ percent: 70, stage: "Nous sélectionnons les cadeaux" },
{ percent: 100, stage: "Nous constituons le résultat" }
];
for (const s of stages) {
await sendNotification("notifications/job/progress", {
eventId: `evt_${jobId}_${s.percent}`,
jobId,
timestamp: new Date().toISOString(),
percentage: s.percent,
stage: s.stage
});
await new Promise(r => setTimeout(r, 1000));
}
jobs.set(jobId, { id: jobId, status: "completed" });
await sendNotification("notifications/job/completed", {
eventId: `evt_${jobId}_done`,
jobId,
timestamp: new Date().toISOString(),
summary: "Analyse des cadeaux terminée",
resultResourceId: `resource:gifts:${jobId}`
});
}
Le code est volontairement simple, mais on y voit bien :
- nous utilisons la séquence d’événements started → progress* → completed ;
- chaque événement reçoit un eventId unique ;
- tous les événements sont rattachés au même jobId.
Plus tard, lorsque vous ajouterez de vraies files et de vrais workers, la structure des événements restera à peu près la même — seul changera l’endroit où sendNotification est appelé.
8. Client : gestionnaire d’événements idempotent minimal
Côté client (par exemple, dans votre widget Apps SDK), vous devez apprendre à recevoir de tels événements, à les lier aux tâches en cours et à ne pas devenir fou avec les doublons.
Sans détailler le transport pour l’instant (plus tard), imaginons une fonction onMcpNotification, que votre couche client MCP appelle à chaque notification entrante.
Ajoutons une déduplication minimale :
const processedEvents = new Set<string>();
function handleNotification(method: string, params: any) {
const eventId = params.eventId as string | undefined;
if (!eventId) return; // très discutable, mais pour l’exemple ça passe
if (processedEvents.has(eventId)) {
// Doublon — on ignore ou on met à jour l’UI en douceur
return;
}
processedEvents.add(eventId);
if (method === "notifications/job/progress") {
updateJobProgress(params.jobId, params.percentage, params.stage);
} else if (method === "notifications/job/completed") {
markJobCompleted(params.jobId, params.resultResourceId);
}
}
L’implémentation de updateJobProgress et markJobCompleted — c’est du code React/UI pur :
function updateJobProgress(jobId: string, percent: number, stage: string) {
// par exemple, on le place dans Zustand/Redux/React state
console.log(`Job ${jobId}: ${percent}% — ${stage}`);
}
function markJobCompleted(jobId: string, resourceId: string) {
console.log(`Job ${jobId} terminé, ressource: ${resourceId}`);
}
Un tel gestionnaire :
- ne casse pas si l’événement arrive deux fois ;
- n’a pas d’effets de bord (du genre « on a affiché une deuxième fois la modale “Terminé !” ») ;
- ouvre la voie à une logique plus avancée, par exemple la validation des transitions d’état autorisées (ne pas accepter failed après déjà completed).
En code de production, vous voudrez probablement remettre à zéro processedEvents lors d’une reconnexion au serveur MCP, et aussi conserver non seulement eventId mais l’état courant de chaque jobId, afin de vous comporter plus intelligemment en cas de séquence d’événements étrange.
Il est ensuite important de comprendre comment tous ces événements MCP traversent l’agent/le widget et se transforment en expérience utilisateur concrète : barre de progression, étapes, apparition des résultats finaux. Passons à la liaison des événements avec run/workflow et l’UX.
9. Liaison des événements, run/workflow et UX
Bien que nous ayons déjà eu un module complet sur le workflow et les agents, vous allez maintenant voir l’ensemble. Nous avons déjà introduit des familles d’événements (job.*, resource.*, système) ; voyons comment ils traversent l’agent/le widget et ChatGPT pour devenir une expérience utilisateur concrète.
Un scénario typique avec une tâche longue ressemble à ceci : ChatGPT appelle un outil MCP, obtient un jobId ; ensuite, pour ce jobId, le serveur envoie des événements de progression, de fin ou d’erreur ; votre widget ou la logique de l’agent met à jour l’UI et prend des décisions sur cette base.
Sur un diagramme de séquence, on peut le dessiner ainsi :
sequenceDiagram
participant User as Utilisateur
participant GPT as ChatGPT (modèle)
participant App as Serveur MCP GiftGenius
participant Widget as Widget GiftGenius
User->>GPT: "Propose des cadeaux pour 2000 employés"
GPT->>App: tools.call start_bulk_gift_analysis
App-->>GPT: response { jobId: "bulk_2025_001" }
GPT->>Widget: ToolOutput { jobId }
Widget->>Widget: Afficher une barre de progression
App-->>GPT: notification job.started
App-->>GPT: notification job.progress (10%, 40%, 70%, 100%)
App-->>GPT: notification job.completed { resultResourceId }
GPT->>Widget: Transmet les événements/données au widget
Widget->>User: Met à jour la progression et affiche le résultat
En pratique, le diagramme réel sera un peu plus complexe, mais l’idée clé est simple : les événements MCP sont le « système nerveux » entre vos opérations en arrière‑plan et l’expérience utilisateur.
10. Erreurs courantes lors du travail avec les événements MCP
Erreur n° 1 : « Événement = log au format production ».
Parfois, les développeurs commencent par simplement relayer dans MCP ce qu’ils écrivaient auparavant dans console.log. Résultat : les événements n’ont ni eventId, ni jobId, ni timestamp correct, seulement des messages semi‑poétiques « on a presque fini ». Cette approche rend le système fragile : difficile à parser, impossible à dédupliquer, l’UI ne sait pas à quelle tâche se rapporte le message. Mieux vaut concevoir les événements dès le départ comme un contrat formel : nom de méthode clair, jeu de champs stable, payload logique.
Erreur n° 2 : Absence d’idempotence et d’un eventId unique.
Beaucoup partent de l’idée naïve : « bah, les événements arrivent une fois ». Une semaine plus tard, ça commence : lors d’une reconnexion client, les notifications se dupliquent, l’utilisateur reçoit deux fois la même chose, le backend commercial crédite les bonus deux fois. Sans eventId unique et une déduplication élémentaire côté client, vous finirez tôt ou tard par rencontrer un bug sérieux. Dans un système distribué, il faut raisonner en « at-least-once delivery » : les doublons sont inévitables.
Erreur n° 3 : Mélanger événements système et métier dans une même soupe.
Par exemple, dans un même flux arrivent logging/message, job.progress, job.completed, resources/updated, le tout sans séparation claire via type/method. L’UI finit par faire des choses étranges comme if (message.includes("terminé")) pour comprendre que la tâche est finie. Mieux vaut séparer clairement : il y a des notifications système (logs, heartbeat) et des événements métier (job.*, resource.*) avec des schémas strictement décrits.
Erreur n° 4 : Transitions d’états de tâche incohérentes.
Il arrive que le serveur, dans un même flux d’événements, envoie d’abord job.completed, puis soudain job.progress, puis job.failed. Cela se produit s’il n’y a pas d’automate d’états explicite ni de vérifications à l’émission des événements. Les clients ne peuvent plus comprendre ce qui se passe réellement. Il vaut mieux décrire un automate fini d’états et ne pas émettre d’événements qui le violent : par exemple, après completed, on peut au mieux envoyer un événement informatif supplémentaire, mais pas ramener la tâche à running.
Erreur n° 5 : Liaison rigide à des noms de méthodes MCP spécifiques à la version courante de la spec.
La spécification MCP évolue encore. Si vous accrochez tout à des méthodes précises actuelles avec des noms système, sans prévoir vos propres espaces de noms, le moindre changement de protocole vous forcera à réécrire la moitié du système. Mieux vaut considérer les événements comme votre mini‑spec au‑dessus de MCP : vous pouvez vous appuyer sur les méthodes existantes (notifications/progress, resources/updated), mais concevez les événements métier (notifications/job/*) dans votre propre espace de noms et gardez‑les relativement indépendants.
Erreur n° 6 : Pas de lien entre les événements et l’UX.
Parfois, l’équipe construit un beau modèle événementiel côté backend, mais ne l’amène jamais jusqu’au widget : job.progress existe dans les logs, mais l’UI affiche un spinner solitaire pendant 40 secondes. Dans un tel scénario, l’utilisateur ne croit ni en MCP ni en l’IA. En concevant les événements, pensez toujours à l’effet UI concret que vous voulez obtenir : barre de progression, étapes, résultats partiels. Les événements MCP existent non pas pour le protocole, mais pour un comportement d’application compréhensible.
GO TO FULL VERSION