MCP Fusion/Core concepts/Middleware und Kontext

Middleware und Kontext

Frag die KI über Vinkius

Kombinieren Sie Authentifizierung, Tenant-Isolierung und Rate-Limiting mit typisierter Kontextableitung: Ein defineMiddleware-Aufruf reichert ctx für jeden nachgelagerten Handler an und wird zur Build-Zeit in O(1) kompiliert.

Middleware ist der Punkt, an dem ein Connector aufhört, eine Sammlung von Funktionen zu sein, und zu einem Service wird: Identität wird aufgelöst, Tenants werden isoliert, Datenverkehr wird begrenzt und jeder Aufruf wird auditiert. Das Middleware-Modell von MCP Fusion ist klein, durchgängig typisiert und vor der ersten Anfrage kompiliert.

Zwei Formen, ein Konzept

Kontextableitung ist die idiomatische Form. Eine Middleware gibt die Teile zurück, die sie zu ctx hinzufügt:

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

Tool-Nutzung bindet sie ein:

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

Das Typsystem führt die Ableitung weiter: .use(withUser) ändert den Kontexttyp des Builders zu AppContext & { user }, sodass ctx.user im Handler kompiliert und das Entfernen der Middleware den Build fehlschlagen lässt. Es gibt keinen Laufzeit-Cast, den Sie erst in der Produktion entdecken.

Die klassische Form gibt es ebenfalls

Middleware, die vor und nach dem Handler agieren oder vorzeitig abbrechen muss, verwendet (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;
};

Eine ToolResponse aus einer Middleware beendet den Ablauf sofort: next() wird nie aufgerufen und diese Antwort ist das Ergebnis. So weisen inputFirewall und rateLimit einen Aufruf zurück, ohne den Handler zu berühren. Wenn Sie return next() vergessen, erscheint einmalig eine Konsolenwarnung, weil jeder Entwickler diesen Fehler schon einmal gemacht hat.

Werfen funktioniert ebenfalls: throw toolError('NOT_FOUND', ...) durchläuft die Pipeline mit intaktem Code und Recovery. Jeder andere geworfene Wert wird als INTERNAL_ERROR verpackt.

Reihenfolge und Geltungsbereich

Drei Bereiche werden zu einer Kette pro Aktion zusammengesetzt:

BereichDeklariert aufPosition
Globalf.middleware() und auf Registry-Ebeneäußerster
GruppeActionGroupBuilder.use()mittlerer
Tool oder Aktion.use() am Builderinnerster

Globale Middleware läuft zuerst, mit Authentifizierung vor allem anderen, und die Middleware pro Aktion zuletzt, direkt vor Ihrem Handler. Ketten werden in buildToolDefinition() zu einer Closure pro Aktion kompiliert, daher ist der Anfragepfad ein Aufruf und keine Schleife über ein Array.

Die integrierten Komponenten

Drei Middleware-Komponenten werden mit dem Framework ausgeliefert. Sie sind normale Middleware, keine Magie:

  • inputFirewall({ judge }): LLM-as-Judge für die Argumente, Fail-Closed, gibt INPUT_REJECTED zurück
  • rateLimit({ windowMs, limit, keyFn }): Sliding Window über Zeitstempel; abgelehnte Anfragen werden nicht aufgezeichnet, sodass ein missbräuchlicher Client seine eigene Sperre nicht verlängern kann
  • auditTrail({ sink, hashArgs }): gibt pro Aufruf ein Ereignis mit einem SHA-256-Digest der Argumente aus, niemals die Argumente selbst

Authentifizierungs-Middleware kommt aus den dedizierten Paketen: requireJwt, requireApiKey und requireAuth. Siehe Authentifizierung.

Isolierung durch Konstruktion

contextFactory läuft pro Anfrage und gibt ein neues Objekt zurück. Die Middleware schreibt anschließend abgeleitete Schlüssel mit einem Schutz hinein, der __proto__, constructor und prototype überspringt. Zwei gleichzeitige Anfragen können daher keine Tenant-ID, keinen Benutzer und kein Datenbank-Handle teilen, selbst wenn sie denselben langlebigen Prozess erreichen. Bei einem zustandslosen Transport erhält jede Anfrage einen neuen Server; bei stdio kommt dieselbe Garantie vom Anfrage-Kontextobjekt.

Das ist der Mechanismus hinter Multi-Tenant-Connectoren: Isolation ist eine Eigenschaft der Laufzeit und keine Disziplin, an die Ihre Handler denken müssen.

Nächste Schritte