Suche, der dein Agent vertrauen kann: searxng-mcp-server
Diese Woche habe ich searxng-mcp-server auf npm veröffentlicht. Der MCP-Server verbindet KI-Coding-Clients mit einer selbst gehosteten SearXNG-Instanz: Web-, Bild-, Nachrichten-, Video- und Musiksuche, dazu Seitenabruf als sauberes Markdown. API-Keys braucht der Betrieb keine, und die Anfragen verlassen die eigene Infrastruktur nicht. Das ist der Baubericht, einschließlich der Teile, die einen Abend gekostet haben.
Warum braucht ein Agent eine eigene Suche?#
Die meisten Such-MCP-Server wrappen eine bezahlte API. Du bringst einen Vendor-Key mit, der Agent ruft das Tool auf, und der Anbieter berechnet den Request und protokolliert die Anfrage. Für einen Menschen ist ein bezahlter Lookup nichts. Ein Agent, der zwanzig Minuten lang ein Problem recherchiert, feuert Dutzende davon ab, und jede Anfrage landet in den Logs eines Anbieters, direkt neben einer Konto-ID. Das Tool sieht deine Arbeit in Echtzeit.
SearXNG deckt die andere Hälfte. Die Metasuche läuft auf eigener Hardware, aggregiert Ergebnisse von bis zu 260 Suchdiensten, und die Dokumentation trägt den Slogan „Search without being tracked.“ samt der Zusage, Nutzer „are neither tracked nor profiled“ (SearXNG-Doku). Ein Docker-Container, eine URL, null Keys. Es fehlte die MCP-Schicht zwischen Instanz und Editor. MCP beschreibt sich selbst als „an open-source standard for connecting AI applications to external systems“, und ein stdio-Server ist die kleinste solche Schicht, die es gibt.
Der Server registriert sieben Tools, jedes mit einem Zod-Schema; die Such-Tools teilen sich eine harte Ergebnisgrenze: max_results steht standardmäßig auf 10 und endet bei 50.
- search: Webtreffer mit Titel, URL und Snippet.
- fetch_content: den Artikelkörper einer Seite, konvertiert zu Markdown.
- image_search, news_search, video_search, music_search: dieselbe Ergebnisform, je eine Kategorie.
- list_engines: die auf der verbundenen Instanz aktivierten Engines und Kategorien, damit eine Suche das anfragt, was es dort tatsächlich gibt.
stdio bleibt der Transport in der Box: Der Client startet den Prozess, es gibt kein Netzwerk und keine Auth-Fläche. Seit 0.3.0 deckt ein opt-in Streamable-HTTP-Endpunkt den entfernten Fall ab, einen Server für viele Clients. Er spricht nur die aktuelle Spezifikationsrevision, bleibt pro Request zustandslos, authentifiziert per Bearer-Token mit timing-sicherem Vergleich, prüft Host und Origin und verweigert einen unauthentifizierten Start auf einer Nicht-localhost-Adresse. Geteilte Team-Umgebungen sind der neueste Grund für diese Form: JetBrains Air verspricht MCP-Verbindungen, die einmal konfiguriert einem ganzen Team samt Agenten dienen.
Wie eine Seite zum Kontext wird#
Das interessante Tool ist fetch_content. Rothes HTML ist ein schlechter Payload für ein Kontextfenster: Navigation, Skripte und Cookie-Banner um den eigentlichen Text, alles in Tokens abgerechnet. Die Pipeline ist Readability für die Artikelextraktion, linkedom zum Parsen ohne Browser, turndown für die Markdown-Konvertierung. Im Modell landet lesbarer Text mit intakter Struktur.
Die Eingabe ist eine URL, und ihre Herkunft wechselt: der Nutzer, eine Datei, die der Agent gelesen hat, oder eine Webseite, die er vorher abgerufen hat. Damit ist fetch_content das Tool, das ein Angreifer am liebsten hätte. Zwei Fehlerklassen gehören zum Handwerk.
Die erste ist SSRF, die Klasse, in der ein Angreifer deine Maschine ein Ziel seiner Wahl abrufen lässt; die OWASP-Zusammenfassung lautet, er könne „abuse functionality on the server to read or update internal resources“ (OWASP). Eine URL wie http://169.254.169.254/latest/meta-data/ ist im Browser harmlos und aus dem Netzwerk, in dem der Agent läuft, bedeutungsvoll. Die zweite kommt per Redirect: Du validierst https://example.com, es antwortet mit 302, und der Hop landet auf localhost. Eine Validierung zur Request-Zeit prüft nur die erste URL. searxng-mcp-server wiederholt deshalb die Prüfung auf private Adressbereiche bei jedem Redirect-Hop; eine Kette, die öffentlich beginnt und intern endet, wird mitten im Ablauf abgelehnt.
Seiteninhalt ist ein dritter, leiserer Kanal: Alles, was fetch_content zurückgibt, landet im Kontext des Modells, und dort verhält sich Text wie eine Anweisung. Jede abgerufene Seite kommt gewrappt und mit dem Marker UNTRUSTED_WEB_CONTENT, der Client behandelt den Payload zuerst als Daten. Die Mechanik bekommt einen eigenen Artikel: was dein Coding-Agent liest, wenn er eine Seite abruft.
Was ein npm-Release heute bedeutet#
Das Paket liegt auf npm, und das Veröffentlichen dort ist eine Supply-Chain-Übung geworden. Releases laufen über GitHub Actions mit OIDC Trusted Publishing: Ein Tag löst npm publish --provenance aus, das Attestationen anhängt, die das Artefakt an Repository und Workflow binden, die es gebaut haben (npm-Doku). In keinem Secret Store liegt ein npm-Token.
Die Qualitätstore laufen, bevor irgendetwas das Gerät verlässt: prepublishOnly führt Lint, Typecheck, Tests und Build aus, und die CI erzwingt eine Zeilenabdeckung von 95 Prozent auf einer Baseline, die nur nach oben rutscht. Renovate bewegt Abhängigkeiten nach den Supply-Chain-Presets: GitHub Actions per Digest gepinnt, npm-Pakete zurückgehalten, bis sie ein Mindestalter erreichen. Ein Transporttest fährt einen vollständigen tools/call-Roundtrip über einen In-Memory-Transport und prüft unterwegs den Untrusted-Marker. Der Changelog reist im npm-Tarball mit, direkt neben dem Code, den er beschreibt.
Zwei Registry-Lektionen haben einen Abend gekostet. Die MCP-Registries (server.json für das offizielle Verzeichnis, dazu smithery.yaml und glama.json für die Listing-Dienste) validieren Metadaten jeweils anders, und eine davon erzwingt eine 100-Zeichen-Grenze für das Beschreibungsfeld. Die zweite Lektion: Ein Paketname auf npm gehört dir nie ganz. Die Registry zeigt eine Version 1.0.0 von searxng-mcp-server mit Zeitstempel April 2026, Monate bevor dieses Projekt existierte; jemand anders hat sie veröffentlicht und entfernt. Diese Nummer ist für immer verbrannt, die Roadmap springt deshalb von 0.1.x auf 1.0.1.
Wo die eigentliche Arbeit lag#
Nicht im Plumbing. Einen stdio-Server an SearXNGs JSON-API anzubinden kostet ein Wochenende gewöhnliches TypeScript; das Repository läuft auf Node 22.19 oder neuer und hat über das Protocol-SDK und die Parsing-Kette hinaus keine Runtime-Abhängigkeiten. Die eigentliche Arbeit saß an den Grenzen: welche URL abgerufen werden darf, was bei Hop drei einer Redirect-Kette passiert, wie eine Seite markiert wird, damit ein Modell sie als Material behandelt und nicht als Befehle. Grenzarbeit ist unsichtbar, wenn sie funktioniert, und teuer, wenn sie fehlt.
Dieselbe Struktur zieht sich durch diese Seite: Der Support-Agent validiert Tool-Ergebnisse mit Zod, bevor sie zurück ins Modell gehen, und die Misalignment-Reports von OpenAI lesen sich wie ein langes Plädoyer, jeden Kanal in einen Kontext als nicht vertrauenswürdige Eingabe zu behandeln. Die Suche war die letzte ungeschützte Fläche in meiner eigenen Toolchain.
Was das Repository zeigt#
Alles oben ist prüfbar. Das GitHub-Repository trägt die Coverage-Schwellen in vitest.config.ts, den Release-Workflow mit seinen OIDC-Berechtigungen, die Renovate-Presets und ein Design-Dokument, das die Schutzmaßnahmen erklärt. Die Case Study auf dieser Seite bündelt die Architektur: searxng-mcp-server. Wer eine eigene SearXNG-Instanz betreibt, ist eine Zeile entfernt: npx -y searxng-mcp-server mit SEARXNG_URL auf die eigene Instanz.
Die Suche ist eine Hälfte der Agenten-Infrastruktur. Die andere Hälfte ist die Frage, wie die Aufgabe selbst gefahren wird. Ich vergleiche Ziele und Workflows über Claude Code, Codex und ZCode in einem eigenen Beitrag.