Files changed: - CHANGES.md - INSTALL-MCP.md - INSTALL.md - README.md - VERSION - tools/chemenu/commands/dist_cmd.py
9.7 KiB
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; die vollständige Kommandoreferenz in 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 — inklusive
tools/.venvundripgrep - 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.
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.
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:
{
"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.
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://<host>: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 <token> 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:
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:
{ "commit": "<40-stelliger SHA>", "as_of": "<ISO-8601, UTC>", "count": 3, "...": "..." }
Aktuell gehalten wird der Baum durch Polling, aus einem Timer neben dem Server:
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:
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:
tools/wikitool search "<begriff>" --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):
https://gitea.nehmer.net/torben/chemenu/issues/37. Bis dahin ist der Weg oben — venv,
python -m chemenu.mcp, Proxy davor — der vollständige.