MCP-Leseserver für chemenu: search/types/lint/status als Tools, ohne Schreibpfad nach kb/ #19
Reference in New Issue
Block a user
Delete Branch "%!s()"
Deleting a branch is permanent. Although the deleted branch may continue to exist for a short time before it actually gets removed, it CANNOT be undone in most cases. Continue?
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):
Python-Prozess nicht.
Befund
wikitoolist ein lokales CLI ohne Netzwerkschnittstelle. Der gitea-mcp-Connector bildet nur dieGitea-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 alswikitool search.Die tragfähige Formulierung ist nicht „wikitool über MCP exponieren", sondern:
chemenubekommt einen zweiten Konsumenten. CLI und MCP-Server sind zwei Adapter auf denselben Kern.
run_search()(commands/search.py:82) undrun_lint()(commands/lint.py:77) sind bereitsweitgehend reine Funktionen; was fehlt, ist die Bibliotheksgrenze darunter — das ist #31 und
Voraussetzung hierfür.
Oberfläche
searchrun_search, inkl.--field-Prädikatentypes list/types describetypes_cmdlintrun_lintlinteinen Report nachreports/(lint.py:457-467)statuswikitool status.wiki-statusist ein Skill, derkb/index.md+lint+kb/log.mdzusammensetzt — der MCP-statusist eine neue, serverseitig komponierte Oberfläche, kein Wrapper.Explizit nicht exponiert:
new,touch,xref,cite,publish,migrate,version bump, alles unterwork/. Strukturell — die Funktionen existieren im Servercode garnicht —, 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, einGolden-Test hält beide gegeneinander.
Direktimport, nicht Subprocess
Der Iteration Budget Gate zählt nicht, was man erwartet (
run_budget.py:109):searchunddoctorsind befreit —lintundtypesnicht. Ein Subprocess-Server ohneSession-Injektion teilt sich über den
getppid()-Fallback einen Zähler und steht nach 60lint/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) wirfttyper.Exitund setzt modulglobalen Zustand. Teil von #31.WIKI_TRACEist 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 --hardsauber hält.WIKI_TRACE_DIRnach außen legen oder abschalten.Der residente Prozess ist zugleich die Performance-Lösung
Ein
wikitool searchkostet 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
CSafeLoaderaus #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:
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
searchliest den Arbeitsbaum. Ein veralteter Checkout antwortet selbstbewusst falsch — genauder Fehler, den
SOUL.mdals Kardinalfehler benennt.Jede Antwort trägt den ausgelieferten Commit (
{"commit": "<sha>", "as_of": "<iso>"}). Damitwird 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/mainalle paar Minuten braucht keineneingehenden 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
chemenuals Abhängigkeit zieht?Akzeptanzkriterien
search/types/lint/status, keine Schreib-Tools — weil die Funktionendort nicht existieren, nicht weil sie gefiltert werden
kb/,reports/oder in git schreibt--json-Formen der CLI; Golden-Test hält beide zusammengültige Credentials erreicht den Prozess nachweislich nicht
WIKI_TRACE_DIRzeigt aus dem Arbeitsbaum heraus, oder Tracing ist abgeschaltetHängt ab von: #31 (Bibliotheksgrenze), #33 (Härtung des Lesepfads).
Blockiert: #32 (Ingest-Queue). Verwandt: #35 (semantisches Backend im selben Prozess).
Read-only MCP-Frontend für wikitool (search/status/types/lint) an Perplexity Space koppelnto MCP-Leseserver für chemenu: search/types/lint/status als Tools, ohne Schreibpfad nach kb/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
tools/wiki_tools-Module direkt"tools/chemenu/. Instanz von #29.search.py, kb_state.py, type_resolver.pysearchist heute zweigeteilt:commands/search.py(CLI) undchemenu/search/(Backends, Filter, Fusion). Die Liste zeigt auf eine Struktur von vor dem Split.kb/concepts/External Ingress Model.md"wikitool searchfindet weder „External Ingress" noch „ADR-002" noch „Abydos" — der Korpus-Purge vom 2026-09-01 hat die private Infrastruktur-Hälfte entfernt.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") undENVIRONMENT.mdziehen diese Grenzeausdrü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/Lwäre das nie angefasst worden:typer.Exitsize/Ssize/Msize/MFachlich neu im Text: der Befund zum Iteration Budget Gate (
search/doctorbefreit,lint/typesnicht — ein Subprocess-Server läuft damit nach 60 Aufrufen tot), dieEmpfehlung 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/3bleibt, mit jetzt benanntem Auslöser: ein Konsument, der die Oberflächetatsächlich braucht.
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
wikitoolvollständig nach Go migriert werden sollte. ZweiMehr-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:
Prozessstart. Das stärkste gemessene Go-Argument verschwindet durch die Entscheidung, die in
diesem Issue ohnehin gefallen ist.
CSafeLoaderist verfügbar und dreimal schneller(246 → 82 ms), plus Cache — #33.
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.
qmdist entgegenkb/entities/tools/qmd.mdnicht Go oderRust, 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
ripgrepseitTag eins praktiziert. Die Korrektur der Seite läuft separat über
wiki-manage.Der Terminkonflikt gab den Ausschlag.
instructions/private-instance.mdbeziehtStack-Updates per
git merge upstream/mainunter dem Leitsatz „Take the machinery, never thecontent". 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 intools/? Nimm die Upstream-Seite" hieße: nimm eine Sprache, für die dein Checkout keine Laufzeithat. 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:
gegen einen authentifizierten Konsumenten, der sich selbst versehentlich beschädigt.
Relabel
prio/3→prio/2Der 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.
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.
Umgesetzt in 2.4.0.
tools/chemenu/mcp/.Die beiden offenen Fragen sind entschieden (Betreiber, 2026-09-02):
stdiozum Entwickeln und Testen ohne Netz,streamable-httpfür dieAuslieferung — der einzige, vor den sich die Traefik-Middleware setzen kann, weil sie ein
HTTP-Reverse-Proxy ist.
sseist über das SDK erreichbar und wird nicht angeboten: derabgelöste Remote-Transport, jetzt darauf zu bauen verschiebt den Wechsel nur.
Testsuite; getrennt würde aus einem Contract eine Versionsabsprache.
Oberfläche:
search,types,describe_type,lint,status.statusist wiebeschrieben serverseitig komponiert und kein Wrapper.
Akzeptanzkriterien:
chemenu.apiimportiert etwasunter
chemenu.commands; ein Test importiert das Servermodul in einem frischen Interpreterund prüft
sys.modules.kb/,reports/oder in git schreibt: alle fünf Toolswerden aufgerufen, danach Dateibaum (Größe und Inhalt),
HEADundgit status --porcelaingegen vorher verglichen; zusätzlich, dassreports/gar nichterst entsteht.
--json-Formen der CLI, Golden-Test hält beide zusammen. Er ruftwikitoolals Subprozess gegen denselben Baum auf, überCHEMENU_ROOT— womit er nebenbeidie Root-Auflösung von außen mitprüft. Für
searchund für beidetypes-Formen.commitundas_of.WIKI_TRACE_DIRzeigt aus dem Arbeitsbaum heraus — erzwungen: der Server verweigertden 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.
(
git fetch && git reset --hard origin/main) ininstructions/mcp-read-server.md.reset --hardist dort tragend — ein abgedrifteter Baum antwortet zwar richtig, parst aberbei jeder Anfrage neu und stempelt jede Antwort mit
null.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 (
lintliestraw/-Dateinamen,lint.py:230) war hier zu entscheiden:lintist unverändert exponiert, inklusiveuncovered_raw_files. Für diese Instanz ist dasfolgenlos — 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 searchend-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.