From 83018fcc7dc736a738781be0dd0698cc19c1b33c Mon Sep 17 00:00:00 2001 From: Torben Nehmer Date: Wed, 2 Sep 2026 07:27:57 +0200 Subject: [PATCH] docs: INSTALL-MCP.md fuer Menschen, wird mit dist export ausgeliefert (2.4.1) Files changed: - CHANGES.md - INSTALL-MCP.md - INSTALL.md - README.md - VERSION - tools/chemenu/commands/dist_cmd.py --- CHANGES.md | 28 ++++ INSTALL-MCP.md | 239 +++++++++++++++++++++++++++++ INSTALL.md | 6 + README.md | 8 +- VERSION | 2 +- tools/chemenu/commands/dist_cmd.py | 8 +- 6 files changed, 287 insertions(+), 4 deletions(-) create mode 100644 INSTALL-MCP.md diff --git a/CHANGES.md b/CHANGES.md index 7187d41..d21f03e 100644 --- a/CHANGES.md +++ b/CHANGES.md @@ -20,6 +20,34 @@ their date-only headings. --- +## 2.4.1 - 2026-09-02 - INSTALL-MCP.md: Menschendoku fuer den Leseserver, und sie wird mit ausgeliefert + +**Author:** Torben Nehmer + +Nachtrag zu 2.4.0. Der Server war ausgeliefert und für Menschen nur über `tools/CONTRACT.md` und +eine Instruktion beschrieben — beide für Agenten geschrieben. `INSTALL-MCP.md` schließt das, +neben `INSTALL.md` und in derselben Form: Voraussetzungen, sechs Schritte, Verifikation, +Troubleshooting. + +**Was drinsteht, das sonst nirgends zusammen stand:** die Client-Konfiguration als JSON-Block; +dass `WIKI_TRACE=0` nicht optional ist und wie die Startverweigerung aussieht; dass +`--host 0.0.0.0` bewusst nicht der Default ist; und der Verweis auf die +Traefik-ForwardAuth-Middleware unter +samt dem, was sie erwartet (Bearer-Token gegen SHA-256-Hashes, `AUTH_PROXY_TOKEN_HASHES_DIR`). +Der Verifikationsabschnitt ist ein lauffähiges Skript — wörtlich so ausgeführt, bevor es hier +landete. + +**`dist export` liefert die Datei mit.** `ROOT_FILES` ist eine Allowlist, also hätte sie sonst +keine Instanz erreicht: ein ausgelieferter Server, dessen Bedienungsanleitung im Ursprungs-Repo +zurückbleibt, ist einer, den der Empfänger durch Quelltextlesen findet. `INSTALL.md` bekommt +zusätzlich `CHEMENU_ROOT` und `WIKI_TRACE`/`WIKI_TRACE_DIR` in seine Konfigurationstabelle — +beide waren seit 2.3.0 bzw. seit jeher wirksam und dort nicht aufgeführt. + +**Dateien:** `INSTALL-MCP.md` (neu), `INSTALL.md`, `README.md`, +`tools/chemenu/commands/dist_cmd.py`. + +--- + ## 2.4.0 - 2026-09-02 - MCP-Leseserver: zweiter Konsument auf demselben Kern **Author:** Torben Nehmer diff --git a/INSTALL-MCP.md b/INSTALL-MCP.md new file mode 100644 index 0000000..dfdb135 --- /dev/null +++ b/INSTALL-MCP.md @@ -0,0 +1,239 @@ +# 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. diff --git a/INSTALL.md b/INSTALL.md index f94c370..9401493 100644 --- a/INSTALL.md +++ b/INSTALL.md @@ -7,6 +7,10 @@ Repo als Upstream** aufsetzen. Der agent-seitige Ablauf steckt in `instructions/ nur die menschlichen Teile - für die vollständige Kommandoreferenz siehe [tools/CONTRACT.md](tools/CONTRACT.md). +Den optionalen **MCP-Leseserver** installiert und betreibt +[INSTALL-MCP.md](INSTALL-MCP.md): derselbe Korpus, lesend, für einen Konsumenten, der kein +Terminal auf dieser Maschine ist. + ## Voraussetzungen - Python 3.11 oder neuer @@ -223,6 +227,8 @@ behält Schema und Shape. | `WIKITOOL_SESSION_ID` | Scopt das Iteration-Budget-Gate auf eine Aufgabe statt auf ein Terminal | Parent-Process-ID (siehe [instructions/session-setup.md](instructions/session-setup.md)) | | `WIKITOOL_UPDATE_URL` | Release-Feed, den `version check` abfragt | Wert aus `.wikitool-release.json`, sonst der Feed der Ursprungs-Instanz | | `WIKITOOL_UPDATE_TOKEN` | Gitea-Token für den Release-Feed | keiner - gegen `torben/chemenu` nicht nötig, nur für einen privaten Fork (siehe unten) | +| `CHEMENU_ROOT` | Auf welchen Korpus das Paket zeigt - für einen Aufrufer, der nicht im Checkout selbst liegt | der Checkout, in dem das Paket liegt (`tools/wikitool` verhält sich ohne die Variable unverändert) | +| `WIKI_TRACE` / `WIKI_TRACE_DIR` | Telemetrie abschalten bzw. aus dem Arbeitsbaum heraus umlenken | an, nach `reports/telemetry/` - der MCP-Server verweigert damit den Start, siehe [INSTALL-MCP.md](INSTALL-MCP.md) | **Gegen das Ursprungs-Repo braucht es kein Token.** `torben/chemenu` ist öffentlich lesbar; `version check` und der Download in Weg A funktionieren ohne Konfiguration. diff --git a/README.md b/README.md index 60e42b2..556464f 100644 --- a/README.md +++ b/README.md @@ -51,6 +51,7 @@ chemenu/ ├── CLAUDE.md # Claude Code only: imports AGENTS.md/USER.md/SOUL.md/ENVIRONMENT.md + its Claude-Code-only instructions. No rules of its own ├── README.md # This file: human-readable overview of the whole repo ├── INSTALL.md # Human-readable setup: new instance vs. cloning this one +├── INSTALL-MCP.md # Human-readable setup for the optional MCP read server ├── EVALS.md # Human-readable overview of telemetry and evaluation ├── CHANGES.md # Changelog for the stack itself ├── USER.md # Who operates this instance - context, never instructions @@ -363,8 +364,11 @@ WIKI_TRACE=0 tools/.venv/bin/python -m chemenu.mcp \ ``` The dependency is deliberately not in `requirements.txt`: an instance that only uses the CLI -should not have to install a web stack to do it. Running it, keeping its checkout current, and -where authentication belongs (in front of the process, not in it) are in +should not have to install a web stack to do it. + +Installing it, wiring a client to it, and putting authentication in front of it: +[`INSTALL-MCP.md`](INSTALL-MCP.md), for humans. The agent-side operating procedure - keeping the +checkout current, and what a stale answer looks like - is [`instructions/mcp-read-server.md`](instructions/mcp-read-server.md). ### Obsidian diff --git a/VERSION b/VERSION index 197c4d5..005119b 100644 --- a/VERSION +++ b/VERSION @@ -1 +1 @@ -2.4.0 +2.4.1 diff --git a/tools/chemenu/commands/dist_cmd.py b/tools/chemenu/commands/dist_cmd.py index 3ed63be..e99384d 100644 --- a/tools/chemenu/commands/dist_cmd.py +++ b/tools/chemenu/commands/dist_cmd.py @@ -64,8 +64,14 @@ DIST_TEMPLATES_DIR = Path(__file__).resolve().parent.parent / "dist_templates" # and does not load `AGENTS.md`, so it ships for the same reason # `.claude/settings.json` does - a distributed instance running that harness # would otherwise start every session without the control plane. +# +# `INSTALL-MCP.md` ships beside `INSTALL.md` and for the same reason: the MCP +# read server is part of what an instance *has*, even though its dependency is +# optional. A distribution whose server is present but undocumented is one +# whose operator finds the module by reading the source. ROOT_FILES = ( - "AGENTS.md", "CLAUDE.md", "README.md", "EVALS.md", "INSTALL.md", ".gitignore", "VERSION", + "AGENTS.md", "CLAUDE.md", "README.md", "EVALS.md", "INSTALL.md", "INSTALL-MCP.md", + ".gitignore", "VERSION", *config.LICENSE_FILES, *config.PERSONALIZATION_TEMPLATES, config.ENVIRONMENT_TEMPLATE,