Skip to main content
Glama

research-mcp

Eine zustandslose MCP-Fassade, die eine Pyramide von Such-/Lese-Anbietern hinter einem einzigen streamable-http-MCP-Endpunkt verbirgt und nur 3 saubere Tools mit guten russischen Hilfetexten bereitstellt. Eine LLM erhält ein einfaches „Suche → Lesen“-Werkzeugset; dahinter werden mehrere Anbieter automatisch ausprobiert, zusammengeführt und über Failover umgeschaltet.

Die App führt keine Authentifizierung durch – sie wird über Traefik + basicAuth auf dem Host veröffentlicht. Sie hält keinen Anwendungszustand: Das Einzige, was persistiert wird, ist eine Logdatei unter data/ (auf einem Volume gehalten).

Tools

Tool

Was es tut

web_search(query, num_results=8, page=1, language=None)

Durchsucht alle aktivierten Anbieter, führt zusammen + dedupliziert → sortierte Liste (Titel, URL, Ausschnitt). Nur Suche.

read_page(url)

Eine Seite oder PDF → sauberes Markdown. Erkennt den Typ automatisch, durchläuft die Lese-Pipeline (leicht → schwer), bis eine erfolgreich ist.

read_pages(urls)

Bis zu 20 URLs gleichzeitig → Liste von {url, ok, markdown|error}.

Related MCP server: Web Search MCP

Architektur: Typen + Instanzen

Anbieter sind Plugins. Wir unterscheiden:

  • Typ – eine Implementierungsklasse (z. B. der searxng-Suchanbieter), eine pro Modul in src/providers/, registriert mit @register("type").

  • Instanz – eine konfigurierte Kopie eines Typs mit Geheimnissen/URL, aufgelöst aus benannten Umgebungsvariablen (mehrere Instanzen eines Typs sind erlaubt, z. B. tavily-1 / tavily-2 mit verschiedenen Schlüsseln).

Welche Instanzen existieren und die Reihenfolge, in der jede Pipeline sie ausprobiert, wird im Code konfiguriert (src/pipeline_config.py); Schlüssel/URLs kommen aus der ENV per Variablennamen.

  • Such-Pipeline (searxng → brave → jina-search → serper → exa): aktivierte Instanzen laufen gleichzeitig; Ergebnisse werden zusammengeführt und nach normalisierter URL dedupliziert (frühere Pipeline-Position gewinnt). Wenn JINA_API_KEY gesetzt ist (und SEARCH_RERANK_ENABLED nicht deaktiviert ist), wird die gesamte zusammengeführte Liste dann von jina-reranker-v3.5 neu bewertet, sodass der Zuschnitt auf num_results die relevantesten Treffer behält statt eines blinden Präfixes in Pipeline-Reihenfolge; ein Fehler beim Re-Ranking fällt auf die Zusammenführungsreihenfolge zurück. searxng und brave drosseln sich zusätzlich lokal (eine Abfrage pro 45s bzw. pro 1,1s, entsprechend einem gemessenen Upstream-Limit); wenn der Slot belegt ist, überspringen sie die aktuelle Suche, statt darauf zu warten.

  • Lese-Pipeline (trafilatura → jina → crawl4ai → tavily-1 → tavily-2 → firecrawl): ein einzelner Probe-GET klassifiziert die URL. PDFs (Content-Type / .pdf / %PDF-Magic) werden mit pypdf extrahiert; für HTML wird derselbe Body an trafilatura übergeben, sodass der heiße Pfad nie zweimal GET ausführt, dann werden die restlichen Instanzen der Reihe nach ausprobiert und die erste, die Inhalt >= FALLBACK_MIN_CHARS zurückgibt, gewinnt.

Querschnittlich: ein einmaliger transienter Retry (5xx / Transportfehler) mit kurzem Backoff; 402 (kein Guthaben) / 429 (Rate-Limit) werden als Anbieterfehler behandelt → nächste Instanz (das ermöglicht das Failover von tavily-1 → tavily-2).

Eine Instanz ist nur dann aktiviert, wenn ihre erforderlichen Umgebungsvariablen gesetzt sind; andernfalls wird sie mit einer Logzeile übersprungen. trafilatura benötigt keine Konfiguration (immer an); jina funktioniert ohne Schlüssel (sein Schlüssel ist optional). Beim Start verlangt der Server mindestens eine Such- und eine Lese-Instanz, sonst beendet er sich mit einer klaren Meldung.

Einen Anbieter hinzufügen

  1. Schreibe src/providers/<type>.py mit einer Klasse, die mit @register("<type>") dekoriert ist und SearchProvider.search(...) oder ReadProvider.read(...) implementiert.

  2. Importiere das Modul in src/providers/__init__.py (damit der Dekorator ausgeführt wird).

  3. Füge eine Zeile Instance("name", "<type>", api_key_env="YOUR_ENV_NAME") in src/pipeline_config.py hinzu und referenziere ihren name in SEARCH_PIPELINE / READ_PIPELINE. Verwende den ENV-Variablennamen, niemals einen Wert.

  4. Dokumentiere die Umgebungsvariable in .env.example.

Schnellstart

make install                # create .venv + install dev/test deps
cp .env.example .env        # fill in the keys you have  (shortcut: make env)
make test                   # run tests
make run                    # run the server (streamable-http on MCP_HOST:MCP_PORT, endpoint /mcp)

Konfiguration

Die gesamte Konfiguration stammt aus ENV / .env (siehe .env.example). Anbieter-Geheimnisse/URLs werden im Instanz-Loader per Name gelesen, nicht als Settings-Felder deklariert. Die nicht geheimen Stellschrauben (alle mit Standardwerten): MCP_HOST, MCP_PORT, LOG_LEVEL, LOG_FILE, LOG_ROTATION, LOG_RETENTION, REQUEST_TIMEOUT, FALLBACK_MIN_CHARS, READ_PAGES_CONCURRENCY, RETRIES, SEARCH_RERANK_ENABLED, JINA_TOKEN_BUDGET. Das URL-Limit pro Aufruf von read_pages ist fest 20 (harte Konstante, passend zur Tool-Beschreibung) – nicht konfigurierbar.

Umgebungsvariablen der Anbieter: SEARXNG_URL, BRAVE_API_KEY, SERPER_API_KEY, EXA_API_KEY, JINA_API_KEY (ein Schlüssel aktiviert den jina-Reader im Schlüsselmodus, den jina-search-Anbieter und den Such-Reranker; der Reader allein funktioniert auch ohne Schlüssel), CRAWL4AI_URL + CRAWL4AI_TOKEN, TAVILY_1_API_KEY, TAVILY_2_API_KEY, FIRECRAWL_API_KEY.

Proxy

Jede externe Instanz kann über einen eigenen SOCKS5/HTTP-Proxy geroutet werden, indem <INSTANCE>_PROXY gesetzt wird – nützlich für sauberen Egress an IP-basierten Sperren vorbei (z. B. Cloudflare vor Exa). Unterstützt pro Instanz: EXA_PROXY, BRAVE_PROXY, SERPER_PROXY, JINA_PROXY, TAVILY_1_PROXY, TAVILY_2_PROXY, FIRECRAWL_PROXY. Interne Instanzen (searxng, crawl4ai, trafilatura) haben keinen Proxy.

Der Wert wird direkt an httpx übergeben; socks5://host:port macht Proxy-seitiges DNS (der Zielhostname wird vom Proxy aufgelöst, wie bei curl --socks5-hostname), und socks5h:// / http://host:port werden ebenfalls akzeptiert. Nicht gesetzt → diese Instanz geht direkt. Die Pipeline hält einen gepoolten httpx-Client pro eindeutiger Proxy-URL (und einen direkten Client), ausgewählt pro Instanz, sodass proxierte und direkte Anbieter nebeneinander laufen. Benötigt das socks-Extra (httpx[socks], bereits gepinnt).

Protokollierung

Neben stderr (vom Docker-json-file-Treiber mit Rotationslimit erfasst) schreibt der Server eine persistente Logdatei nach data/research-mcp.log (Standard; LOG_ROTATION=20 MB, LOG_RETENTION=14 Tage). Sie liegt auf dem data/-Volume und überlebt daher Container-Neustarts und Image-Updates. Die Datei enthält eine Zeile pro Anfrage pro Tool-Aufruf – Suche (query, welche Anbieter-Instanzen tatsächlich liefen, Ergebnisanzahl, Latenz) und Lesen (url, der gewinnende Anbieter/Stufe oder pdf, ok, Latenz), plus eine read_pages count=N ok=K-Zusammenfassung – nützlich, um zu analysieren, wie sich Anfragen auf Anbieter-Stufen verteilen. Es werden keine Anfrageinhalte oder Geheimnisse protokolliert, nur URLs/Queries, Anbieternamen, Zählungen, Zeitmessungen.

Bereitstellung

Gitea Actions baut das Image und pusht es in die Gitea-Registry gitea.vvzvlad.xyz/projects/research-mcp (test → build, Tags latest + sha). In Produktion ziehen wir das vorgebaute Image über docker-compose.yml (hinter Traefik + basicAuth, watchtower aktualisiert latest automatisch; das data/-Volume behält die Logdatei über Updates hinweg) – wir bauen nie in Produktion.

Aufbau

Pfad

Zweck

src/providers/base.py

Anbieter-Schnittstellen + SearchResult / ProviderError.

src/providers/registry.py

@register-Dekorator → REGISTRY.

src/providers/<type>.py

Ein Modul pro Anbieter-Typ.

src/providers/pdf.py

PDF-Erkennung + pypdf-Text-Extraktion (von der Pipeline verwendet).

src/pipeline_config.py

Instanzen im Code + Pipeline-Reihenfolge.

src/pipeline.py

Instanz-Loader + Such-/Lese-Logik.

src/rerank.py

JinaReranker – Re-Ranking der Suchergebnisse nach der Zusammenführung.

src/settings.py

Nicht geheime Stellschrauben (pydantic-settings).

src/server.py

build_server() mit den 3 @mcp.tool-Definitionen.

main.py

Dünner Einstiegspunkt: Server bauen, streamable-http ausführen.

tests/

pytest-Suite (Netzwerk mit respx gemockt).

Maintenance

ActivityActive
ResponsivenessUnresponsive

Related MCP Connectors

Related MCP Servers