MCP Fusion/Protocol and runtime/Streaming und Abbruch

Streaming und Abbruch

Frag die KI über Vinkius

Langlaufende Tools richtig gemacht: Generator-Handler, die Fortschritt emittieren, eine AbortSignal-Propagierung, die jede Stufe übersteht, und rückkehrbasierte MRTR-Elicitation für fehlende Eingaben.

Ein Tool-Aufruf wirkt für einen Agenten atomar, die Arbeit dahinter ist es nicht. Diese Seite behandelt die drei Mechanismen für nicht-atomare Aufrufe: Fortschritt per Streaming hinaus, Abbruch hinein und Eingaben, die mitten im Lauf angefordert werden.

Streaming: Generator-Handler

Jeder Handler kann ein Async-Generator sein:

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
  });

Der Vertrag ist streng: yield ist ein Nebenkanal, return ist die Antwort. Nur progress(percent, message)-Events sind für Clients sichtbar; alles andere per yield wird ignoriert. Der finalem Rückgabewert durchläuft die normale Presenter-Pipeline, also ergänzen sich Streaming und geformte Wahrnehmung.

Fortschritt erreicht den Client als MCP notifications/progress, wenn und nur wenn der Aufruf einen _meta.progressToken trug. Kein Token, kein Kanal, keine Allokation. Weil Fortschritt eine lebende Session braucht, gibt es im zustandslosen JSON-Modus keinen Fortschritt: das ist der Kompromiss des Transports, ehrlich dokumentiert.

Abbruch: das Signal, das überallhin gelangt

Das AbortSignal der Anfrage wird durch jede Stufe der Engine gereicht:

  1. Warten in der Schlange: eine abgebrochene Anfrage verlässt die Concurrency-Queue sofort
  2. chain gate: die kompilierte Middleware-Kette prüft signal.aborted, bevor sie läuft
  3. destructive Mutex: der FIFO-Serializer für Mutationen verwirft abgebrochene Waiter
  4. Generatoren: jedes yield prüft das Signal erneut, und jedes next() wettet gegen den Abort, damit ein Generator, der an langsamer I/O hängt, den Server nicht als Geisel halten kann; beim Abort ruft das Framework gen.return() fire-and-forget auf, um das finally-Cleanup auszuführen, ohne die Antwort zu blockieren

Das Signal erreicht Ihren Code über ctx.signal (erfassen Sie es in contextFactory, wo extra die rohe Anfrage enthält), und Sie geben es an fetch, ORMs und Driver weiter. Kooperativer Abbruch verwandelt das Abbrechen von einem Wunsch in eine Garantie: ein langer Job stoppt in einem einzigen I/O-Schritt.

Interaktive Eingabe: MRTR statt Blockieren

Wenn ein Tool eine Eingabe braucht, die der Agent nicht geliefert hat, war das alte Muster eine serverinitiierte Anfrage, die nur auf persistenten Verbindungen funktioniert. Die Antwort von MCP 2.0 sind Multi Round-Trip Requests: Der Handler gibt zurück, dass er Eingabe braucht, der Client sammelt sie ein, und der Server betritt den Handler mit den Antworten erneut. MCP Fusion macht daraus einen einzigen 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() und ask.enum(values) sind Feld-Deskriptoren; requireInput.elicit(message, fields) verlangt ein Formular, requireInput.url(message, url) einen Besuch einer URL außerhalb des Bands (OAuth-Consent-Screens, der klassische Fall). readInput liefert die Antworten beim erneut betretenen Aufruf zurück, und readRequestState() gibt Ihnen das Fortsetzungs-Payload: Zustand, der die Hin- und Rückreise überleben muss.

Zwei Garantien: Auf Verbindungen der Ära 2026 emittiert das Framework die Antwort unverändert, und das Protokoll treibt den erneuten Versuch; auf persistenten Verbindungen der Ära 2025 erfüllt es die Anfrage über den lebenden Kanal selbst. Ohne verfügbaren Kanal degradiert der Aufruf zu einem sauberen ELICITATION_UNSUPPORTED-Fehler, und die Schleife ist gedeckelt (acht Runden), damit ein verwirrter Client nicht ewig Ping-Pong spielt.

Die imperativische Form (ein awaited ask()-Aufruf im Handler) wurde in MCP Fusion 5.0 entfernt, weil eine Anfrage auf eine menschliche Antwort warten zu blockieren Serverless nicht übersteht. Wenn Sie alte Beispiele mit await ask(...) finden: die MRTR-Form oben ist die aktuelle API.

Nächste Schritte