MCP Fusion/Protocol and runtime/Streaming y cancelación

Streaming y cancelación

Pregunta a la IA sobre Vinkius

Tools de larga duración bien hechas: handlers generadores que emiten progreso, propagación de AbortSignal que sobrevive a cada etapa y elicitación MRTR basada en return para entradas que faltan.

Una llamada a una tool parece atómica para un agente, pero el trabajo detrás no lo es. Esta página cubre los tres mecanismos para llamadas no atómicas: progreso saliendo en streaming, cancelación entrando y entrada solicitada a mitad de ejecución.

Streaming: handlers generadores

Cualquier handler puede ser un generador asíncrono:

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

El contrato es estricto: yield es un canal lateral, return es la respuesta. Solo los eventos progress(percent, message) son visibles para el cliente; cualquier otra cosa cedida con yield se ignora. El valor de retorno final pasa por el pipeline normal de Presenter, así que streaming y percepción con forma se componen.

El progreso llega al cliente como notifications/progress de MCP cuando, y solo cuando, la llamada llevaba un _meta.progressToken. Sin token, sin canal, sin asignación. Como el progreso necesita una sesión viva, el modo JSON sin estado no tiene progreso: ese es el trade-off del transporte, documentado con honestidad.

Cancelación: la señal que llega a todas partes

El AbortSignal de la petición atraviesa cada etapa del engine:

  1. espera en cola: una petición cancelada sale de la cola de concurrencia de inmediato
  2. chain gate: la cadena de middleware compilada comprueba signal.aborted antes de ejecutarse
  3. mutex destructivo: el serializador FIFO de mutations descarta los waiters abortados
  4. generadores: cada yield vuelve a comprobar la señal y cada next() compite con el abort, así que un generador atascado en un I/O lento no puede tomar el servidor como rehén; al abortar, el framework llama a gen.return() fire-and-forget para ejecutar su limpieza finally sin bloquear la respuesta

La señal llega a tu código a través de ctx.signal (captúrala en contextFactory, donde extra contiene la petición cruda) y tú la pasas a fetch, ORMs y drivers. El abort cooperativo convierte la cancelación de un deseo en una garantía: un job largo se detiene en un solo paso de I/O.

Entrada interactiva: MRTR en lugar de bloquear

Cuando una tool necesita una entrada que el agente no proporcionó, el patrón antiguo era una petición iniciada por el servidor, que solo funciona en conexiones persistentes. La respuesta de MCP 2.0 son los Multi Round-Trip Requests: el handler devuelve que necesita entrada, el cliente la recoge, y el servidor vuelve a entrar en el handler con las respuestas. MCP Fusion lo reduce a un solo 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() y ask.enum(values) son descriptores de campo; requireInput.elicit(message, fields) solicita un formulario y requireInput.url(message, url) una visita a una URL fuera de banda (pantallas de consentimiento OAuth, el caso clásico). readInput devuelve las respuestas en la llamada reentrante, y readRequestState() te da el payload de continuación: estado que debe sobrevivir al viaje de ida y vuelta.

Dos garantías: en conexiones de la era 2026 el framework emite la respuesta tal cual y el protocolo impulsa el reintento; en conexiones persistentes de la era 2025 satisface la petición por el propio canal vivo. Sin canal disponible, la llamada degrada a un error limpio ELICITATION_UNSUPPORTED, y el bucle está limitado (ocho rondas) para que un cliente confuso no haga ping-pong para siempre.

La forma imperativa (una llamada ask() con await dentro del handler) se eliminó en MCP Fusion 5.0 porque bloquear una petición esperando una respuesta humana no sobrevive en serverless. Si encuentras ejemplos antiguos con await ask(...), la forma MRTR de arriba es la API actual.

Próximos pasos