MCP-Leseserver für chemenu: search/types/lint/status als Tools, ohne Schreibpfad nach kb/ #19

Closed
opened 2026-08-31 13:28:39 +00:00 by torben · 4 comments
Owner

Ein MCP-Server ist beschlossen, nicht mehr bedingt: es wird mehrere Konsumenten geben, der
Perplexity Space war nur der erste. Zwei Grundsatzfragen sind am 2026-09-01 entschieden und
stehen hier nicht mehr zur Debatte (Herleitung im Kommentarverlauf):

  • Plattform: Python. Kein Go, auch nicht als Option.
  • Authentifizierung: Traefik-Middleware. Nicht sauber authentifizierte Zugriffe erreichen den
    Python-Prozess nicht.

Befund

wikitool ist ein lokales CLI ohne Netzwerkschnittstelle. Der gitea-mcp-Connector bildet nur die
Gitea-REST-API ab, keinen Ausführungs-Endpunkt. Ohne eigene Ausführungsschicht kann ein externer
Konsument nur über einzelne get_file_contents-Aufrufe lesen — langsamer und ungenauer als
wikitool search.

Die tragfähige Formulierung ist nicht „wikitool über MCP exponieren", sondern: chemenu
bekommt einen zweiten Konsumenten.
CLI und MCP-Server sind zwei Adapter auf denselben Kern.
run_search() (commands/search.py:82) und run_lint() (commands/lint.py:77) sind bereits
weitgehend reine Funktionen; was fehlt, ist die Bibliotheksgrenze darunter — das ist #31 und
Voraussetzung hierfür.

Oberfläche

Tool Quelle Anmerkung
search run_search, inkl. --field-Prädikaten
types list / types describe types_cmd
lint run_lint nur die JSON-Form; ohne Flag schreibt lint einen Report nach reports/ (lint.py:457-467)
status komponiert Es gibt kein wikitool status. wiki-status ist ein Skill, der kb/index.md + lint + kb/log.md zusammensetzt — der MCP-status ist eine neue, serverseitig komponierte Oberfläche, kein Wrapper.

Explizit nicht exponiert: new, touch, xref, cite, publish, migrate,
version bump, alles unter work/. Strukturell — die Funktionen existieren im Servercode gar
nicht —, nicht als Prompt-Regel.

Wire-Contract sind exakt die --json-Formen der CLI (search --json, lint --json,
types list --json). Ein Contract, nicht zwei; die CLI wird zur ausführbaren Spezifikation, ein
Golden-Test hält beide gegeneinander.

Direktimport, nicht Subprocess

Der Iteration Budget Gate zählt nicht, was man erwartet (run_budget.py:109):

SKIP_COMMANDS = {"search", "doctor"}

search und doctor sind befreit — lint und types nicht. Ein Subprocess-Server ohne
Session-Injektion teilt sich über den getppid()-Fallback einen Zähler und steht nach 60
lint/types-Aufrufen. Mit Session-Injektion pro Anfrage ist das Gate umgekehrt bedeutungslos.
Beide Enden sind falsch.

Dazu: „strukturell, nicht Prompt-Regel" hält beim Subprocess nur mit fester argv-Allowlist —
etwa so viel Arbeit wie der Direktimport, ohne dessen Vorteile.

Fallen beim Direktimport:

  • _util.fail() (_util.py:41-47) wirft typer.Exit und setzt modulglobalen Zustand. Teil von #31.
  • WIKI_TRACE ist per Default an und schreibt Traces in den Checkout (telemetry/writer.py:35) —
    also pro Anfrage, in genau das Verzeichnis, das der Sync per git reset --hard sauber hält.
    WIKI_TRACE_DIR nach außen legen oder abschalten.

Der residente Prozess ist zugleich die Performance-Lösung

Ein wikitool search kostet gemessen 593 ms: 262 ms Modulimport, 274 ms Korpus-Parse
(176 Seiten), 7 ms ripgrep. Ein Server zahlt den Import einmal pro Prozessstart statt pro
Aufruf
. Zusammen mit dem Korpus-Cache und CSafeLoader aus #33 bleiben davon grob 100 ms.

Derselbe residente Prozess ist später der Ort, an dem ein Vektorindex lebt (#35) — ein
Embedding-Modell pro CLI-Aufruf zu laden wäre absurd.

Budget-Gate gehört nicht in den Server

Der Gate existiert, um eine Agenten-Session am unbemerkten Iterieren über den Zustand des
Wikis zu hindern; deshalb ist Retrieval befreit. Ein Nutzer, der zu oft sucht, ist ein
Ressourcen-Problem — anderes Instrument, anderer Zweck. Beides zu vermischen würde den Gate zu
einem Rate Limiter verwässern.

Ressourcenschutz gehört damit an zwei Stellen, keine davon ist der Budget-Gate:

  • Vor dem Prozess: Rate Limiting in Traefik, neben der Authentifizierung.
  • Im Prozess: die Limits aus #33 (Subprozess-Timeout, Regex, YAML-Grenzen) — sie schützen
    gegen einen authentifizierten Konsumenten, der sich selbst versehentlich beschädigt.

Session-Scoping wird erst mit dem Schreibpfad real (#32), wo eine Einreichung zurechenbar ist.

Staleness

search liest den Arbeitsbaum. Ein veralteter Checkout antwortet selbstbewusst falsch — genau
der Fehler, den SOUL.md als Kardinalfehler benennt.

Jede Antwort trägt den ausgelieferten Commit ({"commit": "<sha>", "as_of": "<iso>"}). Damit
wird aus einer stillen veralteten Antwort eine sichtbare. Derselbe SHA ist der Cache-Key aus #33
— ein Schlüssel für beides.

Sync: Polling. git fetch && git reset --hard origin/main alle paar Minuten braucht keinen
eingehenden Endpunkt und keine Signaturprüfung. Ein Webhook ist eine spätere Optimierung.

Abgrenzung zu #15

#15 (Recherche-Fähigkeit) läuft in die Gegenrichtung: ein Agent ohne eigene Websuche recherchiert
über eine externe API und landet das Ergebnis unverifiziert in raw/research/ (push,
kostenpflichtig, nicht-deterministisch). Dieses Vorhaben liest bestätigtes kb/-Wissen (pull,
deterministisch). Beide unabhängig umsetzbar, bewusst nicht dieselbe Oberfläche.

Offene Fragen

  • Transport: stdio, SSE oder streamable HTTP?
  • Wird der Server im Repo entwickelt oder als eigenes Artefakt, das chemenu als Abhängigkeit zieht?

Akzeptanzkriterien

  • Server exponiert search/types/lint/status, keine Schreib-Tools — weil die Funktionen
    dort nicht existieren, nicht weil sie gefiltert werden
  • Ein Test beweist, dass kein Pfad des Servers nach kb/, reports/ oder in git schreibt
  • Antwortformate sind die --json-Formen der CLI; Golden-Test hält beide zusammen
  • Jede Antwort trägt Commit-SHA und Zeitstempel
  • Korpus-Cache, invalidiert am SHA (gemeinsam mit #33)
  • Traefik-Middleware für Auth und Rate Limiting steht und ist gegengeprüft: ein Aufruf ohne
    gültige Credentials erreicht den Prozess nachweislich nicht
  • WIKI_TRACE_DIR zeigt aus dem Arbeitsbaum heraus, oder Tracing ist abgeschaltet
  • Sync-Mechanismus definiert und dokumentiert
  • Deployment dieser Instanz gehört nicht hierher — private Infrastruktur, siehe Kommentar

Hängt ab von: #31 (Bibliotheksgrenze), #33 (Härtung des Lesepfads).
Blockiert: #32 (Ingest-Queue). Verwandt: #35 (semantisches Backend im selben Prozess).

Ein MCP-Server ist **beschlossen**, nicht mehr bedingt: es wird mehrere Konsumenten geben, der Perplexity Space war nur der erste. Zwei Grundsatzfragen sind am 2026-09-01 entschieden und stehen hier nicht mehr zur Debatte (Herleitung im Kommentarverlauf): - **Plattform: Python.** Kein Go, auch nicht als Option. - **Authentifizierung: Traefik-Middleware.** Nicht sauber authentifizierte Zugriffe erreichen den Python-Prozess nicht. ## Befund `wikitool` ist ein lokales CLI ohne Netzwerkschnittstelle. Der gitea-mcp-Connector bildet nur die Gitea-REST-API ab, keinen Ausführungs-Endpunkt. Ohne eigene Ausführungsschicht kann ein externer Konsument nur über einzelne `get_file_contents`-Aufrufe lesen — langsamer und ungenauer als `wikitool search`. Die tragfähige Formulierung ist nicht „wikitool über MCP exponieren", sondern: **`chemenu` bekommt einen zweiten Konsumenten.** CLI und MCP-Server sind zwei Adapter auf denselben Kern. `run_search()` (`commands/search.py:82`) und `run_lint()` (`commands/lint.py:77`) sind bereits weitgehend reine Funktionen; was fehlt, ist die Bibliotheksgrenze darunter — das ist #31 und Voraussetzung hierfür. ## Oberfläche | Tool | Quelle | Anmerkung | |---|---|---| | `search` | `run_search`, inkl. `--field`-Prädikaten | | | `types list` / `types describe` | `types_cmd` | | | `lint` | `run_lint` | **nur** die JSON-Form; ohne Flag schreibt `lint` einen Report nach `reports/` (`lint.py:457-467`) | | `status` | komponiert | Es gibt **kein** `wikitool status`. `wiki-status` ist ein Skill, der `kb/index.md` + `lint` + `kb/log.md` zusammensetzt — der MCP-`status` ist eine neue, serverseitig komponierte Oberfläche, kein Wrapper. | Explizit **nicht** exponiert: `new`, `touch`, `xref`, `cite`, `publish`, `migrate`, `version bump`, alles unter `work/`. Strukturell — die Funktionen existieren im Servercode gar nicht —, nicht als Prompt-Regel. Wire-Contract sind **exakt die `--json`-Formen der CLI** (`search --json`, `lint --json`, `types list --json`). Ein Contract, nicht zwei; die CLI wird zur ausführbaren Spezifikation, ein Golden-Test hält beide gegeneinander. ## Direktimport, nicht Subprocess Der Iteration Budget Gate zählt nicht, was man erwartet (`run_budget.py:109`): ```python SKIP_COMMANDS = {"search", "doctor"} ``` `search` und `doctor` sind befreit — `lint` und `types` **nicht**. Ein Subprocess-Server ohne Session-Injektion teilt sich über den `getppid()`-Fallback einen Zähler und steht nach 60 `lint`/`types`-Aufrufen. Mit Session-Injektion pro Anfrage ist das Gate umgekehrt bedeutungslos. Beide Enden sind falsch. Dazu: „strukturell, nicht Prompt-Regel" hält beim Subprocess nur mit fester argv-Allowlist — etwa so viel Arbeit wie der Direktimport, ohne dessen Vorteile. Fallen beim Direktimport: - `_util.fail()` (`_util.py:41-47`) wirft `typer.Exit` und setzt modulglobalen Zustand. Teil von #31. - `WIKI_TRACE` ist per Default an und schreibt Traces in den Checkout (`telemetry/writer.py:35`) — also pro Anfrage, in genau das Verzeichnis, das der Sync per `git reset --hard` sauber hält. `WIKI_TRACE_DIR` nach außen legen oder abschalten. ## Der residente Prozess ist zugleich die Performance-Lösung Ein `wikitool search` kostet gemessen 593 ms: 262 ms Modulimport, 274 ms Korpus-Parse (176 Seiten), 7 ms ripgrep. Ein Server zahlt den Import **einmal pro Prozessstart statt pro Aufruf**. Zusammen mit dem Korpus-Cache und `CSafeLoader` aus #33 bleiben davon grob 100 ms. Derselbe residente Prozess ist später der Ort, an dem ein Vektorindex lebt (#35) — ein Embedding-Modell pro CLI-Aufruf zu laden wäre absurd. ## Budget-Gate gehört nicht in den Server Der Gate existiert, um eine **Agenten-Session** am unbemerkten Iterieren über den Zustand des Wikis zu hindern; deshalb ist Retrieval befreit. Ein Nutzer, der zu oft sucht, ist ein *Ressourcen*-Problem — anderes Instrument, anderer Zweck. Beides zu vermischen würde den Gate zu einem Rate Limiter verwässern. Ressourcenschutz gehört damit an zwei Stellen, keine davon ist der Budget-Gate: - **Vor dem Prozess:** Rate Limiting in Traefik, neben der Authentifizierung. - **Im Prozess:** die Limits aus #33 (Subprozess-Timeout, Regex, YAML-Grenzen) — sie schützen gegen einen authentifizierten Konsumenten, der sich selbst versehentlich beschädigt. Session-Scoping wird erst mit dem Schreibpfad real (#32), wo eine Einreichung zurechenbar ist. ## Staleness `search` liest den Arbeitsbaum. Ein veralteter Checkout antwortet selbstbewusst falsch — genau der Fehler, den `SOUL.md` als Kardinalfehler benennt. **Jede Antwort trägt den ausgelieferten Commit** (`{"commit": "<sha>", "as_of": "<iso>"}`). Damit wird aus einer stillen veralteten Antwort eine sichtbare. Derselbe SHA ist der Cache-Key aus #33 — ein Schlüssel für beides. Sync: Polling. `git fetch && git reset --hard origin/main` alle paar Minuten braucht keinen eingehenden Endpunkt und keine Signaturprüfung. Ein Webhook ist eine spätere Optimierung. ## Abgrenzung zu #15 #15 (Recherche-Fähigkeit) läuft in die Gegenrichtung: ein Agent ohne eigene Websuche recherchiert über eine externe API und landet das Ergebnis unverifiziert in `raw/research/` (push, kostenpflichtig, nicht-deterministisch). Dieses Vorhaben liest bestätigtes `kb/`-Wissen (pull, deterministisch). Beide unabhängig umsetzbar, bewusst nicht dieselbe Oberfläche. ## Offene Fragen - Transport: stdio, SSE oder streamable HTTP? - Wird der Server im Repo entwickelt oder als eigenes Artefakt, das `chemenu` als Abhängigkeit zieht? ## Akzeptanzkriterien - [ ] Server exponiert `search`/`types`/`lint`/`status`, keine Schreib-Tools — weil die Funktionen dort nicht existieren, nicht weil sie gefiltert werden - [ ] Ein Test beweist, dass kein Pfad des Servers nach `kb/`, `reports/` oder in git schreibt - [ ] Antwortformate sind die `--json`-Formen der CLI; Golden-Test hält beide zusammen - [ ] Jede Antwort trägt Commit-SHA und Zeitstempel - [ ] Korpus-Cache, invalidiert am SHA (gemeinsam mit #33) - [ ] Traefik-Middleware für Auth und Rate Limiting steht und ist gegengeprüft: ein Aufruf ohne gültige Credentials erreicht den Prozess nachweislich nicht - [ ] `WIKI_TRACE_DIR` zeigt aus dem Arbeitsbaum heraus, oder Tracing ist abgeschaltet - [ ] Sync-Mechanismus definiert und dokumentiert - [ ] Deployment dieser Instanz gehört **nicht** hierher — private Infrastruktur, siehe Kommentar **Hängt ab von:** #31 (Bibliotheksgrenze), #33 (Härtung des Lesepfads). **Blockiert:** #32 (Ingest-Queue). **Verwandt:** #35 (semantisches Backend im selben Prozess).
torben added the prio/waitingsize/L labels 2026-08-31 13:28:39 +00:00
torben changed title from Read-only MCP-Frontend für wikitool (search/status/types/lint) an Perplexity Space koppeln to MCP-Leseserver für chemenu: search/types/lint/status als Tools, ohne Schreibpfad nach kb/ 2026-09-01 20:00:47 +00:00
torben added size/M and removed size/L labels 2026-09-01 20:00:52 +00:00
Author
Owner

Neu geschnitten am 2026-09-01, nach Prüfung gegen den heutigen Code. Was sich geändert
hat und warum:

Was am alten Text nicht mehr stimmte

Stelle Problem
„importiert tools/wiki_tools-Module direkt" Paket heißt seit dem Rename tools/chemenu/. Instanz von #29.
Modulliste search.py, kb_state.py, type_resolver.py search ist heute zweigeteilt: commands/search.py (CLI) und chemenu/search/ (Backends, Filter, Fusion). Die Liste zeigt auf eine Struktur von vor dem Split.
„ADR-002 / kb/concepts/External Ingress Model.md" Existiert nicht mehr. wikitool search findet weder „External Ingress" noch „ADR-002" noch „Abydos" — der Korpus-Purge vom 2026-09-01 hat die private Infrastruktur-Hälfte entfernt.
Akzeptanzkriterium „exponiert search/status/types/lint" Es gibt kein wikitool status. Steht jetzt als eigener Punkt im Text.

Deployment-Teil entfallen. Abydos, hermes, Traefik-Label, MetalLB, 1Password/ESO,
k3s-Namespace: das ist private Infrastruktur in einem öffentlichen Issue-Tracker.
USER.md („Keine privaten Infrastrukturdaten") und ENVIRONMENT.md ziehen diese Grenze
ausdrücklich, der alte Issue-Text zog sie nicht. Was hier bleibt, ist die Schnittstelle;
wo sie läuft, gehört in die private Instanz.

Aufgeteilt in drei Pakete. Als ein size/L wäre das nie angefasst worden:

Paket Issue Größe
A — Root-Auflösung konfigurierbar, Bibliotheksgrenze ohne typer.Exit #31 size/S
B — MCP-Leseserver (dieses Issue) #19 size/M
C — Ingest-Queue (Schreibpfad) #32 size/M

Fachlich neu im Text: der Befund zum Iteration Budget Gate (search/doctor befreit,
lint/types nicht — ein Subprocess-Server läuft damit nach 60 Aufrufen tot), die
Empfehlung Direktimport statt Subprocess mit Begründung, der Commit-Stempel in jeder
Antwort als Mittel gegen stille Staleness, und die Feststellung, dass Session-Scoping für
die Leseoberfläche das falsche Instrument ist (Rate Limit statt Budget-Gate).

Relabel size/Lsize/M: die offenen Designfragen, die das L begründet haben
(Subprocess vs. Direktimport, Sync-Mechanismus, Session-Scoping), sind beantwortet, und
der Schreibpfad ist nach #32 abgeflossen. Was übrig bleibt, ist mehrere Dateien plus
eigener Testaufwand.

prio/3 bleibt, mit jetzt benanntem Auslöser: ein Konsument, der die Oberfläche
tatsächlich braucht.

Neu geschnitten am 2026-09-01, nach Prüfung gegen den heutigen Code. Was sich geändert hat und warum: **Was am alten Text nicht mehr stimmte** | Stelle | Problem | |---|---| | „importiert `tools/wiki_tools`-Module direkt" | Paket heißt seit dem Rename `tools/chemenu/`. Instanz von #29. | | Modulliste `search.py, kb_state.py, type_resolver.py` | `search` ist heute zweigeteilt: `commands/search.py` (CLI) und `chemenu/search/` (Backends, Filter, Fusion). Die Liste zeigt auf eine Struktur von vor dem Split. | | „ADR-002 / `kb/concepts/External Ingress Model.md`" | Existiert nicht mehr. `wikitool search` findet weder „External Ingress" noch „ADR-002" noch „Abydos" — der Korpus-Purge vom 2026-09-01 hat die private Infrastruktur-Hälfte entfernt. | | Akzeptanzkriterium „exponiert search/**status**/types/lint" | Es gibt kein `wikitool status`. Steht jetzt als eigener Punkt im Text. | **Deployment-Teil entfallen.** Abydos, hermes, Traefik-Label, MetalLB, 1Password/ESO, k3s-Namespace: das ist private Infrastruktur in einem öffentlichen Issue-Tracker. `USER.md` („Keine privaten Infrastrukturdaten") und `ENVIRONMENT.md` ziehen diese Grenze ausdrücklich, der alte Issue-Text zog sie nicht. Was hier bleibt, ist die Schnittstelle; *wo* sie läuft, gehört in die private Instanz. **Aufgeteilt in drei Pakete.** Als ein `size/L` wäre das nie angefasst worden: | Paket | Issue | Größe | |---|---|---| | A — Root-Auflösung konfigurierbar, Bibliotheksgrenze ohne `typer.Exit` | #31 | `size/S` | | B — MCP-Leseserver (dieses Issue) | #19 | `size/M` | | C — Ingest-Queue (Schreibpfad) | #32 | `size/M` | **Fachlich neu im Text:** der Befund zum Iteration Budget Gate (`search`/`doctor` befreit, `lint`/`types` nicht — ein Subprocess-Server läuft damit nach 60 Aufrufen tot), die Empfehlung Direktimport statt Subprocess mit Begründung, der Commit-Stempel in jeder Antwort als Mittel gegen stille Staleness, und die Feststellung, dass Session-Scoping für die Leseoberfläche das falsche Instrument ist (Rate Limit statt Budget-Gate). **Relabel `size/L` → `size/M`:** die offenen Designfragen, die das L begründet haben (Subprocess vs. Direktimport, Sync-Mechanismus, Session-Scoping), sind beantwortet, und der Schreibpfad ist nach #32 abgeflossen. Was übrig bleibt, ist mehrere Dateien plus eigener Testaufwand. `prio/3` bleibt, mit jetzt benanntem Auslöser: ein Konsument, der die Oberfläche tatsächlich braucht.
torben added prio/planned and removed prio/waiting labels 2026-09-01 21:12:27 +00:00
Author
Owner

Plattformentscheidung 2026-09-01: Python bleibt, Go ist vom Tisch

Ausgelöst durch die Frage, ob Python für einen netzexponierten Dienst security-seitig noch
tragfähig ist, und ob wikitool vollständig nach Go migriert werden sollte. Zwei
Mehr-Perspektiven-Debatten mit neun Positionen, alle tragenden Befunde in der Sitzung selbst
nachgemessen. Ergebnis: Python, kein Go, auch nicht als Option.

Die Begründung, kurz — damit niemand die Frage in sechs Monaten neu aufmacht:

Sicherheit trägt die Migration nicht. Von zehn Befunden am Lesepfad waren zwei
sprachabhängig (ReDoS, YAML-Alias-Budget), beide mit wenigen Zeilen in Python behebbar — jetzt
#33. Die teuersten Befunde waren Isolation und Konfiguration, und die sind sprachneutral.

Die drei Argumente, die für Go sprachen, lösen sich am eigenen Vorhaben auf:

  • Startkosten (262 ms Import pro Aufruf): ein residenter MCP-Server zahlt sie einmal pro
    Prozessstart. Das stärkste gemessene Go-Argument verschwindet durch die Entscheidung, die in
    diesem Issue ohnehin gefallen ist.
  • Korpus-Parse (274 ms, skaliert linear): CSafeLoader ist verfügbar und dreimal schneller
    (246 → 82 ms), plus Cache — #33.
  • Statisches Binary für die Distribution: überlebt Index und Suche, stirbt an der
    Embedding-Schicht. Lokale Embedding-Erzeugung gibt es in reinem Go nicht; der Weg führt über
    ONNX oder llama.cpp, also cgo plus Shared Library plus Cross-Toolchain pro Zielplattform.
    Ausgerechnet dort ist Python durch vorkompilierte Wheels besser aufgestellt. Go läuft an dieser
    Stelle gegen sein eigenes bestes Argument.

Das Vorbild widerlegt die These. qmd ist entgegen kb/entities/tools/qmd.md nicht Go oder
Rust, sondern TypeScript/Bun (verifiziert gegen das Repository): SQLite FTS5 + sqlite-vec +
node-llama-cpp. Das Werkzeug, dessen Fähigkeit hier gewünscht ist, erreicht sie aus einer
Skriptsprache heraus über native Bibliotheken — dasselbe Muster, das Chemenu mit ripgrep seit
Tag eins praktiziert. Die Korrektur der Seite läuft separat über wiki-manage.

Der Terminkonflikt gab den Ausschlag. instructions/private-instance.md bezieht
Stack-Updates per git merge upstream/main unter dem Leitsatz „Take the machinery, never the
content". Eine Portierung ersetzt genau diese Maschinerie; jeder künftige Update-Merge in die
private Instanz aus #30 wäre ein Totalkonflikt in tools/, und die Auflösungsregel „Konflikt in
tools/? Nimm die Upstream-Seite" hieße: nimm eine Sprache, für die dein Checkout keine Laufzeit
hat. Die private Instanz entsteht jetzt.

Zur Vollständigkeit, weil es fair ist: eine agentengeführte Portierung wäre ausführbar
gewesen — geschätzt 25–40 Sitzungen, differentielle Testbank gegen die bestehende Implementierung
als Orakel, mit der Regel „das Go-Binary ist bis zum letzten Schnitt nur Diff-Partner", die den
Zustand zweier halbportierter Implementierungen strukturell ausschließt. Die Entscheidung fällt
also nicht gegen die Machbarkeit, sondern gegen den Nutzen.

Authentifizierung: Traefik-Middleware

Ebenfalls entschieden. Auth wird als Traefik-Middleware vor dem Dienst abgebildet; nicht sauber
authentifizierte Zugriffe erreichen den Python-Prozess nicht. Konsequenzen, im Text
eingearbeitet:

  • Der Auth-Mechanismus ist keine offene Frage dieses Issues mehr.
  • Rate Limiting gehört an dieselbe Stelle — vor den Prozess, nicht in den Budget-Gate.
  • Die In-Prozess-Limits aus #33 bleiben trotzdem nötig: sie schützen nicht gegen Anonyme, sondern
    gegen einen authentifizierten Konsumenten, der sich selbst versehentlich beschädigt.

Relabel prio/3prio/2

Der Auslöser, den ich beim letzten Neuschnitt als Bedingung notiert hatte („ein Konsument, der
die Oberfläche tatsächlich braucht"), ist eingetreten: der Server ist beschlossen, mehrere
Konsumenten sind absehbar. Damit ist es geplante Arbeit, kein wartender Vorschlag mehr.

## Plattformentscheidung 2026-09-01: Python bleibt, Go ist vom Tisch Ausgelöst durch die Frage, ob Python für einen netzexponierten Dienst security-seitig noch tragfähig ist, und ob `wikitool` vollständig nach Go migriert werden sollte. Zwei Mehr-Perspektiven-Debatten mit neun Positionen, alle tragenden Befunde in der Sitzung selbst nachgemessen. Ergebnis: **Python, kein Go, auch nicht als Option.** Die Begründung, kurz — damit niemand die Frage in sechs Monaten neu aufmacht: **Sicherheit trägt die Migration nicht.** Von zehn Befunden am Lesepfad waren zwei sprachabhängig (ReDoS, YAML-Alias-Budget), beide mit wenigen Zeilen in Python behebbar — jetzt #33. Die teuersten Befunde waren Isolation und Konfiguration, und die sind sprachneutral. **Die drei Argumente, die für Go sprachen, lösen sich am eigenen Vorhaben auf:** - *Startkosten* (262 ms Import pro Aufruf): ein residenter MCP-Server zahlt sie einmal pro Prozessstart. Das stärkste gemessene Go-Argument verschwindet durch die Entscheidung, die in diesem Issue ohnehin gefallen ist. - *Korpus-Parse* (274 ms, skaliert linear): `CSafeLoader` ist verfügbar und dreimal schneller (246 → 82 ms), plus Cache — #33. - *Statisches Binary für die Distribution*: überlebt Index und Suche, stirbt an der Embedding-Schicht. Lokale Embedding-Erzeugung gibt es in reinem Go nicht; der Weg führt über ONNX oder llama.cpp, also cgo plus Shared Library plus Cross-Toolchain pro Zielplattform. Ausgerechnet dort ist Python durch vorkompilierte Wheels besser aufgestellt. Go läuft an dieser Stelle gegen sein eigenes bestes Argument. **Das Vorbild widerlegt die These.** `qmd` ist entgegen `kb/entities/tools/qmd.md` nicht Go oder Rust, sondern **TypeScript/Bun** (verifiziert gegen das Repository): SQLite FTS5 + sqlite-vec + node-llama-cpp. Das Werkzeug, dessen Fähigkeit hier gewünscht ist, erreicht sie aus einer Skriptsprache heraus über native Bibliotheken — dasselbe Muster, das Chemenu mit `ripgrep` seit Tag eins praktiziert. Die Korrektur der Seite läuft separat über `wiki-manage`. **Der Terminkonflikt gab den Ausschlag.** `instructions/private-instance.md` bezieht Stack-Updates per `git merge upstream/main` unter dem Leitsatz „Take the machinery, never the content". Eine Portierung ersetzt genau diese Maschinerie; jeder künftige Update-Merge in die private Instanz aus #30 wäre ein Totalkonflikt in `tools/`, und die Auflösungsregel „Konflikt in `tools/`? Nimm die Upstream-Seite" hieße: nimm eine Sprache, für die dein Checkout keine Laufzeit hat. Die private Instanz entsteht jetzt. Zur Vollständigkeit, weil es fair ist: eine agentengeführte Portierung wäre **ausführbar** gewesen — geschätzt 25–40 Sitzungen, differentielle Testbank gegen die bestehende Implementierung als Orakel, mit der Regel „das Go-Binary ist bis zum letzten Schnitt nur Diff-Partner", die den Zustand zweier halbportierter Implementierungen strukturell ausschließt. Die Entscheidung fällt also nicht gegen die Machbarkeit, sondern gegen den Nutzen. ## Authentifizierung: Traefik-Middleware Ebenfalls entschieden. Auth wird als Traefik-Middleware vor dem Dienst abgebildet; nicht sauber authentifizierte Zugriffe erreichen den Python-Prozess nicht. Konsequenzen, im Text eingearbeitet: - Der Auth-Mechanismus ist keine offene Frage dieses Issues mehr. - Rate Limiting gehört an dieselbe Stelle — vor den Prozess, nicht in den Budget-Gate. - Die In-Prozess-Limits aus #33 bleiben trotzdem nötig: sie schützen nicht gegen Anonyme, sondern gegen einen authentifizierten Konsumenten, der sich selbst versehentlich beschädigt. ## Relabel `prio/3` → `prio/2` Der Auslöser, den ich beim letzten Neuschnitt als Bedingung notiert hatte („ein Konsument, der die Oberfläche tatsächlich braucht"), ist eingetreten: der Server ist beschlossen, mehrere Konsumenten sind absehbar. Damit ist es geplante Arbeit, kein wartender Vorschlag mehr.
Author
Owner

Teil der Sequenz in #36 (Master: Weg zum MCP-Leseserver) — dort Schritt 3 von 3, nach #33 und
#31. Das Master-Issue hält die getroffenen Entscheidungen, die Messungen und die verifizierten
Befunde gebündelt, damit sie nicht über mehrere Kommentarverläufe verstreut nachgelesen werden
müssen.

Teil der Sequenz in **#36** (Master: Weg zum MCP-Leseserver) — dort Schritt 3 von 3, nach #33 und #31. Das Master-Issue hält die getroffenen Entscheidungen, die Messungen und die verifizierten Befunde gebündelt, damit sie nicht über mehrere Kommentarverläufe verstreut nachgelesen werden müssen.
Author
Owner

Umgesetzt in 2.4.0. tools/chemenu/mcp/.

Die beiden offenen Fragen sind entschieden (Betreiber, 2026-09-02):

  • Transport: beide. stdio zum Entwickeln und Testen ohne Netz, streamable-http für die
    Auslieferung — der einzige, vor den sich die Traefik-Middleware setzen kann, weil sie ein
    HTTP-Reverse-Proxy ist. sse ist über das SDK erreichbar und wird nicht angeboten: der
    abgelöste Remote-Transport, jetzt darauf zu bauen verschiebt den Wechsel nur.
  • Im Repo, nicht als eigenes Artefakt. Der Golden-Test läuft nur mit beiden Seiten in einer
    Testsuite; getrennt würde aus einem Contract eine Versionsabsprache.

Oberfläche: search, types, describe_type, lint, status. status ist wie
beschrieben serverseitig komponiert und kein Wrapper.

Akzeptanzkriterien:

  • Kein Schreibpfad — strukturell. Weder der Server noch chemenu.api importiert etwas
    unter chemenu.commands; ein Test importiert das Servermodul in einem frischen Interpreter
    und prüft sys.modules.
  • Ein Test beweist, dass nichts nach kb/, reports/ oder in git schreibt: alle fünf Tools
    werden aufgerufen, danach Dateibaum (Größe und Inhalt), HEAD und
    git status --porcelain gegen vorher verglichen; zusätzlich, dass reports/ gar nicht
    erst entsteht.
  • Antwortformate sind die --json-Formen der CLI, Golden-Test hält beide zusammen. Er ruft
    wikitool als Subprozess gegen denselben Baum auf, über CHEMENU_ROOT — womit er nebenbei
    die Root-Auflösung von außen mitprüft. Für search und für beide types-Formen.
  • Jede Antwort trägt commit und as_of.
  • Korpus-Cache, invalidiert am SHA (kam mit #33).
  • WIKI_TRACE_DIR zeigt aus dem Arbeitsbaum heraus — erzwungen: der Server verweigert
    den Start, wenn Telemetrie an ist und in den bedienten Checkout schreiben würde, statt
    still umzuleiten. Heute emittiert auf diesem Pfad nichts (der Emitter hängt an cli.main()
    und den Gates), die Sperre ist gegen später.
  • Sync-Mechanismus definiert und dokumentiert: Polling
    (git fetch && git reset --hard origin/main) in instructions/mcp-read-server.md.
    reset --hard ist dort tragend — ein abgedrifteter Baum antwortet zwar richtig, parst aber
    bei jeder Anfrage neu und stempelt jede Antwort mit null.
  • Deployment nicht hier.
  • Traefik-Middleware gegengeprüft — offen, siehe unten.

Was nicht Teil davon war und offen bleibt: die Middleware selbst steht
(https://gitea.nehmer.net/torben/gitea-mcp-forward-auth), aber ein Aufruf ohne gültige
Credentials wurde gegen diesen Server noch nicht nachweislich abgewiesen — dafür braucht es
ein Deployment. Das hängt am Container-Image, das jetzt #37 ist.

Zum Budget-Gate: wie im Issue argumentiert nicht im Server. Ressourcenschutz im Prozess sind
die Limits aus #33 (Subprozess-Timeout, Regex, YAML-Grenzen).

Befund 8 aus #36 (lint liest raw/-Dateinamen, lint.py:230) war hier zu entscheiden:
lint ist unverändert exponiert, inklusive uncovered_raw_files. Für diese Instanz ist das
folgenlos — ein Konsument sieht die Dateinamen eines öffentlichen Korpus. Es wird relevant, sobald
mehrere Einreicher existieren, und das ist #32; dort gehört die Entscheidung hin, nicht hierher,
wo sie ohne den Schreibpfad nichts zu schützen hätte.

Gemessen und gefahren: beide Transports gegen den echten Korpus (176 Seiten, korrekter
HEAD-Stempel); wikitool search end-to-end von 593 ms auf 347 ms (aus #33), die verbleibenden
~262 ms Modulimport entfallen im residenten Prozess.

Menschendoku: INSTALL-MCP.md. Betrieb: instructions/mcp-read-server.md.

Umgesetzt in **2.4.0**. `tools/chemenu/mcp/`. **Die beiden offenen Fragen sind entschieden** (Betreiber, 2026-09-02): - **Transport: beide.** `stdio` zum Entwickeln und Testen ohne Netz, `streamable-http` für die Auslieferung — der einzige, vor den sich die Traefik-Middleware setzen kann, weil sie ein HTTP-Reverse-Proxy ist. `sse` ist über das SDK erreichbar und wird **nicht** angeboten: der abgelöste Remote-Transport, jetzt darauf zu bauen verschiebt den Wechsel nur. - **Im Repo**, nicht als eigenes Artefakt. Der Golden-Test läuft nur mit beiden Seiten in einer Testsuite; getrennt würde aus einem Contract eine Versionsabsprache. **Oberfläche:** `search`, `types`, `describe_type`, `lint`, `status`. `status` ist wie beschrieben serverseitig **komponiert** und kein Wrapper. **Akzeptanzkriterien:** - [x] Kein Schreibpfad — **strukturell**. Weder der Server noch `chemenu.api` importiert etwas unter `chemenu.commands`; ein Test importiert das Servermodul in einem frischen Interpreter und prüft `sys.modules`. - [x] Ein Test beweist, dass nichts nach `kb/`, `reports/` oder in git schreibt: alle fünf Tools werden aufgerufen, danach Dateibaum (Größe **und** Inhalt), `HEAD` und `git status --porcelain` gegen vorher verglichen; zusätzlich, dass `reports/` gar nicht erst entsteht. - [x] Antwortformate sind die `--json`-Formen der CLI, Golden-Test hält beide zusammen. Er ruft `wikitool` als Subprozess gegen denselben Baum auf, über `CHEMENU_ROOT` — womit er nebenbei die Root-Auflösung von außen mitprüft. Für `search` und für beide `types`-Formen. - [x] Jede Antwort trägt `commit` und `as_of`. - [x] Korpus-Cache, invalidiert am SHA (kam mit #33). - [x] `WIKI_TRACE_DIR` zeigt aus dem Arbeitsbaum heraus — **erzwungen**: der Server verweigert den Start, wenn Telemetrie an ist und in den bedienten Checkout schreiben würde, statt still umzuleiten. Heute emittiert auf diesem Pfad nichts (der Emitter hängt an `cli.main()` und den Gates), die Sperre ist gegen später. - [x] Sync-Mechanismus definiert und dokumentiert: Polling (`git fetch && git reset --hard origin/main`) in `instructions/mcp-read-server.md`. `reset --hard` ist dort tragend — ein abgedrifteter Baum antwortet zwar richtig, parst aber bei jeder Anfrage neu und stempelt jede Antwort mit `null`. - [x] Deployment nicht hier. - [ ] **Traefik-Middleware gegengeprüft** — offen, siehe unten. **Was nicht Teil davon war und offen bleibt:** die Middleware selbst steht (<https://gitea.nehmer.net/torben/gitea-mcp-forward-auth>), aber ein Aufruf ohne gültige Credentials wurde gegen *diesen* Server noch nicht nachweislich abgewiesen — dafür braucht es ein Deployment. Das hängt am Container-Image, das jetzt **#37** ist. **Zum Budget-Gate:** wie im Issue argumentiert nicht im Server. Ressourcenschutz im Prozess sind die Limits aus #33 (Subprozess-Timeout, Regex, YAML-Grenzen). **Befund 8 aus #36** (`lint` liest `raw/`-Dateinamen, `lint.py:230`) war hier zu entscheiden: `lint` ist unverändert exponiert, inklusive `uncovered_raw_files`. Für diese Instanz ist das folgenlos — ein Konsument sieht die Dateinamen eines öffentlichen Korpus. Es wird relevant, sobald mehrere Einreicher existieren, und das ist #32; dort gehört die Entscheidung hin, nicht hierher, wo sie ohne den Schreibpfad nichts zu schützen hätte. **Gemessen und gefahren:** beide Transports gegen den echten Korpus (176 Seiten, korrekter HEAD-Stempel); `wikitool search` end-to-end von 593 ms auf 347 ms (aus #33), die verbleibenden ~262 ms Modulimport entfallen im residenten Prozess. Menschendoku: `INSTALL-MCP.md`. Betrieb: `instructions/mcp-read-server.md`.
Sign in to join this conversation.
1 Participants
Notifications
Due Date
No due date set.
Dependencies

No dependencies set.

Reference: torben/chemenu#19