MCP Fusion/Core concepts/Middleware et contexte

Middleware et contexte

Demandez à l’IA à propos de Vinkius

Composez authentification, isolation des tenants et limitation de débit avec une dérivation de contexte typée : un appel à defineMiddleware enrichit ctx pour chaque handler en aval, compilé en O(1) au build.

Le middleware est l’endroit où un connecteur cesse d’être un ensemble de fonctions pour devenir un service : l’identité est résolue, les tenants sont isolés, le trafic est limité et chaque appel est audité. Le modèle de middleware de MCP Fusion est petit, typé de bout en bout et compilé avant la première requête.

Deux formes, un concept

La dérivation de contexte est la forme idiomatique. Un middleware renvoie les éléments qu’il ajoute à 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 };
});

L’utilisation d’un tool l’attache :

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

Le système de types transporte la dérivation : .use(withUser) change le type de contexte du builder en AppContext & { user }, donc ctx.user est compilé dans le handler et retirer le middleware fait échouer le build. Aucun cast d’exécution ne reste à découvrir en production.

La forme classique existe aussi

Un middleware qui doit agir avant et après, ou court-circuiter, utilise (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;
};

Renvoyer un ToolResponse depuis un middleware court-circuite le flux : next() n’est jamais appelé et cette réponse devient la réponse. C’est ainsi que inputFirewall et rateLimit rejettent un appel sans toucher au handler. Oublier return next() déclenche un avertissement unique dans la console, car tout développeur a déjà fait cette erreur.

Lever une exception fonctionne aussi : throw toolError('NOT_FOUND', ...) traverse le pipeline avec le code et la récupération intacts. Toute autre valeur levée est enveloppée comme INTERNAL_ERROR.

Ordre et portée

Trois portées se composent en une chaîne par action :

PortéeDéclarée surPosition
Globalef.middleware() et au niveau du registryla plus externe
GroupeActionGroupBuilder.use()intermédiaire
Tool ou action.use() sur le builderla plus interne

Le middleware global s’exécute en premier, avec l’authentification avant tout, et celui de l’action s’exécute en dernier, au plus près de votre handler. Les chaînes sont compilées dans buildToolDefinition() en une closure par action, donc le chemin de requête est un appel, pas une boucle sur un tableau.

Les composants intégrés

Le framework fournit trois middlewares ordinaires, sans magie :

  • inputFirewall({ judge }) : LLM-as-Judge sur les arguments, échec fermé, renvoie INPUT_REJECTED
  • rateLimit({ windowMs, limit, keyFn }) : fenêtre glissante sur les timestamps ; les requêtes rejetées ne sont pas enregistrées, un client abusif ne peut donc pas prolonger son propre blocage
  • auditTrail({ sink, hashArgs }) : émet un événement par appel avec un digest SHA-256 des arguments, jamais les arguments eux-mêmes

Le middleware d’authentification vient des paquets dédiés : requireJwt, requireApiKey et requireAuth. Consultez Authentification.

L’isolation par construction

contextFactory s’exécute par requête et renvoie un objet vierge ; le middleware y écrit ensuite les clés dérivées avec une protection qui ignore __proto__, constructor et prototype. Deux requêtes concurrentes ne peuvent donc pas partager un identifiant de tenant, un utilisateur ou une connexion de base de données, même si elles atteignent le même processus de longue durée. Avec un transport stateless, chaque requête reçoit un serveur vierge ; en stdio, la même garantie vient de l’objet de contexte propre à la requête.

C’est le mécanisme qui se trouve derrière les Connecteurs multi-tenant : l’isolation est une propriété du runtime, pas une discipline que vos handlers doivent retenir.

Étapes suivantes