MCP Fusion/Core concepts/Middleware y contexto

Middleware y contexto

Pregunta a la IA sobre Vinkius

Combina autenticación, aislamiento de tenants y limitación de tasa con derivación de contexto tipada: una llamada a defineMiddleware enriquece ctx para cada handler posterior, compilada en O(1) durante el build.

Middleware es donde un conector deja de ser un conjunto de funciones y se convierte en un servicio: se resuelve la identidad, se aíslan los tenants, se limita el tráfico y se audita cada llamada. El modelo de middleware de MCP Fusion es pequeño, está tipado de extremo a extremo y se compila antes de la primera solicitud.

Dos formas, un concepto

Derivación de contexto es la forma idiomática. Un middleware devuelve las partes que añade a ctx:

typescript
import { initMCPFusion } from '@mcpfusion/core';

interface AppContext {
  db: PrismaClient;
  user?: { id: string; role: 'viewer' | 'admin' };
}

const f = initMCPFusion<AppContext>();

const withUser = f.middleware(async (ctx) => {
  const token = ctx.headers?.authorization;
  const user = await verify(token);
  if (!user) throw f.error('UNAUTHORIZED', 'Sign in first');
  return { user };
});

Uso de la tool la conecta:

typescript
f.query('billing.list_invoices')
  .use(withUser)
  .handle(async (input, ctx) => {
    return ctx.db.invoices.findMany({
      where: { userId: ctx.user.id },
    });
  });

El sistema de tipos transporta la derivación: .use(withUser) cambia el tipo de contexto del builder a AppContext & { user }, así que ctx.user compila dentro del handler y quitar el middleware rompe el build. No hay ningún cast en tiempo de ejecución que descubrir en producción.

También existe la forma clásica

El middleware que necesita actuar antes y después, o interrumpir el flujo, usa (ctx, args, next):

typescript
import type { MiddlewareFn } from '@mcpfusion/core';

const audit: MiddlewareFn<AppContext> = async (ctx, args, next) => {
  const started = Date.now();
  const result = await next();
  ctx.audit?.({ tool, args, ms: Date.now() - started });
  return result;
};

Devolver un ToolResponse desde un middleware interrumpe el flujo: nunca se llama a next() y esa respuesta es la respuesta final. Así inputFirewall y rateLimit rechazan una llamada sin tocar el handler. Olvidar return next() activa una advertencia única en la consola, porque todo desarrollador comete ese error alguna vez.

Lanzar una excepción también funciona: throw toolError('NOT_FOUND', ...) atraviesa el pipeline con el código y la recuperación intactos. Cualquier otro valor lanzado se envuelve como INTERNAL_ERROR.

Orden y ámbito

Tres ámbitos se componen en una cadena por acción:

ÁmbitoDeclarado enPosición
Globalf.middleware() y a nivel del registrymás externo
GrupoActionGroupBuilder.use()intermedio
Tool o acción.use() en el buildermás interno

El middleware global se ejecuta primero, con la autenticación antes que todo, y el middleware por acción se ejecuta al final, más cerca de tu handler. Las cadenas se compilan en buildToolDefinition() en un closure por acción, así que la ruta de la solicitud es una llamada, no un bucle sobre un array.

Los componentes integrados

El framework incluye tres middlewares que son middlewares normales, no magia:

  • inputFirewall({ judge }): LLM-as-Judge sobre los argumentos, falla cerrada y devuelve INPUT_REJECTED
  • rateLimit({ windowMs, limit, keyFn }): ventana deslizante sobre timestamps; las solicitudes rechazadas no se registran, así que un cliente abusivo no puede prolongar su propio bloqueo
  • auditTrail({ sink, hashArgs }): emite un evento por llamada con un digest SHA-256 de los argumentos, nunca los argumentos mismos

El middleware de autenticación procede de los paquetes dedicados: requireJwt, requireApiKey y requireAuth. Consulta Autenticación.

Aislamiento por construcción

contextFactory se ejecuta por solicitud y devuelve un objeto nuevo; el middleware escribe después las claves derivadas en él con una protección que omite __proto__, constructor y prototype. Por tanto, dos solicitudes simultáneas no pueden compartir un id de tenant, un usuario ni un handle de base de datos, aunque lleguen al mismo proceso de larga duración. En un transporte stateless cada solicitud obtiene un servidor nuevo; en stdio, la misma garantía procede del objeto de contexto por solicitud.

Este es el mecanismo detrás de Conectores multi-tenant: el aislamiento es una propiedad del runtime, no una disciplina que tus handlers deban recordar.

Siguientes pasos