# MCP-Leseserver installieren Dieses Dokument richtet sich an Menschen. Es beschreibt, wie der MCP-Leseserver eines Chemenu- Wikis lokal läuft, wie ein Client ihn einbindet, und wie er hinter einer Authentifizierung erreichbar wird. Der agent-seitige Betriebsablauf steht in [instructions/mcp-read-server.md](instructions/mcp-read-server.md); die vollständige Kommandoreferenz in [tools/CONTRACT.md](tools/CONTRACT.md). **Was der Server ist.** Ein zweiter Konsument desselben Kerns, nicht ein zweites Programm. CLI und Server rufen dieselben Funktionen auf; ein Golden-Test hält ihre Ausgaben gegeneinander. Was `tools/wikitool search --json` liefert, liefert das MCP-Tool `search` auch — plus den Commit, aus dem die Antwort berechnet wurde. **Was er nicht ist.** Kein Schreibpfad. Es gibt kein Tool, das eine Seite anlegt, ändert oder publiziert — nicht weil eine Liste gefiltert wird, sondern weil der Server nichts unter `tools/chemenu/commands/` importiert. Die Funktionen sind aus diesem Prozess heraus nicht erreichbar. ## Voraussetzungen - Eine funktionierende Instanz nach [INSTALL.md](INSTALL.md) — inklusive `tools/.venv` und `ripgrep` - Python 3.11 oder neuer (wie die CLI) ## Schritt 1: Abhängigkeit installieren Sie liegt bewusst nicht in `tools/requirements.txt`. Eine Instanz, die nur die CLI benutzt, soll dafür nicht pydantic, starlette, uvicorn und cryptography mitinstallieren müssen. ```bash tools/.venv/bin/pip install -r tools/requirements-mcp.txt ``` ## Schritt 2: Lokal starten (stdio) `stdio` ist der Weg zum Ausprobieren und für einen Client auf derselben Maschine: ein Prozess pro Konsument, lokal gestartet, kein Netzwerk. ```bash WIKI_TRACE=0 tools/.venv/bin/python -m chemenu.mcp ``` Der Prozess spricht MCP über stdin/stdout und gibt für sich genommen nichts aus — das ist richtig so. Gestartet wird er normalerweise nicht von Hand, sondern vom Client (Schritt 3). **`WIKI_TRACE=0` ist nicht optional.** Telemetrie ist per Default an und schreibt nach `reports/telemetry/` im Repo. Der Server **verweigert den Start**, solange das so ist, statt still umzuleiten: ``` ERROR Telemetry is on and would write into the served checkout (...). Set WIKI_TRACE=0, or point WIKI_TRACE_DIR outside the corpus. ``` Beide Auswege sind gleichwertig: `WIKI_TRACE=0` schaltet ab, `WIKI_TRACE_DIR=/var/log/chemenu` lenkt um. Der Grund steht in Schritt 6 — der Sync darf `reports/` wegräumen. ## Schritt 3: Einen Client einbinden Die Konfiguration folgt der üblichen MCP-Client-Form. Absolute Pfade, weil der Client kein Arbeitsverzeichnis erbt: ```json { "mcpServers": { "chemenu": { "command": "/pfad/zur/instanz/tools/.venv/bin/python", "args": ["-m", "chemenu.mcp"], "cwd": "/pfad/zur/instanz/tools", "env": { "WIKI_TRACE": "0", "CHEMENU_ROOT": "/pfad/zur/instanz" } } } } ``` `CHEMENU_ROOT` sagt, **welches** Wiki bedient wird. Ohne die Variable nimmt der Server den Checkout, in dem das Paket selbst liegt — für eine einzelne Instanz reicht das, aber wer mehrere Korpora hat, setzt sie besser immer. Danach kennt der Client fünf Werkzeuge: | Tool | Was es beantwortet | |---|---| | `search` | Seiten in `kb/` nach Text, nach Frontmatter (`confidence<0.6`, `tags~k8s`) oder beidem | | `types` | Welche Seitentypen dieses Wiki kennt | | `describe_type` | Der vollständige Vertrag eines Typs: Felder, Pflichtangaben, Enums | | `lint` | Strukturelle Befunde: kaputte Wikilinks, Waisen, Index-Drift, Schema-Lücken | | `status` | Momentaufnahme: Seitenzahl, Verteilung auf Collections, Befundzahlen | ## Schritt 4: Ausgeliefert starten (streamable HTTP) Der Transport für den Betrieb, und der einzige, vor den sich ein Reverse Proxy setzen kann. ```bash WIKI_TRACE=0 CHEMENU_ROOT=/srv/chemenu \ tools/.venv/bin/python -m chemenu.mcp \ --transport streamable-http --host 0.0.0.0 --port 8000 ``` Der Endpunkt ist dann `http://:8000/mcp`. `--host 0.0.0.0` ist bewusst nicht der Default. Ohne die Angabe bindet der Server auf Loopback, was lokal richtig und im Container falsch ist — dort muss der Proxy ihn erreichen können. Wer `0.0.0.0` setzt, muss also auch dafür sorgen, dass davor etwas steht (Schritt 5). `sse` wird nicht angeboten. Es ist der abgelöste Remote-Transport; jetzt darauf zu bauen verschiebt den Wechsel nur. ## Schritt 5: Authentifizierung davor **Der Server authentifiziert nicht selbst, und das ist Absicht.** Nicht sauber authentifizierte Zugriffe sollen den Python-Prozess gar nicht erst erreichen. Die Authentifizierung ist eine Traefik-ForwardAuth-Middleware: > **** Kurz, was sie tut: sie prüft `Authorization: Bearer ` gegen SHA-256-Hashes erlaubter Tokens, antwortet `200` bei gültigem und `401` bei fehlendem oder falschem Token, und hält `GET /healthz` immer offen. Klartext-Tokens liegen weder in der Konfiguration noch im Log — nur Hashes und ein kurzer Fingerprint. Konfiguriert wird sie über `AUTH_PROXY_TOKEN_HASHES_DIR` (ein Verzeichnis, eine Datei je Token-Hash — passend für ein Kubernetes-Secret-Volume) oder `AUTH_PROXY_TOKEN_HASHES` (kommagetrennte Liste). Einzelheiten, Referenzmanifeste und ein Testskript stehen im README dort. Einen Token-Hash erzeugen: ```bash echo -n "mein-token" | sha256sum | awk '{print $1}' ``` **Rate Limiting gehört an dieselbe Stelle** — vor den Prozess, neben die Authentifizierung. Nicht in den Iteration Budget Gate: der begrenzt eine *Agenten-Session* am unbemerkten Iterieren über den Wiki-Zustand, weshalb Retrieval von ihm ausgenommen ist. Ihn als Rate Limiter zu benutzen würde ihn dazu verwässern. ## Schritt 6: Den Korpus aktuell halten Der Server liest den Arbeitsbaum. Ein veralteter Checkout antwortet selbstbewusst falsch — deshalb trägt **jede Antwort den Commit**, aus dem sie berechnet wurde: ```json { "commit": "<40-stelliger SHA>", "as_of": "", "count": 3, "...": "..." } ``` Aktuell gehalten wird der Baum durch Polling, aus einem Timer neben dem Server: ```bash git -C "$CHEMENU_ROOT" fetch --quiet origin && \ git -C "$CHEMENU_ROOT" reset --hard --quiet origin/main ``` Polling statt Webhook, weil es keinen eingehenden Endpunkt und keine Signaturprüfung braucht — eine kleinere Angriffsfläche als das, was es optimieren würde. `reset --hard` ist dabei tragend und keine Bequemlichkeit: der Korpus-Cache verwendet einen Parse wieder, solange der Commit gleich bleibt, und cacht einen **schmutzigen Baum überhaupt nicht**. Ein abgedrifteter Checkout antwortet also zwar richtig, parst aber bei jeder Anfrage neu — und stempelt jede Antwort mit `"commit": null`, weil sie keiner Revision entspricht. ## Verifikation Läuft es? Der schnellste Test ohne Client — startet den Server über stdio, listet die Tools und stellt eine Frage: ```bash cd tools && WIKI_TRACE=0 .venv/bin/python - <<'EOF' import asyncio, os from mcp import ClientSession, StdioServerParameters from mcp.client.stdio import stdio_client async def main(): params = StdioServerParameters( command=".venv/bin/python", args=["-m", "chemenu.mcp"], env={"WIKI_TRACE": "0", "PATH": os.environ["PATH"]}, ) async with stdio_client(params) as (r, w): async with ClientSession(r, w) as s: await s.initialize() print("Tools:", [t.name for t in (await s.list_tools()).tools]) out = await s.call_tool("status", {}) d = getattr(out, "structuredContent", None) or out.structured_content print("Seiten:", d["pages"], "| Commit:", d["commit"]) asyncio.run(main()) EOF ``` Erwartete Ausgabe, sinngemäß: ``` Tools: ['search', 'types', 'describe_type', 'lint', 'status'] Seiten: 176 | Commit: 576df2cdddc96614a7e6641e562022d52112d411 ``` Gegen die CLI gegenprüfen — beide müssen dieselbe Antwort geben: ```bash tools/wikitool search "" --json ``` ## Troubleshooting **`ERROR Telemetry is on and would write into the served checkout`** — erwartetes Verhalten, kein Fehler in der Installation. `WIKI_TRACE=0` setzen oder `WIKI_TRACE_DIR` aus dem Korpus heraus zeigen lassen (Schritt 2). **`ModuleNotFoundError: No module named 'mcp'`** — Schritt 1 fehlt, oder der Client startet ein anderes Python als das der Instanz. Im Client den absoluten Pfad auf `tools/.venv/bin/python` setzen. **`"commit": null` in jeder Antwort** — der bediente Baum hat uncommittete Änderungen. Entweder läuft der Sync nicht, oder etwas schreibt in den Korpus, das dort nichts zu suchen hat. Der Server selbst schreibt nie; ein Test prüft das, indem er alle fünf Tools aufruft und Dateibaum, `HEAD` und `git status --porcelain` vorher/nachher vergleicht. **`commit` nennt eine alte Revision** — der Sync aus Schritt 6 läuft nicht. **Der Server antwortet anders als `wikitool`** — das ist ein Defekt, keine Konfigurationsdifferenz: beide gehen durch dieselben Funktionen, und ein Golden-Test hält sie zusammen. Zuerst prüfen, ob beide auf denselben Root zeigen; `CHEMENU_ROOT` ist leicht für einen von beiden gesetzt und für den anderen nicht. **Von außen nicht erreichbar** — ohne `--host 0.0.0.0` bindet der Server auf Loopback (Schritt 4). Wenn er dann erreichbar ist, aber jeder Aufruf `401` bekommt, arbeitet die Middleware aus Schritt 5 korrekt und das Token stimmt nicht. ## Was hier bewusst nicht steht Deployment — Cluster, Ingress-Hosts, Secret-Store, FluxCD-Quelle. Das ist private Infrastruktur und dieses Repo ist öffentlich. Ein **Container-Image** für den Betrieb gibt es noch nicht; es ist als eigenes Vorhaben erfasst, mitsamt den Entscheidungen, die dafür noch offen sind (Korpus im Image oder als Volume, wer den Sync ausführt, Basis-Image, Healthcheck): . Bis dahin ist der Weg oben — venv, `python -m chemenu.mcp`, Proxy davor — der vollständige.