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.
Unit-Backend vs. Composite-Tool
Abschnitt betitelt „Unit-Backend vs. Composite-Tool“ToolMesh hat zwei Kompositions-Mechanismen, und sie sind nicht dasselbe:
| Komponiert | Lebt in | |
|---|---|---|
| Composite-Tool | andere Tools desselben Backends | einer DADL-Datei |
| Unit-Backend | andere 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.
Wann sich eine Unit lohnt
Abschnitt betitelt „Wann sich eine Unit lohnt“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:
- Mehrschritt-Kapselung. Ein sauberes Tool, das eine zwingende „Erst-entdecken-dann-abfragen”-Kette verbirgt.
- Tool-übergreifende Komposition. Ein Digest, aus mehreren Aufrufen zusammengesetzt.
- 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:
unit: netdataimplementation: ./netdata.jsexpose: 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 toolsbackends: # 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 unterbackends: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" } } };}Sandbox
Abschnitt betitelt „Sandbox“Der Glue läuft pro Aufruf in einer abgeschotteten goja-Runtime:
- Erlaubt:
api.<sub>.<tool>(...), dasparams-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].textselbst parsen (derunwrap()-Helfer oben). Dasexecute_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.
Durchgängiges Beispiel: Netdata
Abschnitt betitelt „Durchgängiges Beispiel: Netdata“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-Tool | Was es tut | Roh-Entsprechung |
|---|---|---|
metric | Per-Gruppe-Summary (avg/min/max/anomaly%) für beliebige Context | get_metrics_details → query_metrics + verschachteltes Objekt parsen |
compare_nodes | A↔B oder jetzt↔vorhin: Delta/Ratio für eine Context | dieselbe Zwei-Call-Kette, zweimal |
health_summary | Nodes + offene Alerts in einem Digest | list_nodes + list_raised_alerts + zwei Tabellen parsen |
anomalies_now | ML-markierte Anomalien, gerankt & getrimmt | find_anomalous_metrics + 11-Spalten-Tabelle parsen |
what_changed | Metriken, die sich ggü. einem Baseline-Fenster verschoben haben | find_correlated_metrics mit von Hand gesetzten Baseline-Fenstern |
Der Gewinn, gemessen
Abschnitt betitelt „Der Gewinn, gemessen“Abfrage von system.cpu über fünf Nodes:
- Roh-
query_metricsliefert ein verschachteltessummary/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. metricentdeckt 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.
Roh und kuratiert nebeneinander
Abschnitt betitelt „Roh und kuratiert nebeneinander“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äfix — netdata_raw_query_metrics erreicht immer das Passthrough, netdata_metric immer die Unit. Im Zweifel kollisionsfreie Namen wählen.
Governance
Abschnitt betitelt „Governance“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) alsdangerouskapseln. _meta-Signale, die die Unit emittiert, werden überexpose.meta_signalsgegatet 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.
Hinweis zur Lizenzierung
Abschnitt betitelt „Hinweis zur Lizenzierung“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.