Site
Alle Artikel

Veröffentlicht 21. Sept. 202611 Min. Lesezeit

Eigene MCP-Connectoren bauen: warum MCP Fusion der entscheidende Punkt ist

Was zwischen Ihren Daten und der Wahrnehmung eines Agents liegt, und warum diese Lücke eine Architektur ist: die MVA-Trennung, die den Egress schließt, der Presenter, der festlegt, was der Agent sieht, selbstheilende Fehler, Zustand, den der Agent spüren kann, und ein Deploy, das all das als ein einziges, hashbares Bundle liefert.

Renato Marinho

Von Renato Marinho

Founder · Vinkius

The anatomy of an MCP connector: source of truth on the left, the MVA stack in the middle with the model as egress boundary, the presenter as perception and the tools as verbs, and the perception package the agent receives on the right; undeclared fields are stripped at the model layer.

Ein MCP-Connector ist eine Zusage. Du zustimmst einem System, das weder deinen Kontext noch deine Historie noch deine Absicht teilt, dass ein Aufruf von billing.void_invoice genau das bedeutet, was du meinen wolltest. Die Datenbankzeile ist keine Antwort. Sie ist Rohmaterial, und zwischen ihr und dem Agenten liegt eine Schicht Architektur, die die meisten handgeschriebenen Server überspringen.

Ein Agent ist per Konstruktion stochastisch: Er halluziniert Parameter, formatiert Eingaben falsch, retryt ohne nachzudenken und verliert zwischen Aufrufen den Kontext. Ein roher Server behandelt jeden Tool-Aufruf als unabhängig. Genau diese eine Lücke verwandelt eine funktionierende Demo in ein System, das Daten beschädigt, Tokens verbrennt und auf Wegen scheitert, die niemand nachvollziehen kann. Roher JSON an einen Agenten zu senden erzeugt vier strukturelle Fehlerszenarien: Kontextmangel, Aktionsblindheit, Wahrnehmungsinkonsistenz und Sicherheitsleck. Das sind Defizite, die kein Maß an Prompt Engineering behebt.

Das ist die These dieses Artikels. Wenn die Fehlerszenarien strukturell sind, muss die Lösung strukturell sein. MCP Fusion macht diese Lösung mit einem Muster, das MVA heißt. Das Muster ist eine Trennung von Verantwortlichkeiten, die Anwendungskode längst kennt. Nur der Konsument ist ein anderer:

  • Der Model besitzt, was die Daten sind und was den Prozess verlassen darf.
  • Der Presenter besitzt, was der Agent davon wahrnimmt.
  • Die Tools besitzen die Verben: die Queries, Mutationen und Aktionen.

Die MVA-Pipeline: Eine rohe Zeile überschreitet die Model-Grenze, nicht erklärte Felder werden entfernt, und der Presenter stellt das Wahrnehmungs-Paket zusammen, das der Agent tatsächlich empfängt

Eine einzige Regel hält die Trennung ehrlich. Sie ist eine Sicherheitseigenschaft, keine Stilfrage: die Richtung. Tools importieren Presenter, Presenter importieren Models, Models importieren den Kern. Nichts importiert rückwärts. Die Schicht, die deine Daten berührt, darf nie die Schicht sein, die ein Agent steuern kann.

Model: Am Ende der Leitung

In einer normalen Anwendung validiert ein Schema die Eingaben. In einem Connector muss es auch die Ausgabe schließen. Denn auf der anderen Seite der Leitung sitzt kein Kollege, der Code liest. Es ist ein Sprachmodell, das mit allem agiert, was man ihm hinlegt. defineModel zieht diese Grenze. Seine vier Deklarationen machen vier verschiedene Jobs:

m.casts erklärt die Felder, ihre Typen und ihre Beschreibungen. Diese Beschreibungen sind keine Dokumentation für Menschen. Sie werden just in time zu den Interpretationsregeln kompiliert, die der Agent mit jeder Antwort bekommt. m.hidden erklärt die Felder, die nie die Leitung erreichen: Passwort-Hashes, interne Flags, Tenant-Marker. m.guarded erklärt die Felder, die nie von einem Agenten kommen können. m.fillable erklärt die Eingabe-Profile: create, update und filter. Die Parameter eines Tools werden aus diesen Profilen abgeleitet, nicht von Hand neu getippt.

Die Konsequenz ist die, die in Produktion zählt. Wenn eine Migration eine Spalte zur Tabelle hinzufügt, leckt diese Spalte nicht. Sie bleibt von der Leitung ausgeschlossen, bis jemand sie in das Model legt und jemand, bewusst, sie in den Presenter legt. Ein roher Server hat diese Eigenschaft nicht. Seine Ausgabe ist die Zeile, serialisiert. Das heißt: Passwort-Hash, interner Flag und Tenant-ID landen im Kontextfenster des Agenten, im Moment, in dem ein neues Feld im Schema ankommt.

Presenter: Die Wahrnehmung, die du nie gezeigt hast

Das V in MVA ist nicht für das menschliche Auge. Es ist das Paket, das der Agent wahrnimmt. Es wird aus vier Schichten zusammengesetzt.

Daten, die die Firewall überlebt haben. Bevor etwas serialisiert wird, läuft das Schema des Presenters im Strip-Modus über das rohe Ergebnis. Was auch immer die Datenbank zurückgibt: Der Agent sieht nur die erklärte Oberfläche. Das ist Egress-Kontrolle auf RAM-Ebene. Keine View-Schicht, kein Template, keine Konvention. Ein Feld, das das Schema nicht kennt, kann nicht hinüber.

Regeln, just in time geliefert. Die Feldbeschreibungen des Models kompilieren zu Systemregeln, die an diese Antwort für diese Entität gehängt werden. Der Agent trägt keinen globalen Prompt mit tausenden Tokens. Er bekommt die Interpretationsregeln für das, worum er gebeten hat. Das Domänenwissen liegt an einem Ort und wird nur ausgeliefert, wenn die Domäne im Spiel ist.

Eine Arbeitsgrenze. Der Presenter erklärt, wie viele Einträge dieser Agent in einer Antwort sehen darf und was er erfährt, wenn die Liste abgeschnitten wird. Eine Liste mit zehntausend Zeilen ist für einen Agenten keine Datenquelle. Sie ist ein Denial of Service in Datenkleidung. Die Grenze gehört zur Wahrnehmung, und der Hinweis auf den Bruch ist das, was den Agenten davon abhält, vorzutäuschen, er habe mehr gesehen, als er gesehen hat.

Affordances. suggestActions sagt dem Agenten, was er als Nächstes mit dem tut, was er gerade gesehen hat. Die Doku nennt es HATEOAS für Agenten. Genau hier stirbt die Aktionsblindheit: Die Antwort ist kein Payload, sondern eine Position in einem Workflow. Vom Server gerenderte Chart- und Diagrammblock-Teile gehören zum selben Paket. Und sie sind deterministisch: Das Framework rendert sie, der Agent liest sie. Kein Modell in der Schleife erzeugt die Pixel.

Tools: Verben mit Intention

Ein Tool in diesem Framework ist keine benannte Funktion mit Schema. Es ist ein semantisches Verb mit einer Default-Intention. f.query ist read-only. f.mutation ist standardmäßig destruktiv. f.action ist neutral. Das sind keine Metadaten-Labels. Sie steuern, was die Plattform als sicher zum Retry einstuft, was die Observability-Pipeline markiert und welches Governance-Tool flaggt, wenn aus einem Lesezugriff ein Schreibzugriff wird.

Vom Verb aus ist die Kette bewusst klein. .fromModel zieht die Eingabeform aus dem fillable-Profile des Models, damit die Parameter des Tools aus derselben Deklaration abgeleitet werden, die den Egress schließt. .returns hängt einen Presenter an die Antwort. .proxy schreibt den Handler für dich: Er bestimmt die HTTP-Methode aus dem Verb, löst die Pfadparameter aus der Eingabe auf und entpackt die Antwort-Envelope. Die .with-Schritte bleiben für domänenspezifische Eingaben, die ein Model nicht ausdrücken kann.

f.router gruppiert Verben unter einem Präfix und vererbt Middleware und Tags an jedes. f.middleware leitet einen getypten Kontext flussab. Der Tenant-Identifikator kommt aus einem verifizierten Credential in diesem Kontext. Deshalb kann die Doku ungeschminkt sagen: Der Agent kann ihn nicht überschreiben. Concurrency-Caps und Egress-Bytegrenzen hängen an derselben Kette. Und wenn ein Workflow statt eines Tools einen Prompt braucht, baut definePrompt ihn aus demselben Presenter: Die Regeln werden zur System-Message, Daten und UI zum User-Block. Eine Quelle der Wahrheit, zwei Oberflächen.

Fehler, die lenken, und State, den der Agent spüren kann

Ein roher Server antwortet auf einen schlechten Aufruf mit einem flachen String. Die Antwort des Agenten auf einen flachen String ist: retry, mit derselben Eingabe, und noch einmal. In dieser Schleife sterben Token-Budgets. Und hier wird ein falsches Refund viermal versucht.

Die Antwort des Frameworks ist eine selbstheilende Envelope. f.error baut sie aus einem konkreten Code, einer Message, einem Vorschlag, einer Liste von Aktionen, die der Agent stattdessen ausführen kann, optionalen Details und einem Retry-Fenster:

<tool_error code="InvoiceNotFound">
  <message>Invoice "INV-999" does not exist.</message>
  <recovery>Call billing.list_invoices first to find valid IDs.</recovery>
  <available_actions>billing.list_invoices</available_actions>
</tool_error>

Konkrete Codes schlagen generische. AlreadyPaid sagt dem Agenten etwas, das BAD_REQUEST nicht kann. Und die Recovery-Zeile nimmt das Raten, das Retry-Schleifen teuer macht.

State ist das andere Sinnorgan, das ein Sprachmodell nicht hat. Nach einer Mutation glaubt der Agent immer noch, die Liste, die er vor der Mutation geholt hat, sei aktuell. Die Antwort des Frameworks sind State-Sync-Signale in der Antwort, übernommen aus dem HTTP-Caching. Ein Tool markiert sein Ergebnis als immutable, volatile oder causal. Ein immutable Ergebnis darf vertraut werden. Ein volatile Ergebnis heißt: Frag mich erneut. Eine causal-Markierung heißt: Nach dieser Mutation müssen diese anderen Verben neu abgefragt werden. Causalität, nicht Zeit. Genau das, was ein Agent ohne Uhr braucht.

Deploy: Der Connector als Bundle

Ein so gebauter Connector wird als ein einzelnes Artefakt ausgeliefert. Die CLI macht den ganzen Job. mcpfusion deploy bündelt den Server in eine einzige selbstenthaltene Datei: Alle Dependencies inline, Node-Builtins durch Stubs ersetzt, die nur existieren, um den Bundler zu besänftigen, und nie aufgerufen werden. Auch der Transport ist gestubbt, weil die Plattform ihn liefert. Das Bundle besteht eine Größensperre von 1,5 MB im Rohtext. Das ist ein Budget, keine Spezifikation. Danach wird es komprimiert und gehasht und wandert an die Edge. Passt der Hash zum deployed, meldet die Plattform ein instant restore: dieselben Bytes, kein Reload.

Zwei Schritte lohnen ein Verständnis. Sie machen die Architektur auditierbar. Die CLI führt ein zweites, introspektives Kompilieren des Bundles durch: Sie extrahiert die Tool-Verträge, die Prompts und das Credential-Schema und schreibt sie in eine Capability-Lockfile. Das ist eine deterministische Snapshot des Verhaltensumfangs des Connectors. Die Lockfile ist per Git diffbar. Ein fusion lock --check in CI ist das Tor: Die Oberfläche, die dein Code tatsächlich exponiert, wird mit der Oberfläche verglichen, die du committed hast. Der Diff wird eingeordnet als breaking, risky, safe oder cosmetic. Das Protokoll kennt keinen Mechanismus für Drift. Das Framework gibt dir einen. Das ist der Unterschied zwischen einem Deploy, das du nicht auditieren kannst, und einem, das ein Reviewer lesen kann.

Das gleiche Bundle spricht die aktuelle Ära des Protokolls: stateless, pro Request, hinter jedem Load Balancer. Und die Registry, die es gebaut hat, läuft unverändert auf stdio und auf HTTP in der Ära 2025. So bleiben lokale Entwicklung und Produktion auf demselben Code.

Edge-Deployment hat drei Restriktionen. Alle drei sind Designentscheidungen: Die Tool-Registry wird explizit registriert, weil Discovery in einem Dateisystem scannt, das dort nicht existiert. Native Addons gibt es nicht. Und nichts im Bundle berührt den Prozess. Explizite Imports und explizite Registrierung. Der Server ist Edge-tauglich.

Das Deploy als Artefakt: Quellbaum und Lockfile links, die CLI-Pipeline in der Mitte, und dieselbe Registry rechts, die auf stdio, HTTP und der stateless Edge läuft

Warum mit MCP Fusion bauen

Die Frage hinter all dem ist die, die sich direkt beantworten lohnt: Warum nicht einen einfachen MCP-Server schreiben und Sicherheit dazulegen, wo es schmerzt?

Weil die Fehlerszenarien strukturell sind und strukturelle Fixes im Framework sitzen, nicht im Handler. Ein roher Server hat sechs Abwesenheiten. Dieser hat sechs Präsenzen:

  • Er leckt, was er zurückgibt. Dieser schließt den Egress in der Model-Layer. Eine neue Spalte erreicht einen Agenten nicht, bis jemand sie erklärt.
  • Er erzwingt nichts. Dieser friert die Registry nach dem Anhängen ein und hält die Pipeline-Reihenfolge per Konstruktion. Sicherheit wird erzwungen, nicht konventionell.
  • Er sieht seinen eigenen Drift nicht. Dieser hasht den Verhaltensumfang in eine Lockfile und ordnet jede Änderung vor dem Merge ein.
  • Er beantwortet Fehler mit Strings. Dieser beantwortet mit Recovery-Anweisungen und den nächsten verfügbaren Aktionen.
  • Er ist blind für Zeit. Dieser trägt causale Invalidierungs-Signale in den Antworten mit.
  • Er ist eine Oberfläche, egal wo gehostet. Dieser ist eine Registry über stdio, HTTP, die Vinkius-Edge und Serverless-Targets. Mit Observability, die sich auf SOC 2-Kontrollen abbildet und in ein SIEM weiterleiten kann.

Zwei Multiplikatoren ändern die Ökonomie der Arbeit selbst. Du kannst den Connector aus einem vorhandenen Vertrag generieren: Eine OpenAPI-Spec oder ein Prisma-Schema wird zu einem vollständigen Server. Egress, Tenant-Isolation und Memory-Protection sitzen im generierten Code. In einem einzigen Befehl. Und die Tests sind die echte Pipeline: Das Testing-Paket läuft deinen Connector im RAM, durch dieselbe Validierung, dieselbe Middleware, denselben Handler, denselben Presenter und denselben Egress wie die Produktion. Mit null Tokens und voller Deterministik. Du assertest: Die Daten haben kein Secret-Feld. Die Regeln sind angekommen. Der Fehler wurde eingeordnet.

Die ehrliche Rechnung ist: Das ist eine Architektur, keine Helper-Bibliothek. Die MVA-Trennung braucht ein paar Tage, um sich einzugewöhnen. Das Bundle-Budget hält die Dependencies schlank. Was du kaufst, ist die Grenze zwischen deinen Daten und der Wahrnehmung eines Agenten. Erzwungen vom Framework, nicht von Review. Für alles, was in Produktion mit Agenten anderer Menschen auf der anderen Seite der Leitung läuft, ist das der Punkt.

Ein Connector, von der Spec bis an die Edge

Der komplette Workflow, Ende zu Ende:

mcpfusion create invoices --vector openapi
mcpfusion remote --server-id <uuid from the dashboard>
mcpfusion deploy

Statt einer generierten Basis zum nackten Start: dieselben drei Befehle mit --vector vanilla. Dann das Minimum aus drei Deklarationen:

defineModel('Invoice', m => {
  m.casts({
    id: m.uuid().label('Invoice ID, a UUID'),
    total: m.number().label('Total, integer cents'),
    status: m.string().label('open, paid, or voided'),
  });
  m.hidden(['webhookSecret', 'internalFlags']);
  m.fillable({ create: ['id', 'total'], filter: ['status'] });
});

const router = f.router('billing');
router.mutation('void_invoice')
  .withString('id')
  .returns(invoiceUI)
  .invalidates('billing.*')
  .proxy('invoices/:id/void');

const tester = createMCPFusionTester(registry, {
  contextFactory: () => ({ tenantId: 't_777' }),
});

const result = await tester.callAction('billing', 'void_invoice', { id: 'INV-7' });

expect(result.data).not.toHaveProperty('webhookSecret');
expect(result.uiBlocks.length).toBeGreaterThan(0);

Der Test läuft die gleiche Pipeline wie die Produktion und beweist, ohne ein einziges Token, dass der Egress gehalten hat. Deploy, die Lockfile in CI prüfen, und der Connector ist ein Hash im Repository, ein Diff im Pull Request und ein isoliertes Programm an der Edge. Dasselbe Objekt, in dreier Gestalt.

Das Runtime, das dieses Programm als Letztes ausführt, versiegelt und per Snapshot wiederhergestellt, ist das Thema von dem Artikel über V8-Isolates.

Themenmcpfusionconnectorsmcpagentsedge