Zum Inhalt springen

Tool Discovery für 4.800 MCP-Tools — ohne Vektordatenbank

Auf unserer ToolMesh-Produktivinstanz lieferte eine discover_tools-Anfrage nach allem, was mit NetBox zu tun hat, rund 320 KB TypeScript-Deklarationen. Das sind etwa 80.000 Tokens — für viele Modelle das halbe Kontextfenster, verbraucht bevor der Agent ein einziges Tool aufgerufen hat. Die Anfrage nach allen Tools hätte eine Ausgabe in der Größenordnung von 2 MB erzeugt.

Das hat niemand so entworfen. Es ist so gewachsen.

ToolMesh ist ein self-hosted MCP-Gateway, das Code Mode spricht: Backends werden als flache JavaScript-Funktionen exponiert (toolmesh.netbox_list_devices(...)), und ein discover_tools-Meta-Tool liefert TypeScript-Signaturen, damit das Modell korrekte Aufrufe schreiben kann. Mit einer Handvoll Backends ist es genau richtig, jede passende Signatur vollständig zurückzugeben.

Unsere Instanz ist längst keine Handvoll mehr. Sie betreibt 49 Backends mit 4.767 Tools: NetBox allein steuert 608 bei, zwei MikroTik-Switches je 297, zwei OPNsense-Firewalls je 292, GitHub 203, Cloudflare 200. Eine vollständige Signatur liegt im Schnitt bei ~530 Bytes — NetBox-List-Endpoints tragen ein Dutzend und mehr Filterparameter.

Drei Effekte verstärken sich auf dieser Skala gegenseitig:

  • Volle Deklarationen sind der einzige Ausgabemodus. Jeder Treffer wird mit kompletter Parameterliste gerendert.
  • Das Such-Regex matcht Namen und Beschreibungen. Ein Pattern wie device fächert gleichzeitig über NetBox, UniFi, MikroTik und Shelly auf.
  • Multi-Instanz-Backends teilen sich Prefixe. Die Suche nach mikrotik liefert beide Switches — 594 Deklarationen.

Eine völlig vernünftige Agenten-Anfrage — „zeig mir die NetBox-Tools” — sprengt damit das Kontextfenster.

Die reflexhafte Antwort des Jahres 2026 lautet: Tool-Beschreibungen embedden, in eine Vektordatenbank legen, semantisch suchen. Wir betreiben dieses Muster an anderer Stelle, und dort verdient es sein Geld.

Für dieses Problem haben wir bewusst Nein gesagt — aus drei Gründen.

Der Korpus ist winzig und statisch. ~4.800 kurze, gut benannte, gut beschriebene Dokumente, die sich nur bei einem Config-Reload ändern. Das ist ein In-Memory-Index, der in Millisekunden neu aufgebaut ist — kein Persistenzproblem. PostgreSQL/pgvector in ein Single-Binary-Go-Gateway zu holen, um fünftausend Strings zu durchsuchen, ist Infrastruktur außer jedem Verhältnis. Selbst der schlanke Weg — ein eingebettetes ONNX-Modell — schleppt eine native Runtime-Bibliothek mit, ein realer Packaging-Schmerz für Software, die andere selbst deployen.

Das Vokabular ist bereits ausgerichtet. Tool-Namen und -Beschreibungen sind für LLM-Konsum geschrieben. Lexikalisches Ranking hat exzellentes Material.

Vor allem aber: Der Agent ist selbst die semantische Schicht. Ein LLM, das eine kompakte, gerankte Trefferliste mit einer ehrlichen Statuszeile sieht, formuliert seine Anfrage in einem Zug selbst um. Es braucht keine perfekte Suche. Es braucht eine billige Iterationsschleife und Feedback darüber, was es gerade sieht.

Drei Änderungen, alle in ToolMesh v0.3.1+, null neue Dependencies.

discover_tools skaliert seinen Detailgrad jetzt mit der Treffermenge: bis 25 Treffer kommen volle TypeScript-Signaturen, bis 250 Einzeiler-Zusammenfassungen, bis 2.000 nur Namen, darüber Zählungen pro Backend. Ein detail-Parameter übersteuert die Automatik, und ein harter 50-KB-Cap sichert jede Stufe ab.

Der frühere 2-MB-Worst-Case — gar kein Filter — antwortet jetzt mit etwa 1,5 KB:

// ToolMesh tools — per-backend overview
rest:netbox — 608 tools
rest:mikrotik-sw-lii-labor-perlman — 297 tools
rest:mikrotik-sw-mikrotik-10g — 297 tools
rest:opnsense_backup — 292 tools
...
// ── 4767 of 4767 tools matched — detail: overview (auto).
// Refine: narrower pattern, query:"<free text>" for ranked results,
// detail:"full"|"summary"|"names"|"overview", limit:N.

Dieser Footer ist die wichtigste Zeile des ganzen Features. Jede Antwort sagt, wie viel vom Katalog gematcht hat, wie viel gezeigt wird und wie man verfeinert. Der Agent verwechselt eine gekürzte Sicht nie mit der ganzen Welt — und korrigiert sich im nächsten Aufruf selbst, statt zu raten.

2. Geranktes Freitext-Suchen — BM25 in ~200 Zeilen Go

Abschnitt betitelt „2. Geranktes Freitext-Suchen — BM25 in ~200 Zeilen Go“

Regex-Patterns sind großartig für exakte Lookups (^netbox_list) und schlecht für Exploration. Der neue query-Parameter macht geranktes Freitext-Suchen:

discover_tools({ query: "create dns record for a zone" })

liefert die Top 25 nach Relevanz — Cloudflare-, INWX- und Linode-DNS-Tools in gemischter Reihenfolge, ohne NetBox-Rauschen. Darunter liegt ein schlichter BM25-Index über Tool-Namen (geboostet), Beschreibungen und Parameternamen — etwa 200 Zeilen dependency-freies Go, pro Aufruf in Mikrosekunden neu aufgebaut. Weil Parameternamen mitindexiert sind, findet eine Query wie rack_id das Tool netbox_list_devices, obwohl „rack” im Tool-Namen nirgends vorkommt.

BM25 ist fünfzig Jahre alte Information-Retrieval-Technik. Das ist keine Schwäche — bei dieser Korpusgröße ist es genau der Punkt.

Der größte strukturelle Gewinn: Agenten brauchen überhaupt keinen separaten Discovery-Roundtrip mehr. Innerhalb von execute_code stehen zwei lokale Helfer bereit:

// find → inspect → call, in ONE round trip
const hits = toolmesh.discover("move task to kanban bucket", 5);
const schema = toolmesh.describe(hits[0].name);
return await toolmesh[hits[0].name]({
project_id: 2, view_id: 8, task_id: 200, bucket_id: 5
});

toolmesh.discover() und toolmesh.describe() laufen gegen den In-Memory-Index — kein Backend-Call, keine Kontextkosten über das hinaus, was der Code explizit zurückgibt, ausgenommen vom Call-Budget pro Ausführung und gefiltert nach der Autorisierung des Aufrufers wie jede andere Oberfläche auch. Discovery wird etwas, das der Code tut — nicht etwas, das der Kontext bezahlt.

Wir haben den Branch produktiv deployt und aus einer echten Agenten-Session heraus getestet. Zwei Bugs zeigten sich fast sofort — beide für Unit-Tests unsichtbar:

Ein veraltetes Client-Schema machte aus limit einen String. Der MCP-Client hatte das alte discover_tools-Schema gecacht und serialisierte den unbekannten Parameter limit: 5 als String "5" — den der Server stillschweigend ignorierte. Clients, die man nicht kontrolliert, halten veraltete Schemas; agentenseitige Parameter akzeptieren jetzt auch numerische Strings.

Zusammenfassungen brachen bei „e.g.” ab. Der Einzeiler-Summarizer schnitt am ersten Punkt-plus-Leerzeichen und produzierte Set device key properties (e.g. — ein Punkt beendet einen Satz jetzt nur noch, wenn ein Großbuchstabe folgt.

Beides ist unglamourös. Beides ist der Unterschied zwischen einem Feature, das gut demonstriert, und einem, das den Kontakt mit echten Clients überlebt.

Progressive Disclosure für Tools ist nicht unsere Erfindung. Anthropics Beitrag zu Code Execution mit MCP beschreibt das bedarfsweise Laden von Tool-Definitionen; Claude Code lädt Tool-Schemas erst bei Bedarf; Cloudflares Code Mode hat das Muster „Code gegen Tools schreiben” populär gemacht. Das Pattern konvergiert zum Konsens.

Was der Produktionsmaßstab hinzufügt, sind die Teile, die in Demos selten vorkommen: Ranking (weil 764 Kandidaten irgendwo auf „dns” matchen), eine immer präsente Feedback-Zeile (weil Agenten gut verfeinern — aber nur, wenn man ihnen sagt, was sie gesehen haben), harte Output-Caps (weil irgendein Client immer nach allem fragt) und Discovery in der Sandbox (weil der billigste Kontext der ist, der nie ausgegeben wird).

Derselbe Index bekommt zwei weitere Konsumenten: einen Capability Index für verschachtelte Code-Mode-Backends — MCP-Server, die selbst nur search/execute exponieren und deren Fähigkeiten ToolMesh bei der Registrierung sondiert und indexiert — und daraus generierte synthetische Tool-Beschreibungen auf MCP-Ebene. Und falls reale Vokabular-Lücken es je rechtfertigen, lässt sich ein optionaler Embedding-Kanal hinter derselben query-API nachrüsten — erst messen, dann bauen.

Die Implementierung ist Open Source (PR #80, Apache 2.0). Die Tool-Definitionen, die sie durchsucht, kommen aus DADL — eine YAML-Datei pro API, über 25 fertige in der Registry.

Wenn du ein MCP-Setup betreibst, das seiner Tool-Liste entwachsen ist: Wir würden wirklich gern hören, wie Discovery auf deiner Skala aussieht — GitHub Discussions oder [email protected].