nr-llm verbindet sich per HTTP mit externen Model-Context-Protocol-Servern und registriert deren Tools neben den eingebauten – hinter demselben Gate, derselben Freigabe und demselben Audit.
Warum es existiert
Viele Backend-Systeme bieten ihre Funktionen inzwischen als MCP-Server an: Ticketsysteme, Dokumentationsdienste, interne APIs. Ein Agent-Lauf in TYPO3 soll diese Tools nutzen können. Öffnete jede nutzende Extension ihre eigene MCP-Verbindung, bräuchte jede einen eigenen Agent-Loop, eigene Freigaberegeln und ein eigenes Audit. ADR-116 macht nr_llm deshalb zur einzigen Stelle für Tools: Eingebaute und MCP-Tools werden an einem Ort gesammelt und ausgeführt, und kein Konsument spricht einen MCP-Server direkt an.
Ein entferntes Tool ist Code, den hier niemand einsehen kann. Der Server beschreibt seine Tools selbst, und diese Beschreibung darf nicht darüber entscheiden, wie sie autorisiert werden. Der Betreiber legt daher pro Server fest, welche Datenklasse die Tool-Ausgabe trägt und ob Aufrufe eine menschliche Freigabe brauchen. Jedes importierte Tool gilt als nicht idempotenter Schreibzugriff und verlangt einen Administrator – gleich, was die Annotationen des Servers behaupten (ADR-116, ADR-134).
Eine MCP-Operation besteht aus mehreren HTTP-Requests: dem Handshake, seiner Bestätigung und dem Request mit der eigentlichen Arbeit. Vor ADR-170 hatte jeder Request sein eigenes Timeout. Ein langsamer Server konnte einen Backend-Nutzer so ein Vielfaches der angezeigten Grenze warten lassen. Jetzt gilt ein einziges Budget für die ganze Operation.
Betreiber müssen außerdem sehen können, ob ein Server antwortet, ohne dabei seinen Katalog neu zu schreiben. Der Client speichert bei jeder abgeschlossenen Operation den letzten erfolgreichen Kontakt und dessen Latenz. Der Verbindungstest führt nur den Handshake aus (ADR-154).
Wann man es einsetzt
Ein externes System stellt einen MCP-Endpunkt über HTTP bereit, und Agent-Läufe in TYPO3 sollen seine Tools aufrufen.
Eine KI-Funktion fürs Backend soll eingebaute und MCP-Tools aus einer Quelle beziehen: Die Tools kommen aus ToolRegistry, Läufe werden über AgentRuntime gesteuert, statt selbst MCP-Verbindungen zu öffnen.
Ein Deployment legt MCP-Server-Datensätze an und muss deren Kataloge ohne Klick abrufen: nrllm:mcp:import mit einem Identifier oder mit --all ausführen.
Der entfernte Dienst soll die Rechte jedes einzelnen Nutzers durchsetzen: Der Server wird auf delegierte Authentifizierung umgestellt, damit der Aufruf ein für den auslösenden Akteur getauschtes Token trägt.
„Der Server ist weg“ soll sich von „das Tool wurde umbenannt“ unterscheiden lassen: „Verbindung testen“ nutzen und „Letzter erfolgreicher Kontakt“ mit „Letzter Import“ vergleichen.
Wie es funktioniert
Server sind Datensätze in tx_nrllm_mcp_server und werden im Modul „MCP-Server“ gepflegt, das nur Administratoren sehen. McpImportService holt den Katalog eines Servers nur auf ausdrückliche Aktion (die AJAX-Route nrllm_mcp_import des Moduls oder das CLI-Kommando), prüft jedes angebotene Tool und schreibt die angenommenen nach tx_nrllm_mcp_tool. Der lokale Name eines Tools lautet mcp_<identifier>_<remote name>, seine Gruppe mcp_<identifier>. McpSchemaNormalizer normalisiert das Eingabeschema, oder das Tool wird mit Begründung übersprungen. McpToolProvider trägt den Tag nr_llm.tool_provider. Er baut aus aktivierten Servern mit deklarierter Datenklasse McpTool-Objekte, die ToolRegistry neben den eingebauten Tools anbietet. Dabei liest er nur die Datenbank, nie das Netz.
McpClient verantwortet das Protokoll: initialize, die Bestätigung notifications/initialized, danach tools/list (seitenweise) oder tools/call. Er öffnet pro Operation eine neue Session und pro öffentlicher Methode eine McpOperationDeadline. Jeder Teilschritt erhält, was übrig ist – aufgerundet und nie unter einer Sekunde. Die Gesamtdauer ist mcpOperationTimeout in der Extension-Konfiguration, standardmäßig 20 Sekunden. McpHttpTransport spricht ausschließlich HTTP. Er sendet Accept: application/json, text/event-stream und liest eine als Event-Stream gerahmte Antwort aus einem endlichen Body von höchstens 2 MiB. Einen Stream hält er nicht offen, und vom Server ausgehende Requests beantwortet er nicht.
Jeder Request trägt eine opake String-ID aus einem zufälligen Namensraum pro Prozess. Der Transport nimmt eine Antwort nur an, wenn sie jsonrpc 2.0 und genau diese Request-ID enthält, genau einen der Zweige result oder error hat und result ein Objekt ist. In einem Event-Stream werden Notifications übergangen; eine fremde oder zweite Antwort wird abgelehnt (ADR-227). Nach jeder abgeschlossenen Operation schreibt McpHealthRecorder last_contact und last_latency_ms. Eine fehlgeschlagene Operation hinterlässt nichts, und ein fehlgeschlagener Schreibvorgang lässt den Aufruf nie scheitern.
Die Authentifizierung ist entweder legacy (optional ein nr-vault-Credential als Bearer oder in einem benannten Header) oder delegiert (ADR-217). Im delegierten Modus öffnet McpAuthenticationSessionFactory pro Operation eine Credential-Session für den auslösenden Akteur. Der konfigurierte Subject-Resolver ordnet den Akteur einem Subject-Credential in Vault zu, und McpTokenExchange führt einen Token-Tausch nach RFC 8693 für Audience und Scopes des Servers aus. Das Access-Token liegt unter einer kurzlebigen Vault-Kennung, wird vor jedem Teilschritt geprüft und am Ende der Operation gelöscht. Importe und Verbindungstests nutzen ein eigenes Discovery-Credential. Festgeschrieben ist das Verhalten durch AbstractMcpConformanceTestCase: Dieselben Prüfungen laufen gegen ein zustandsloses Verbindungsprofil und ein Profil, dessen Server eine Session vergibt – über einen simulierten PSR-18-Client.
Netresearch\NrLlm\Service\Tool\Mcp\McpClient
Die MCP-Konversation: Handshake, seitenweises tools/list, tools/call und das reine Handshake-ping(); öffnet eine Deadline pro Operation und speichert die Erreichbarkeit einmal.
Ein JSON-RPC-Round-Trip über HTTP: Host-Prüfung vor dem Credential, Vault-Client pro Aufruf, Lesegrenze von 2 MiB, Auflösen des Event-Streams, Request-IDs und Prüfung der Antwort.
Das Budget einer Operation, gelesen über eine monotone Uhr; gibt jedem Teilschritt die verbleibenden ganzen Sekunden und sendet nicht mehr, sobald es aufgebraucht ist.
Der einzige Schreiber von tx_nrllm_mcp_tool: lehnt Server ohne Datenklasse oder URL ab, überspringt Tools ohne gültigen Namen oder normalisierbares Schema, gleicht den Katalog ab.
Tool-Provider für ToolRegistry; baut McpTool-Objekte aus aktivierten Servern mit deklarierter Datenklasse und aus aktuellen Katalogzeilen, ohne Netzwerkzugriff.
Netresearch\NrLlm\Service\Tool\Mcp\McpTool
Ein entferntes Tool aus Sicht des Gates: nur für Administratoren, standardmäßig deaktiviert, NON_IDEMPOTENT_WRITE, Datenklasse und Freigabe-Flag vom Betreiber.
Schreibt last_contact und last_latency_ms nach einer abgeschlossenen Operation; schluckt Speicherfehler, damit ein erfolgreicher Aufruf nie verloren geht.
Öffnet für delegierte Server die Credential-Session einer Operation: an den auslösenden Akteur gebunden für Tool-Aufrufe, mit dem Discovery-Credential für Importe und Verbindungstests.
Für Entwickler
Den Server im Modul „MCP-Server“ anlegen, seinen Katalog importieren, die gewünschten Tools aktivieren und Operationsbudget sowie Delegationsprofile in der Extension-Konfiguration setzen. Konsumenten arbeiten weiter mit ToolRegistry und AgentRuntime.
Einen Katalog über die CLI importieren
Ein Import wird nur angenommen, wenn der Server-Datensatz eine URL und eine deklarierte Datenklasse hat. Das Kommando ruft denselben McpImportService auf wie die Schaltfläche im Modul. Server werden über ihren Identifier angesprochen; --all importiert jeden aktivierten Server, und ein ablehnender Server hält die übrigen nicht auf. Lehnt ein Server ab, endet das Kommando mit einem Exit-Code ungleich null.
McpTool legt die Antworten fest, die ein entfernter Server nicht über sich selbst geben darf. Das Tool startet deaktiviert, verlangt einen Administrator und meldet immer einen nicht idempotenten Schreibzugriff. Die Datenklasse ist der Wert des Betreibers aus dem Server-Datensatz. Weil McpTool RemoteToolInterface implementiert, lehnt ToolCallPolicy es oberhalb der Obergrenze der Trust-Zone auch im Observe-Modus ab. Die Freigabe folgt über RemoteApprovalInterface dem Feld „Freigabe erforderlich“ des Servers.
php
public function isEnabledByDefault(): bool
{
// An imported tool is inert until an operator enables it. Import is an
// act of configuration; it should not also be an act of granting.
return false;
}
public function requiresAdmin(): bool
{
return true;
}
public function getGroup(): string
{
return 'mcp_' . $this->server->identifier;
}
public function getDataClass(): ToolDataClass
{
return $this->dataClass;
}
public function getEffect(): ToolEffect
{
return ToolEffect::NON_IDEMPOTENT_WRITE;
}
Ein Budget für alle Teilschritte
McpClient öffnet pro Operation eine McpOperationDeadline und reicht sie an jeden Teilschritt weiter. Vor jedem Request prüft McpHttpTransport isExhausted() und bricht mit einer eigenen Meldung ab, die das Budget nennt. Andernfalls übergibt er legTimeoutSeconds() an den HTTP-Client von Vault. Die Untergrenze von einer Sekunde gibt es, weil ein Timeout von null „kein Timeout“ bedeuten würde. Die Gesamtdauer legt mcpOperationTimeout fest (Standard 20).
php
/**
* Whether the budget is gone and the next leg must not be sent.
*/
public function isExhausted(): bool
{
return $this->remainingSeconds() <= 0.0;
}
/**
* The timeout the next leg is granted: what remains, never below the floor.
*
* Rounded UP, so a fraction of a second left is a leg that may run for one
* — the overrun is bounded by a second and the alternative is the unbounded
* request the floor exists to prevent.
*/
public function legTimeoutSeconds(): int
{
return max(self::MINIMUM_LEG_SECONDS, (int) ceil($this->remainingSeconds()));
}
Delegierte Authentifizierung einrichten
Für einen Server im delegierten Modus erhält der Datensatz Delegationsprofil, Audience und Scopes sowie ein Discovery-Credential für Importe und Verbindungstests. Profil und ausdrückliche Akteur-Freigaben stehen in der Installationskonfiguration. Ein Backend-Nutzer ohne aktivierte Freigabe wird abgewiesen; der Administratorstatus erzeugt keine Zuordnung. Credential-Kennungen sind nr-vault-Referenzen, niemals Tokens.
Die Spalte data_class hat keinen Standardwert. McpImportService lehnt den Import mit „The server has no declared data class“ ab, und McpServerRepository::findUsable() filtert einen solchen Server heraus, sodass er keine Tools anbietet. Der Verbindungstest funktioniert trotzdem, denn ein Handshake liefert keine Tool-Ausgabe, die einzustufen wäre.
Importierte Tools sind aus und nur für Administratoren
Ein Import legt Katalogzeilen an, er gewährt nichts. Jedes Tool startet deaktiviert und wird im Modul Tools eingeschaltet, wie ein eingebautes. McpTool::requiresAdmin() ist immer true, deshalb verweigert ToolCallPolicy das Tool einem Backend-Nutzer ohne Adminrechte und einem Lauf ganz ohne Nutzer.
Jedes MCP-Tool ist ein Schreibzugriff, der vorher fragt
getEffect() liefert für jedes importierte Tool NON_IDEMPOTENT_WRITE, auch für eine reine Suche; ein Aufruf wird bei einem Retry daher nie wiederholt. requires_approval steht standardmäßig auf 1, und McpServerRecord::approvalRequired() wertet alles außer einer ausdrücklichen 0 als „Freigabe nötig“. Den Haken pro Server erst entfernen, wenn klar ist, was dessen Tools tun.
Das Budget ist geteilt – auch über Katalogseiten
Ein Handshake, der länger als 5 Sekunden dauert, lässt dem Arbeits-Request weniger als 15 Sekunden, und ein großer Katalog durchläuft alle Seiten unter einem Budget. Beides kann jetzt abgelehnt werden. Abhilfe schafft ein höheres mcpOperationTimeout. Ein Tool-Aufruf aus dem Backend läuft synchron im AJAX-Request; ein Budget über dem Limit von PHP oder Proxy endet daher in einem 500 oder 504 statt in einem fehlgeschlagenen Tool-Ergebnis (ADR-170).
Lockere Server werden abgelehnt, nicht geduldet
Eine Antwort, die die ID eines anderen Requests zurückgibt, jsonrpc 2.0 auslässt, ein Array als result liefert, result und error zugleich enthält oder zwei Antworten in einen Event-Stream legt, gilt als fehlerhaft und wird abgelehnt. Ebenso ein Body über 2 MiB. Eine abgelehnte Antwort speichert keinen Kontakt und lässt den gespeicherten Katalog unverändert.