MCP Fusion/Protocol and runtime/Streaming et annulation

Streaming et annulation

Demandez à l’IA à propos de Vinkius

Les tools de longue exécution bien faites : des handlers générateurs qui émettent de la progression, une propagation d’AbortSignal qui survit à chaque étape, et une éllicitation MRTR basée sur return pour les entrées manquantes.

Un appel de tool paraît atomique pour un agent, mais le travail derrière ne l’est pas. Cette page couvre les trois mécanismes des appels non atomiques : la progression qui sort en streaming, l’annulation qui entre, et l’entrée demandée en cours de route.

Streaming : les handlers générateurs

N’importe quel handler peut être un générateur asynchrone :

typescript
export default f.action('report.generate')
  .describe('Build the quarterly report')
  .handle(async function* (input, ctx) {
    const rows = await loadRows(input);
    yield progress(30, 'rows loaded');
    const charts = await renderCharts(rows);
    yield progress(90, 'charts rendered');
    return charts;                       // THIS is the tool response
  });

Le contrat est strict : yield est un canal latéral, return est la réponse. Seuls les événements progress(percent, message) sont visibles par le client ; tout autre yield est ignoré. La valeur de retour finale passe par le pipeline Presenter normal, donc streaming et perception façonnée se composent.

La progression parvient au client sous forme de notifications/progress MCP quand, et seulement quand, l’appel portait un _meta.progressToken. Pas de token, pas de canal, pas d’allocation. Comme la progression a besoin d’une session vivante, le mode JSON sans état n’a pas de progression : c’est le trade-off du transport, documenté honnêtement.

Annulation : le signal qui va partout

Le AbortSignal de la requête est propagé à chaque étape du moteur :

  1. attente en file : une requête annulée quitte immédiatement la file de concurrence
  2. chain gate : la chaîne de middleware compilée vérifie signal.aborted avant de s’exécuter
  3. mutex destructif : le sérialiseur FIFO des mutations écarte les waiters interrompus
  4. générateurs : chaque yield revérifie le signal et chaque next() fait la course avec l’abort, si bien qu’un générateur bloqué sur un I/O lent ne peut pas prendre le serveur en otage ; à l’abort, le framework appelle gen.return() en fire-and-forget pour exécuter son nettoyage finally sans bloquer la réponse

Le signal arrive dans votre code via ctx.signal (capturez-le dans contextFactory, où extra contient la requête brute) et vous le transmettez à fetch, aux ORM et aux drivers. L’abort coopératif fait de l’annulation non pas un vœu mais une garantie : un job long s’arrête en une seule étape d’I/O.

Entrée interactive : MRTR au lieu de bloquer

Quand une tool a besoin d’une entrée que l’agent n’a pas fournie, l’ancien patron était une requête initiée par le serveur, qui ne fonctionne que sur des connexions persistantes. La réponse de MCP 2.0, ce sont les Multi Round-Trip Requests : le handler retourne le fait qu’il lui faut une entrée, le client la collecte, et le serveur réexécute le handler avec les réponses. MCP Fusion en fait un seul import :

typescript
import { ask, requireInput, readInput } from '@mcpfusion/core';

export default f.mutation('billing.refund')
  .withString('id', 'Invoice ID')
  .interactive()
  .handle(async (input, ctx) => {
    const invoice = await getInvoice(input.id);

    const confirm = readInput('confirm');
    if (!confirm) {
      return requireInput.elicit(
        `Refund ${invoice.amountCents / 100} to ${invoice.customer}?`,
        { confirm: ask.boolean('Approve the refund') },
      );
    }

    return processRefund(invoice);
  });

ask.string(desc), ask.number(desc).min().max(), ask.boolean() et ask.enum(values) sont des descripteurs de champ ; requireInput.elicit(message, fields) demande un formulaire et requireInput.url(message, url) une visite d’URL hors bande (les écrans de consentement OAuth, le cas classique). readInput renvoie les réponses sur l’appel réexécuté, et readRequestState() donne le payload de continuation : un état qui doit survivre à l’aller-retour.

Deux garanties : sur les connexions de l’ère 2026, le framework émet la réponse telle quelle et le protocole pilote la nouvelle tentative ; sur les connexions persistantes de l’ère 2025, il satisfait la requête sur le canal vivant lui-même. Sans canal disponible, l’appel dégrade vers une erreur propre ELICITATION_UNSUPPORTED, et la boucle est plafonnée (huit tours) pour qu’un client perplexe ne fasse pas le ping-pong éternellement.

La forme impérative (un appel ask() attendu dans le handler) a été supprimée dans MCP Fusion 5.0 parce que bloquer une requête en attendant une réponse humaine ne survit pas au serverless. Si vous trouvez de vieux exemples avec await ask(...), la forme MRTR ci-dessus est l’API actuelle.

Prochaines étapes