Zum Inhalt springen

Unit-Backends: ein governtes Tool über viele private Backends

Ein Unit-Backend bündelt ein oder mehrere private Sub-Backends (MCP oder REST) hinter einem einzigen JavaScript-Glue-Modul und exponiert nach außen nur eigene, kuratierte Tools. Die Sub-Backends tauchen nie im Tool-Manifest auf — das Modell sieht eine Handvoll zweckgebauter Tools, während die Unit die Mehrschritt-Arbeit, die Komposition und die Antwort-Verdichtung innerhalb von ToolMesh erledigt.

ToolMesh hat zwei Kompositions-Mechanismen, und sie sind nicht dasselbe:

KomponiertLebt in
Composite-Toolandere Tools desselben Backendseiner DADL-Datei
Unit-Backendandere Backends (MCP oder REST)einer unit.yaml + einem JS-Modul

Eine Unit ist die richtige Wahl, wenn ein modellseitiger Aufruf mehrere Backend-Aufrufe orchestrieren, deren Ergebnisse zusammenführen oder eine geschwätzige API hinter einer kompakten Antwort verbergen soll.

Wrapping lohnt nur, wenn die Unit etwas tut, das die Roh-Tools plus das Modell nicht günstig hinbekommen. Drei Gewinne treten immer wieder auf — und alle drei greifen schon bei einem einzigen Sub-Backend:

  1. Mehrschritt-Kapselung. Ein sauberes Tool, das eine zwingende „Erst-entdecken-dann-abfragen”-Kette verbirgt.
  2. Tool-übergreifende Komposition. Ein Digest, aus mehreren Aufrufen zusammengesetzt.
  3. Antwort-Verdichtung. Große, verschachtelte oder tabellarische Payloads auf die wenigen relevanten Felder reduziert — echte Einsparung im Kontextfenster.

Das Beispiel unten stützt sich genau darauf.

Eine Unit ist ein Verzeichnis unter TOOLMESH_UNITS_DIR (Default /app/config/units), das als direktes Unterverzeichnis gescannt wird:

config/units/netdata/unit.yaml
unit: netdata
implementation: ./netdata.js
expose:
meta_signals: [netdata] # which _meta.* branches the Output Gate may pass through
audit: full # full | compact | none
tools: [health_summary] # optional: promote these to direct top-level MCP tools
backends: # private dependencies — same syntax as backends.yaml
- name: netdata
transport: http
url: http://your-parent:19999/mcp
api_key_env: netdata_mcp_api_key # injected as Authorization: Bearer <key>

Der unit.yaml-Root muss ein Mapping sein (unit: / implementation: / expose: / backends:). Nur die Einträge unter backends: sind eine Liste.

expose.tools ist optional und spiegelt expose_tools aus backends.yaml: Die gelisteten describe()-Tool-Namen werden zusätzlich als direkte Top-Level-MCP-Tools promotet. Promotet wird immer unter dem vollen <unit>_<tool>-Namen, nie als nackter Alias — Unit-Tool-Namen wie search oder roll sind für die Root-Ebene zu generisch. Erreichbar bleibt jedes Unit-Tool ohnehin über discover_tools und execute_code; ein Eintrag ohne passendes describe()-Tool wird beim Laden einmal geloggt und übersprungen.

Die Implementierung ist ein JavaScript-Modul mit einer top-level describe()-Funktion (die Tool-Oberfläche) plus einer Funktion je deklariertem Tool. Intern sind private Sub-Backends als api.<name>.<tool>(args) erreichbar:

function describe() {
return {
tools: [{
name: "health_summary",
access: "read",
description: "Connected nodes plus all raised alerts, in one digest."
}]
};
}
async function health_summary() {
const nodes = unwrap(await api.netdata.list_nodes({}));
const alerts = unwrap(await api.netdata.list_raised_alerts({ cardinality_limit: 200 }));
// ...parse, merge, trim...
return { content: [{ type: "text", text: JSON.stringify(/* compact digest */) }],
_meta: { netdata: { tool: "health_summary" } } };
}

Der Glue läuft pro Aufruf in einer abgeschotteten goja-Runtime:

  • Erlaubt: api.<sub>.<tool>(...), das params-Objekt, reines JS (Math, Date, JSON, await).
  • Verboten: fetch, require, import, fs, process, eval, Timer.
  • Limits: Aufruftiefe 16, max. 20.000 api.*-Aufrufe pro Invocation (Runaway-Bremse).

Wissenswerte Falle. Innerhalb der Sandbox liefert api.<sub>.<tool>() den rohen MCP-ToolResult{ content: [{ type: "text", text: "<json>" }], isError, metadata } — nicht das geparste Payload. content[0].text selbst parsen (der unwrap()-Helfer oben). Das execute_code-Meta-Tool packt automatisch aus, derselbe Code kann also einen Inline-Test bestehen und in der Unit trotzdem leer zurückkommen. Unit-Logik daher immer durch die geladene Unit verifizieren.

Netdatas Monitoring-Agent bringt einen eingebauten MCP-Server mit (kostenlos, Open Source, http://HOST:19999/mcp auf jedem Agent oder Parent — kein Netdata-Cloud-Konto nötig). Er exponiert ~13 mächtige, aber low-level Tools. Wir registrieren diesen MCP als privates Sub-Backend und exponieren eine kleine, kuratierte Oberfläche:

Unit-ToolWas es tutRoh-Entsprechung
metricPer-Gruppe-Summary (avg/min/max/anomaly%) für beliebige Contextget_metrics_detailsquery_metrics + verschachteltes Objekt parsen
compare_nodesA↔B oder jetzt↔vorhin: Delta/Ratio für eine Contextdieselbe Zwei-Call-Kette, zweimal
health_summaryNodes + offene Alerts in einem Digestlist_nodes + list_raised_alerts + zwei Tabellen parsen
anomalies_nowML-markierte Anomalien, gerankt & getrimmtfind_anomalous_metrics + 11-Spalten-Tabelle parsen
what_changedMetriken, die sich ggü. einem Baseline-Fenster verschoben habenfind_correlated_metrics mit von Hand gesetzten Baseline-Fenstern

Abfrage von system.cpu über fünf Nodes:

  • Roh-query_metrics liefert ein verschachteltes summary / result / view / db-Objekt — ~7,9 KB — und verlangt vorher explizite Dimensionen (es sind also faktisch zwei Calls), plus eine stille Warnung, wenn man Dimensionen mittelt.
  • metric entdeckt die Dimensionen automatisch, wählt eine sinnvolle Aggregation und liefert ~0,8 KB:
{
"context": "system.cpu",
"units": "Total CPU utilization",
"grouped_by": ["node"],
"groups": [
{ "name": "fermat", "avg": 4.25, "max": 4.25, "anomaly_rate_pct": 0 },
{ "name": "dumfries", "avg": 1.40 },
{ "name": "nomad-node-1", "avg": 1.20 },
{ "name": "nomad-node-0", "avg": 1.07 },
{ "name": "picard", "avg": 0.29 }
]
}

Das ist eine ~90 % kleinere Antwort, der Zwei-Call-Workflow auf einen reduziert, und die Aggregations-Falle für das Modell abgefangen. health_summary zeigt den Kompositions-Gewinn — zwei Tabellen-Dumps werden ein Digest — und anomalies_now den Verdichtungs-Gewinn: eine 11-Spalten-Tabelle über zehntausende analysierte Zeitreihen wird eine kurze, gerankte Liste.

Man kann denselben Netdata-MCP zweimal registrieren: einmal als Unit (netdata, kuratiert) und einmal als Roh-Passthrough (netdata_raw, der volle Werkzeugkasten für ad-hoc). Das Modell sieht dann sowohl netdata_* als auch netdata_raw_*.

Weil ein Backend-Name (netdata) ein Präfix des anderen ist (netdata_raw), routet ToolMesh nach dem längsten passenden Präfixnetdata_raw_query_metrics erreicht immer das Passthrough, netdata_metric immer die Unit. Im Zweifel kollisionsfreie Namen wählen.

Eine Unit ist ein Backend wie jedes andere, also greift die volle Pipeline:

  • Access-Klassifikation pro Tool über describe() (read / write / admin / dangerous) — sensible Aufrufe (Live-Prozesse, Logs) als dangerous kapseln.
  • _meta-Signale, die die Unit emittiert, werden über expose.meta_signals gegatet und vom Output Gate konsumiert.
  • Audit protokolliert jeden api.*-Aufruf mit der Unit als Eltern-Kontext.
  • OpenFGA-Berechtigungen werden auf die Unit vergeben; das autorisiert transitiv ihre privaten Abhängigkeiten.

Dieses Beispiel bleibt vollständig auf Netdatas Open-Source-Agent (GPLv3): der eingebaute MCP, die ML-/Anomalie-Erkennung auf dem Agent und die Multi-Node-Aggregation eines Parent funktionieren ohne Netdata Cloud. Die Unit baut ihre eigene Analyse-Schicht auf dieser Daten-API — sie berührt oder repliziert nicht die separat lizenzierte Netdata-Cloud-UI. Die Kopplung über MCP/HTTP auf Distanz hält ToolMesh (Apache-2.0) sauber, da der Agent GPL ist, nicht AGPL.