MCP Fusion/Core concepts/Middleware et contexte
Middleware et contexte
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 :
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 :
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) :
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ée | Déclarée sur | Position |
|---|---|---|
| Globale | f.middleware() et au niveau du registry | la plus externe |
| Groupe | ActionGroupBuilder.use() | intermédiaire |
| Tool ou action | .use() sur le builder | la 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é, renvoieINPUT_REJECTEDrateLimit({ 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 blocageauditTrail({ 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
- Authentification : JWT, clés API et flux d’appareil OAuth
- Connecteurs multi-tenant : un connecteur, plusieurs tenants
- Architecture du runtime : où le middleware se place dans le pipeline
