Nutzer-FYI: Node.js einzuführen ist eine Dimension schwerer als eine Backend-Abhängigkeit und muss dem Entwickler deutlich gemacht werden — als Regel in CLAUDE.md. Aus den nachgefragten Beweggründen folgt: kein lokaler Prozess. Der MCP-Server lebt im Backend (Streamable HTTP, Bearer-Token), führt die Frontend-Module per GraalJS aus, jedes Werkzeug hat eine text- und eine document-Form; das Node-Paket ist verworfen, seine Tool-Schicht lebt als frontend/src/inspect.js, guard.js, edit.js. Backend-Abhängigkeiten GraalJS + Spring-AI-Starter zugestimmt; der GraalJS-Spike steht vor allem anderen. RFC neu geschrieben (§12 hält die drei Runden fest), Plan-Teilbaum #ai.mcp neu, D93-Nachtrag. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
40 KiB
RFC 001 — MCP-Server: Werkbaum für KI-Agenten außerhalb des Editors
| Status | Abgestimmt (drei Runden am 2026-09-02, Entscheidungen in §11 und D93) — nichts gebaut |
| Plan-Knoten | #ai.mcp in docs/examples/werkbaum.werkbaum |
| Entscheidung | D93 in docs/DECISIONS.md |
| Berührt | Backend (de.werkbaum.integration.mcp, Security, Build), frontend/src/* (headless-Module: eine Verschiebung, drei neue Dateien), tools/pull-doc, README, .mcp.json |
| Berührt nicht | Notation (SPEC), llms.md, frontend/src/app.js im Verhalten, Deploy-Skripte außer einer Umgebungszeile |
| Neue Technologie | keine — die JVM bleibt die einzige Laufzeit (§6, Runde 3) |
1. Zusammenfassung
Werkbaum bekommt einen MCP-Server (Model Context Protocol) im
Backend: Unter /api/v1/mcp bietet die Spring-Boot-Anwendung KI-Agenten
außerhalb des Editors — Claude Code, OpenCode, Codex, IDE-Agenten,
gehostete Agenten — einen Werkbaum-Plan als Ressource (Text) und als
Werkzeuge (Baum, Warnungen, günstigster Pfad, Knoten-Verben,
Schreiben) an, über Streamable HTTP mit einem Bearer-Token.
Das Backend parst nicht selbst: Es führt die headless-Module des
Frontends (parser.js, model.js, live.js und drei neue) unverändert
in der JVM aus — per GraalJS, gebündelt beim Bauen. Damit bleibt D14
gewahrt (genau ein Parser), und ein Agent bekommt wörtlich dieselben
Warnungen, effektiven Status und Stationen, die das Diagramm zeigt.
Zwei Arten von Plänen, ein Satz Werkzeuge: Server-Dokumente bearbeiten
die Tools direkt — als Zeilen-Diff gegen Basisversion und Prüfsumme (D76),
Konflikte werden abgelehnt, jede Änderung steht in der Historie mit dem
Namen des Agenten. Lokale .werkbaum-Dateien liest und schreibt der
Agent mit seinen eigenen Werkzeugen und reicht den Text an dieselben
Tools — zur Analyse, zur Prüfung und für die Knoten-Verben, die dann Text
zurückgeben. Es gibt keinen lokalen Prozess und keine neue
Laufzeit: kein Node-Paket, nichts zu installieren beim Entwickler, nur
eine URL und ein Token in der Host-Konfiguration.
Zwei Leitplanken: keine beschädigte Notation und kein [^] von einem
Agenten — auf Server-Dokumenten als Sperre, auf Text als Prüfergebnis.
Der Git-Spiegel eines geteilten Plans ist das vorhandene
tools/pull-doc --git-commit (D88), das künftig nennt, wer welche Version
geändert hat — vom Cron oder vom Agenten selbst aufgerufen.
Das ist die Gegenrichtung zu #ai.dialog: Dort käme ein Modell in den
Editor, hier geht der Plan zu Agenten, die schon irgendwo laufen. Beides
teilt sich den Boden (llms.md, D43) und dieselben Module — die drei
neuen Dateien in frontend/src sind genau die, die #ai.dialog später
auch braucht.
2. Motivation
Die Beweggründe, wie sie in Runde 3 benannt wurden (§11): Ein oder mehrere
Entwickler arbeiten parallel an geteilten Werkbaum-Plänen und setzen
dabei agentische KI ein — vor allem, um einen Plan im Dialog zu
erstellen und den Feature-Fortschritt zu tracken. Der Agent läuft
meist lokal beim Entwickler (OpenCode, Claude Code, Codex); später soll
ein Agent auch in das Werkbaum-Frontend kommen (#ai.dialog). Die Pläne
liegen beides: als Datei neben dem Code und als geteiltes
Server-Dokument.
Heute kann ein Agent damit dreierlei tun — und alles davon nur halb:
- Eine
.werkbaum-Datei lesen und schreiben. Das geht mit Claude Code bereits,llms.mderklärt die Notation (D43). Aber der Agent muss die Semantik selbst nachrechnen: Ist der Plan warnungsfrei? Welcher Knoten ist effektiv wie weit (§4)? Was ist die nächste Station auf dem günstigsten Pfad (§9)? Für all das gibt es im Frontend geprüfte Funktionen — der Agent bekommt sie nicht zu sehen und rät. Beim Größen-Konflikt (D62) oder der Closure-Rechnung (D42) ist Raten nachweislich falsch. - Ein Server-Dokument (
?live=) anfassen. Nur percurlgegen die REST-API — mit Volltext-PUT, also ohne das Zeilen-Diff-Protokoll (D76). Ein Agent, der so schreibt, überschreibt, was ein Mensch in der Zwischenzeit getippt hat; genau der Verlust, gegen den D89 vier Netze gespannt hat. - Den Plan im Editor ändern lassen —
#ai.dialogist eine Idee ohne Entwurf.
MCP ist inzwischen die Verkehrssprache dafür: Claude Code, OpenCode, Codex, Claude Desktop, IDE-Agenten und die Anthropic-API selbst (MCP-Connector) sprechen es. Ein Server, der Werkbaum darüber anbietet, macht Punkt 1 und 2 richtig — für alle diese Hosts, nicht nur für einen.
3. Ziele und Nicht-Ziele
Ziele
- Ein Agent kann einen Plan lesen (Server-Dokument per Ressource; eine Datei liest er selbst) und den Notationsleitfaden dazu bekommen.
- Ein Agent kann einen Plan befragen, ohne die SPEC nachzuimplementieren: Baum mit allem, was das Diagramm weiß; Warnungen; günstigster Pfad samt Stationen und Personen-Last — für einen Text wie für ein Dokument.
- Ein Agent kann einen Plan ändern, ohne fremde Arbeit zu überschreiben; jede Änderung an einem Dokument ist als Agenten-Änderung erkennbar.
- Kein zweiter Parser, keine zweite Semantik (D14). Keine neue Laufzeit (D93-Nachtrag, CLAUDE.md).
- Kein Geheimnis läuft durch den Server, das er nicht braucht.
Nicht-Ziele (Abgrenzung — wird nicht gebaut, siehe §9)
- Kein LLM im Server. Der Server ruft kein Modell auf und kennt keinen
API-Schlüssel. Das Modell sitzt im Host;
#ai.keybleibt ein eigener Knoten. - Keine neue Notation. Nichts an SPEC oder
llms.mdändert sich. - Kein lokaler Prozess. Kein Node-Paket, kein stdio-Server, kein Jar-Aufruf beim Entwickler (§6).
- Kein Taiga über MCP in der ersten Fassung (§9).
- Keine Rechteverwaltung — ein Token je Installation; das Dokument-Zugriffsmodell (unerratbare UUID, D76) bleibt.
- Kein Ersatz für
#ai.dialogund kein Chat im Editor.
4. Begriffe (soweit hier nötig)
MCP trennt Hosts (die Anwendung mit dem Modell) von Servern (Prozesse oder Endpunkte, die Fähigkeiten anbieten). Ein Server bietet drei Dinge an:
- Resources — lesbare Inhalte mit URI, vom Host in den Kontext geholt („zeig mir den Plan“).
- Tools — aufrufbare Funktionen mit JSON-Schema, die das Modell selbst wählt („prüfe den Plan“, „setze den Status“).
- Prompts — vorgefertigte Anweisungen, die der Nutzer auswählt („zerlege diesen Knoten“).
Transporte: stdio (der Host startet den Server als Kindprozess) und Streamable HTTP (ein Netzwerk-Endpunkt; hat 2025 das ältere SSE-Verfahren abgelöst). MCP verlangt keine bestimmte Sprache — es gibt offizielle SDKs für TypeScript, Python, Kotlin, Java, C#; Werkbaum nimmt den Spring-AI-Starter für Java (§11). Die Protokollrevision trägt das SDK; dieses RFC legt keine fest.
5. Vorschlag
5.1 Architektur
Host beim Entwickler Werkbaum-Backend (Spring Boot, JDK 21)
(Claude Code, OpenCode, Codex, …) ┌───────────────────────────────────────┐
┌──────────────┐ Streamable HTTP + Bearer │ /api/v1/mcp │
│ Modell │◄────────────────────────────►│ de.werkbaum.integration.mcp │
│ + Werkzeuge │ (Apache reicht /api/ durch) │ ├─ resources (Guide, Dokumente) │
│ + eigene │ │ ├─ tools (inspect, check, │
│ Datei- │ │ │ write, verbs) │
│ Zugriffe │ │ └─ WerkbaumCore: GraalJS-Kontext │
└──────────────┘ │ führt werkbaum-core.mjs aus — │
│ liest/schreibt │ das Bündel aus frontend/src: │
▼ .werkbaum-Dateien selbst │ parser · model · live · │
Repo des Entwicklers │ inspect · guard · edit │
(Git-Spiegel: tools/pull-doc) │ DocumentService / LiveEditingService │
└───────────────────────────────────────┘
Es gibt einen Dienst und ein Artefakt: das Backend-Jar. Neu darin
sind ein Paket, ein Bearer-Filter und ein gebündeltes JS-Modul als
Ressource. Alles Entscheidbare kommt aus frontend/src; Kotlin ist
Transport, Zugang, Ablage und die Brücke in die JVM.
5.2 Zwei Formen je Werkzeug: Text und Dokument
Weil es keinen lokalen Prozess gibt, unterscheidet der Server nicht nach
Ablage, sondern nach Eingabeform. Jedes Werkzeug nimmt entweder
text (der Agent hat die Datei selbst gelesen) oder document (die
UUID eines Dokuments dieses Backends):
| Form | Lesen | Ergebnis eines Schreib-Werkzeugs | Leitplanken (§5.5) |
|---|---|---|---|
text |
der Agent liefert den Text mit | der neue Text samt Prüfergebnis — der Agent schreibt ihn selbst in die Datei | Prüfergebnis: findings mit blocking: true/false; der Text wird trotzdem zurückgegeben |
document |
DocumentService |
der neue Stand (version, checksum) — geschrieben per PATCH /content-Weg (D76) |
Sperre: blockierende Befunde ⇒ nicht geschrieben |
document ist eine UUID, keine URL: Der Server fasst nur seine eigenen
Dokumente an. Fremde Backends gibt es für ihn nicht — das nimmt die
SSRF-Frage aus dem Entwurf, bevor sie gestellt ist, und ein Agent, der
mit zwei Werkbaum-Servern arbeitet, konfiguriert zwei MCP-Server.
Der ?live=-Link, den Menschen weitergeben, enthält die UUID; ein Agent
bekommt sie vom Menschen oder aus der Ressourcen-Liste (§5.3).
5.3 Resources
| URI | Inhalt | MIME |
|---|---|---|
werkbaum://guide |
llms.md — der Notationsleitfaden (D43) |
text/markdown |
werkbaum://doc/<uuid> |
Notationstext des Dokuments; version und checksum als Metadaten |
text/plain |
Die Ressourcen-Liste nennt die Dokumente dieses Backends mit Titel
und UUID — für den Agenten, der fragt „welche Pläne gibt es hier?“. Das
ist dieselbe Auskunft wie GET /documents, die heute hinter dem
Master-Passwort steht (D76-Nachtrag 6), und sie steht hier hinter dem
Bearer-Token (§5.6): Wer das Token hat, ist Entwickler dieser
Installation, nicht ein Fremder mit einer erratenen Adresse. Das
Master-Passwort selbst geht nie über MCP.
Resources sind bewusst nur der Text: Er ist das führende Format (D14),
und ein Agent, der ihn im Kontext hat, kann mit llms.md schon alles
lesen. Die Semantik kommt aus den Tools.
5.4 Tools
Wenige Verben, sprechend benannt; jedes nimmt text oder document
(§5.2), die Dokument-Form beim Schreiben zusätzlich base
({version, checksum} vom letzten Lesen — Pflicht):
| Tool | Eingabe | Ausgabe | Nutzt |
|---|---|---|---|
werkbaum_read |
document |
Text, version, checksum, Zeilenzahl |
DocumentService |
werkbaum_inspect |
text|document, optional id (Teilbaum), include: [tree, warnings, path] |
JSON: Baum je Knoten mit id, label, line, gate, optional, status, effectiveStatus, size, assumedSize, tags, deps, desc, taigaSlug, ticketRef; Warnungen als {type, line, …} plus englischer Klartext; Pfad als nötige Knoten, Stationen in Dokumentreihenfolge, assigneeLoads, overloadedAssignee, exact (D42) |
parse, effectiveStatus, computeCheapPlan, assigneeLoads, taigaSlugs, warningText; inspect.js |
werkbaum_check |
text|document, optional against (Vorher-Text) |
findings: Warnungs-Delta und Leitplanken-Befunde mit blocking — das, was ein Schreib-Werkzeug prüfen würde, als Auskunft. Für Agenten, die eine Datei gleich selbst schreiben |
guard.js |
werkbaum_set_status |
…, id, status (Code der §4-Tabelle) |
Text-Form: neuer Text + findings; Dokument-Form: neuer Stand + findings |
setStatusBox (parser.js) |
werkbaum_apply_ops |
…, ops[] (insert/replace/delete mit index, count, lines — dasselbe Schema wie LineOperation der API) |
wie oben | applyOps (live.js) |
werkbaum_write |
…, newText (Volltext) |
wie oben; Dokument-Form: Diff per computeOps |
computeOps (live.js) |
werkbaum_add_node |
…, parent (ID), line (Knotenzeile ohne Einrückung), optional after (Geschwister-ID) |
wie oben | addNodeLine (edit.js) |
werkbaum_move_node |
…, id, parent, optional after |
wie oben; Teilbaum samt Beschreibungs- und Fortsetzungszeilen wandert mit, Einrückung wird angepasst | moveSubtree (edit.js) |
werkbaum_set_size |
…, id, size (XS…XXL oder null) |
wie oben | setSizeToken (edit.js) |
werkbaum_remove_node |
…, id, discard: true|false |
wie oben — discard setzt [-] (§4), false löscht den Teilbaum samt Blöcken |
setStatusBox bzw. removeSubtree (edit.js) |
Die Knoten-Verben (Runde 1, §11) sind Bequemlichkeit für das Modell:
apply_ops könnte jedes davon ausdrücken, aber ein Agent, der
Zeilenindizes ausrechnet, verrechnet sich — ein Verb mit ID ist die
robustere Schnittstelle. Damit dabei keine zweite Stelle mit
Zeilenformat-Wissen entsteht, liegen die Text→Text-Regeln als
frontend/src/edit.js neben setStatusBox/setFoldMark (parser.js) und
appendToken (taiga.js) — headless, getestet, und für #ai.dialog schon
da (§7.1). Kotlin verdrahtet sie nur.
Warnungen tragen Typ und Text. Der Typ (sizeConflict, unknownDep,
…) ist für den Agenten das Verlässliche; der englische Text kommt dazu,
weil das Modell ihn ohne Nachschlagen versteht. Die Texte sind die
Editor-Texte (warningText, D33-Nachtrag) in Englisch — nicht
nacherzählt (§7.1).
5.5 Leitplanken
Auf jedem Schreib-Werkzeug, vor dem Schreiben (Dokument) bzw. als Befund (Text):
- Keine beschädigte Notation. Der Ergebnis-Text wird geparst; trägt
er Struktur-Warnungen, die der Ausgangstext nicht hatte —
mixedGate,unknownStatus,descStray,duplicateId—, ist das blockierend: Ein Dokument wird nicht geschrieben, die Antwort nennt sie; ein Text kommt mitblocking: truezurück. Das sind die Warnungen, bei denen der Parser eine Zeile anders liest, als sie gemeint war. SchalterallowWarnings: trueje Aufruf erlaubt es ausdrücklich. Inhaltliche Widersprüche —sizeConflict,assigneeOverload,xorConflict,unknownDep,unknownDesc— werden geschrieben und gemeldet: Ein Agent arbeitet in Zwischenschritten (das Ziel einer Abhängigkeit entsteht oft erst im nächsten Zug), und im Diagramm sieht ein Mensch sie als bernsteinfarbene Marke — wie bei einer eigenen Änderung (Runde 1; die strengere Fassung war der Vorschlag). Verschwindende Warnungen sind immer erlaubt, jede Antwort trägt das Warnungs-Delta. - Kein
[^]von einem Agenten.in Produktionist die Aussage eines Deploys (D30), nicht die eines Modells. Blockierend wie oben; SchalterallowProd: trueje Aufruf — ausdrücklich, nie still. - Konflikt ist ein Fehler, keine Entscheidung. Bei veralteter
base(Dokument) kommt der aktuelle Text mit zurück; der Agent — und im Zweifel der Mensch hinter ihm — entscheidet. Der Server rebased nichts über das hinaus, was derLiveEditingServiceohnehin tut (D76: nicht überlappende Ops verschiebt er; das bleibt). Für Dateien gilt dasselbe auf der Seite des Agenten: Er hat den Text gelesen und schreibt ihn selbst —werkbaum_checkmitagainstsagt ihm vorher, ob sein Ergebnis Befunde hat.
Der Preis der Text-Form ist benannt und war Teil der Entscheidung
(Runde 3): Bei Dateien ist die Leitplanke Auskunft, keine Sperre. Ein
Agent, der blocking: true ignoriert und die Datei trotzdem schreibt,
hat den Fehler dann wenigstens im Transkript stehen — und der nächste
inspect meldet ihn wieder.
5.6 Zugang und Identität
Zugang: ein statisches Bearer-Token je Installation
(WERKBAUM_MCP_TOKEN, in der Umgebungsdatei des Dienstes wie das
Master-Passwort, D77), geprüft von einem Spring-Security-Filter für
/api/v1/mcp/**; ohne Token 401. Hosts geben es als Header mit
(Claude Code: claude mcp add --transport http werkbaum <url> --header "Authorization: Bearer …"; der API-MCP-Connector: authorization_token).
Das Token reist über HTTPS in der Host-Konfiguration, nie im Transkript.
OAuth 2.1, das die MCP-Spezifikation für HTTP vorsieht, kommt, sobald
es einen Identity Provider gibt — die Taiga-Instanz wechselt auf OIDC
(D91-Nachtrag 1), das wäre der naheliegende Anker; ohne einen
Authorization Server wäre OAuth ein Bau weit über den Anlass hinaus
(Runde 2). Ohne konfiguriertes Token ist der Endpunkt gesperrt, nicht
offen — dieselbe Voreinstellung wie beim Master-Passwort
(D76-Nachtrag 6), und GET /info meldet mcp: false.
Identität: Jeder Schreibvorgang an ein Dokument geht mit clientId
mcp-<sitzung> (laufende seq je Sitzung, D76-Nachtrag 7) und einem
displayName, der den Host nennt — den Namen aus dem MCP-Handshake
(clientInfo.name, etwa „Claude Code“), Rückfall „Agent“ (Runde 1). In
jedem offenen Editor steht damit „geändert von: Claude Code“ (D86); der
Name bleibt eine Behauptung, kein Nachweis — wie beim Anzeigenamen eines
Menschen. Das Token ist zugleich die Schicht, über die später das
Owner-Passwort (#col.live.owner) kommt: je Dokument eine Zuordnung
in der Umgebung des Dienstes, nie als Tool-Parameter.
5.7 Geteiltes Dokument und Git — beides zugleich
Ein Agent soll auf dem geteilten Dokument arbeiten (dort sitzen die Menschen, dort greift das Konfliktprotokoll), und derselbe Plan soll unter Git-Kontrolle stehen (Diff, Blame, Review, ein Netz außerhalb des Servers — D89). Die Antwort ist eine Richtung: Der Server ist die Quelle, Git ist das Archiv.
Der Weg dafür existiert: tools/pull-doc --git-commit (D88,
#col.git.pull) holt ein Server-Dokument in ein Worktree und committet es
datiert, nur bei Änderung. Zwei Aufrufer:
- Der Cron auf dem Server-Host oder beim Entwickler — archiviert alle Änderungen, egal von wem.
- Der Agent selbst, nach seinem Schreibvorgang: Er läuft lokal beim
Entwickler und hat eine Shell;
pull-doc --git-commit --with-history <uuid> <datei>ist ein Befehl wie jeder andere. Ohne lokalen MCP-Prozess (Runde 3) ist das der Weg, den Spiegel sofort nach der eigenen Änderung zu ziehen; der Cron fängt den Rest. Das Promptwerkbaum/decompose(§5.8) endet mit genau diesem Hinweis.
Wer steht im Commit? Ein Spiegel-Commit enthält den ganzen
Serverstand — die Zeilen des Agenten und alles, was Menschen seit dem
letzten Commit geändert haben. Die Form der Urheber-Angabe entscheidet,
ob git blame später lügt:
| Form | Aussage | Preis |
|---|---|---|
a) git commit --author="Claude Code <…>" |
Der Agent ist Git-Autor — blame schreibt ihm jede Zeile darin zu, auch die, die Anna getippt hat |
Falsche Blame-Auskunft; Git verlangt eine E-Mail-Adresse, die es nicht gibt |
b) Co-Authored-By:-Trailer |
Die Repo-Konvention für Claude-Commits: „hat beigetragen“, ohne Autor zu sein | Braucht ebenfalls Name <email>; für Menschen aus der Server-Historie gibt es nur den Anzeigenamen |
c) Freitext im Betreff (… (Version 8, via Claude Code)) |
Lesbar im git log --oneline |
Nicht maschinenlesbar; nennt nur den Auslöser |
| d) Historie im Rumpf + eigene Trailer | Der Rumpf listet aus GET /history, wer welche Version seit dem letzten Spiegel-Commit geändert hat („v7 Anna · v8 Claude Code“); je Beteiligtem Werkbaum-Changed-By: <Anzeigename>, dazu Werkbaum-Version: 8 |
Ein zweiter GET je Commit; eigene Trailer statt der GitHub-Konvention |
Entschieden (Runde 2): d, mit c im Betreff. Git-Autor bleibt, wer den
Prozess betreibt (buchstäblich richtig — er hat committet), die
Beteiligten stehen mit Namen im Rumpf, maschinenlesbar per
git interpret-trailers, und Werkbaum-Version: gibt pull-doc beim
nächsten Lauf die Untergrenze für „seit dem letzten Commit“. Der
Cron-Commit wird damit im selben Zug ehrlicher. Der Schalter
(--with-history) kommt ins Skript (D77-Nachtrag). Die Anzeigenamen
bleiben Behauptungen (D86) — deshalb „Changed-By“, nicht „Author“.
5.8 Prompts (optional, Phase 5)
Zwei vorgefertigte Anweisungen, die ein Host dem Nutzer anbietet:
werkbaum/review— „Prüfe diesen Plan: Warnungen, Größen, fehlende Zerlegung ab M, unklare Zuständigkeiten“ (nutztinspect, schreibt nichts).werkbaum/decompose— „Zerlege Knoten#idin Teilpakete mit Größen“ (endet in den Verben, mit den Leitplanken; bei einem Dokument mit dem Hinweis aufpull-doc).
Beides sind Texte, keine Logik; sie leben im Backend-Paket und nicht in
llms.md (das bleibt Notation, D43).
5.9 Konfiguration
Serverseitig (Umgebungsdatei des Dienstes, D77):
WERKBAUM_MCP_TOKEN— das Bearer-Token; fehlt es, ist der Endpunkt gesperrt.- Kein weiterer Schalter: Es gibt keine Dateiwurzeln (kein Dateizugriff), keinen Spiegel (§5.7 läuft beim Aufrufer), keine Fremdserver (§5.2).
Beim Entwickler: die URL und das Token in der Host-Konfiguration. Im
Repository liegt eine .mcp.json, die auf ${WERKBAUM_MCP_URL} und
${WERKBAUM_MCP_TOKEN} aus der Umgebung zeigt — damit Claude Code hier
den Server von selbst hat, gegen die stabile Instanz oder gegen ein
lokal gestartetes Backend (./gradlew bootRun; der Entwickler-Rechner
hat das JDK ohnehin). Der Plan docs/examples/werkbaum.werkbaum ist damit
das erste Dokument, an dem der Server benutzt wird (Dogfooding, D27).
5.10 Die JVM führt die JS-Module aus
Der Kern der Entscheidung (Runden 2 und 3): Das Backend braucht die
Werkbaum-Logik, darf sie nicht nachbauen (D14) und soll keine zweite
Laufzeit neben sich haben. Also führt es die Module selbst aus —
dieselbe Technik, die der Plan für das IDE-Plugin vorsieht
(#idea.drift.js: „den einen JS-Parser im IDE laufen lassen“).
- GraalJS (
org.graalvm.polyglot:polyglot+js-community) läuft auf dem Stock-OpenJDK 21 der Zielumgebung (D77) im Interpreter-Modus — ohne Graal-Compiler langsamer, aber ein Plan hat Dutzende Kilobyte, keine Megabyte; zu messen, nicht anzunehmen. - Gebündelt beim Bauen: Gradle ruft das vorhandene Frontend-Tooling
(esbuild, das Vite mitbringt) und bündelt
frontend/src/inspect.jssamt allem, was es importiert, zu einem ES-Modulwerkbaum-core.mjsin den Jar-Ressourcen. So braucht GraalJS keine Import-Auflösung und kein virtuelles Dateisystem; Node bleibt, was es ist — Build-Werkzeug, nie Laufzeit. Der Build bricht, wenn das Bündel fehlt: dieselbe Zusage wie bei der OpenAPI-Generierung. - Was in der JVM anders ist:
crypto.subtle(die Prüfsumme inlive.js) gibt es in GraalJS nicht — die Prüfsumme rechnet Kotlin (MessageDigest),checksum()wird dort nicht aufgerufen; alles übrige (TextEncoder,Map,Set, Unicode-Regex) ist ECMAScript und vorhanden. Ein Polyglot-Contextist nicht nebenläufig: ein kleiner Pool von Kontexten mit gecachterSource, je Aufruf einer — die Kosten je Kontext (Speicher, Aufbauzeit) sind Teil des Spikes. - Der Spike vor Phase 1 misst: Laufen
parser.js,model.js,live.js,inspect.js,guard.js,edit.jsals Bündel in GraalJS und liefern für den mitgelieferten Plan dieselben Zahlen wie Vitest (231 Knoten, 0 Warnungen, Stationen)? Was kostet ein Kontext an RSS auf der Zielumgebung (D76-Nachtrag 3: rund 300 MB frei)? Reicht der Apache den SSE-Strom langer Werkzeugaufrufe ungepuffert durch (Long Polling ist gemessen, D76-Nachtrag 2 — ein Strom nicht)? Die Ergebnisse gehören als Nachtrag zu D93. Fällt der Spike durch, ist die Antwort nicht Node, sondern eine neue Frage an den Entwickler (CLAUDE.md-Regel).
6. Alternativen
A — Kotlin-Server mit eigenem Parser
Pro: keine JS-Ausführung in der JVM.
Contra: genau die zweite Grammatik, die D14 verbietet — und die
Drift-Frage, die #idea.drift für das IDE-Plugin schon stellt.
Verworfen (#ai.mcp.kotlin als [-]).
B — Node-Paket, das die Frontend-Module importiert (Entwurf der Runden 1–2)
Pro: ein Parser; stdio ist der Transport, den alle lokalen Hosts
können; Dateien und Git-Spiegel direkt am Prozess.
Contra: Node.js würde Laufzeit — beim Entwickler als Paket, auf dem
Server als zweiter Dienst. Bisher ist Node im Repo Build-Werkzeug. Runde 3
hat das als das erkannt, was es ist: die Einführung einer Technologie,
eine Dimension schwerer als eine Abhängigkeit (CLAUDE.md, D93-Nachtrag).
Und die Beweggründe brauchten es nicht: Der Text darf zum eigenen Server.
Verworfen (#ai.mcp.node als [-]); die Tool-Schicht, die dafür
geplant war, wandert unverändert nach frontend/src.
C — Das Backend-Jar als lokaler stdio-Server (java -jar … --mcp-stdio)
Pro: keine neue Laufzeit, Dateien und Spiegel lokal. Contra: verlangt ein JDK beim Entwickler und startet in Sekunden; und sobald der Text ohnehin zum Server darf, ist der lokale Prozess ohne Anlass. Nicht gewählt, bleibt als Rückfall benannt, falls die Text-Form sich als unpraktisch erweist — sie kostete dann keinen Technologie-Wechsel.
D — Nur im Backend, Text-Form für Dateien (gewählt)
Pro: eine Laufzeit, ein Artefakt, ein Deployment; die vorhandene Zugangsschicht; nichts zu installieren beim Entwickler; die Module bleiben einmal vorhanden. Contra: Netz auch für lokale Dateien; die Leitplanken sind dort Auskunft statt Sperre; der GraalJS-Spike steht vor allem anderen. Gewählt (Runde 3).
E — Nichts bauen: Agenten lesen llms.md und editieren Dateien
Pro: funktioniert heute mit Claude Code.
Contra: keine Semantik, keine Server-Dokumente außer per curl,
Schreiben ohne Konfliktschutz — der Ausgangszustand aus §2.
Bleibt als Rückfall bestehen — der Server ersetzt llms.md nicht.
F — OpenAPI der REST-API den Agenten geben
Contra: dieselbe Semantik-Lücke wie E, und die Prüfsumme müsste das Modell selbst rechnen. Verworfen.
G — #ai.dialog zuerst (Modell im Editor)
Nicht konkurrierend — D liefert die Module, die G später braucht.
7. Impact auf den bestehenden Code
7.1 frontend/src/ — eine Verschiebung, drei neue Dateien
| Datei | Was | Warum |
|---|---|---|
app.js → neu i18n.js |
Das I18N-Objekt (ab app.js:3287) in ein headless-Modul verschieben; app.js importiert es. |
Der Server braucht warningText(w, t) mit englischem t. Eine Kopie im Backend wäre die Drift, vor der D33-Nachtrag warnt. Reine Verschiebung; der Vite-Bundle bleibt gleich. |
neu inspect.js |
Baum → JSON (id, label, line, status, effectiveStatus, assumedSize, …), Warnungen → {type, line, text}, Pfad → Stationen und Lasten. Das Einstiegsmodul des Bündels. |
Die Sicht für Agenten — und für #ai.dialog. Headless, Vitest-getestet gegen SPEC §10 und den mitgelieferten Plan. |
neu guard.js |
Leitplanken (§5.5): Warnungs-Delta nach Klasse (strukturell/inhaltlich), [^]-Befund; Text → findings. |
Eine Stelle, die weiß, was blockiert. |
neu edit.js |
Text→Text-Regeln der Knoten-Verben: addNodeLine, moveSubtree, setSizeToken, removeSubtree — zeilenweise, zeichengenau, mit Fortsetzungs- und Beschreibungszeilen (§1), Kommentare bleiben stehen. |
Die eine Stelle für „Zeile eines Knotens umbauen“; bewusst nicht in parser.js (Sammelbecken). |
parser.js, model.js, live.js, taiga.js, warnings.js, render.js |
unverändert | Alles Nötige ist exportiert; render.js ist headless bis zum Aufruf (esc). |
Die drei neuen Dateien sind reine ES-Module ohne Browser- und ohne Node-APIs — die Bedingung dafür, dass sie in GraalJS laufen, und ohnehin die Hausregel (D54-Nachtrag 3).
7.2 Backend
| Ort | Was |
|---|---|
build.gradle.kts |
Abhängigkeiten spring-ai-starter-mcp-server-webmvc und org.graalvm.polyglot:polyglot + js-community (Runde 3, Rückfrage gestellt und beantwortet — die Boot-4-Verträglichkeit des Starters ist beim Bauen zu prüfen; fällt sie durch, das MCP-Java-SDK direkt mit seinem WebMVC-Transport). Eine Gradle-Task bundleWerkbaumCore, die per npx esbuild --bundle --format=esm aus frontend/src/inspect.js die Ressource werkbaum-core.mjs erzeugt; processResources hängt davon ab. |
neu de.werkbaum.integration.mcp |
WerkbaumCore (GraalJS-Kontext-Pool, gecachte Source, Aufrufe inspect/check/edit als Kotlin-Funktionen mit JSON hinein und heraus), McpServerConfiguration (Resources, Tools, Prompts am Starter registriert), McpTools (die Verben aus §5.4, je mit Text- und Dokument-Form; Dokument-Form über DocumentService/LiveEditingService — derselbe Weg wie PATCH /content, mit Prüfsumme aus MessageDigest). |
api/SecurityConfiguration.kt |
Bearer-Filter für /api/v1/mcp/** gegen werkbaum.mcp.token; ohne Wert denyAll (D76-Nachtrag 6). |
api/DocumentsController.kt (getInfo) |
mcp: true/false |
openapi/api.yaml |
ServiceInfo.mcp; der MCP-Endpunkt selbst steht nicht in der OpenAPI — er spricht JSON-RPC, nicht REST, und der Starter trägt seinen Vertrag selbst (dieselbe Grenze wie beim Long-Polling-Endpunkt, D76-Nachtrag 5: was der Generator nicht ausdrücken kann, bleibt draußen — hier ohne Verlust, denn die Tool-Schemata prüft das SDK) |
application.yaml |
werkbaum.mcp.token: ${WERKBAUM_MCP_TOKEN:} |
scripts/deploy-backend.sh |
zieht WERKBAUM_MCP_TOKEN aus der .env idempotent in die Server-Umgebung — dieselbe Zeile wie für TAIGA_API_URL (D91-Nachtrag 4) |
scripts/prod.htaccess |
unverändert — /api/ ist proxied; der SSE-Strom ist zu messen (§5.10) |
| Tests | WerkbaumCoreTest (Bündel läuft, dieselben Zahlen wie Vitest), Cucumber-Szenarien über einen MCP-Client gegen den laufenden Kontext (Token fehlt ⇒ 401; set_status auf Dokument mit veralteter base ⇒ Konflikt; [^] ⇒ abgelehnt; sizeConflict ⇒ geschrieben und gemeldet) |
Perspektivisch berührt: #col.live.owner — das Owner-Passwort kommt
über die Umgebung des Dienstes (§5.6), die Endpunkte sind so zu schneiden,
dass die Prüfung dazukommt, ohne die Signatur zu brechen (backend/CLAUDE.md).
7.3 Übriges
| Ort | Änderung |
|---|---|
tools/pull-doc |
--with-history (§5.7): holt GET /history, listet im Rumpf, wer welche Version seit dem letzten Spiegel-Commit geändert hat, setzt die Trailer Werkbaum-Changed-By: und Werkbaum-Version:; der Schalter kommt ins Skript, nicht in den Aufrufer (D77-Nachtrag) |
.mcp.json (Repo-Wurzel) |
HTTP-Server-Eintrag mit URL und Token aus der Umgebung (Dogfooding) |
README.md / README.de.md |
Abschnitt „Für KI-Agenten: der MCP-Server“ — URL, Token, die zwei Formen, die Leitplanken, pull-doc |
frontend/public/llms.txt |
ein Link auf den Endpunkt (Wegweiser, D43-Nachtrag 2); llms.md unverändert |
docs/CHANGELOG.md |
je Phase eine Zeile |
tools/remote |
unverändert — es ist derselbe Dienst |
frontend/package.json |
keine neue Abhängigkeit: esbuild bringt Vite mit |
8. Sicherheit
- Plan-Text ist Fremddaten. Beschreibungen (
----Teil) können Anweisungen enthalten („ignoriere die Leitplanken“). Der Server führt nichts aus, was im Text steht — GraalJS läuft ohne Host-Zugriff (keinHostAccess, kein Dateisystem, kein Netz im Kontext); der Text ist Eingabe einer reinen Funktion. Resources sind als Inhalt deklariert, nie als Anweisung. Der Host trägt die Prompt-Injection-Frage — der Server macht sie nur nicht schlimmer. - Kein Geheimnis im Transkript. Master-Passwort und Taiga-Token gehen nie über MCP; das Bearer-Token steht in der Host-Konfiguration. Ein späteres Owner-Passwort kommt als Umgebungsvariable (§5.6).
- Kein Dateizugriff, keine Fremdserver. Der Server liest keine Pfade
und ruft keine URLs —
documentist eine UUID des eigenen Backends (§5.2). Die einzige Netzverbindung des MCP-Endpunkts ist der Aufrufer. - Rechenkosten gedeckelt: Textgröße je Aufruf begrenzt (dasselbe
Limit wie
PATCH /content, D76), Kontext-Pool begrenzt, ein Werkzeugaufruf mit Timeout — ein hängender Kontext wird verworfen, nicht wiederverwendet. - Schreiben nur mit
baseauf Dokumenten; kein Volltext-PUT. Was der Editor an Netzen hat (Konflikt-Band, Rettungs-Sicherung, D89), hat der Agent in Form des zurückgelieferten aktuellen Textes.
9. Abgrenzung — was ausdrücklich nicht gebaut wird
| Nicht gebaut | Warum | Wo es hingehört |
|---|---|---|
| Modellaufrufe, API-Schlüssel, Provider-Wahl | Der Host hat das Modell; ein Server mit eigenem Schlüssel wäre #ai.key unter falschem Namen |
#ai.key, #ai.dialog |
| Ein lokaler MCP-Prozess (Node-Paket oder Jar-Modus) | Neue Laufzeit bzw. ohne Anlass, sobald der Text zum Server darf (Runde 3) | #ai.mcp.node ([-]); Jar-Modus als benannter Rückfall (§6 C) |
| Dateizugriff des Servers | kein Nutzer-Dateisystem am Dienst; ein Pfad über HTTP wäre die Einladung, die §8 verbietet | der Agent liest und schreibt selbst |
| Fremde Backends per URL | SSRF; ein Server, ein Dokumentenbestand | zwei MCP-Server konfigurieren |
| Taiga-Tools (Ticket anlegen, Status schreiben) | Braucht das Taiga-Token — das lebt im Browser (D91) und dürfte nicht durch ein Transkript; außerdem eine eigene Entscheidung, ob ein Modell Tickets anlegen darf | eigener Nachtrag: über den vorhandenen Proxy, Token aus der Umgebung |
| Anlegen/Löschen/Umbenennen von Dokumenten | Verwaltung; wartet auf #col.live.owner |
nach dem Owner-Passwort |
Falten (fold/unfold) als Verb |
Faltmarken sind Darstellung für Menschen (§9); ein Agent liest den ganzen Text | — |
| Änderungsfeed / Long Polling über MCP | Ein Agent arbeitet in Zügen; read vor jedem Schreiben genügt |
— |
| Git als Quelle (Server folgt dem Repo) | Umkehrung von §5.7; Konfliktmarker mitten in der Notation | #col.git.pr, #col.git.auto |
| OAuth 2.1 | kein Identity Provider; Bearer-Token bis dahin | #ai.mcp.auth, wenn ein IdP da ist |
| Ein Kotlin-Parser | D14 | #ai.mcp.kotlin ([-]) |
10. Umsetzungsreihenfolge
Jede Phase ist für sich abgeschlossen und bekommt ihre CHANGELOG-Zeile;
Plan-Knoten gehen beim Mergen auf [x].
- Spike (
#ai.mcp.spike): GraalJS-Bündel läuft in der JVM, dieselben Zahlen wie Vitest für den mitgelieferten Plan; Speicher je Kontext auf der Zielumgebung gemessen (RSS vorher/nachher, wie in D77); SSE durch den Apache mit einem absichtlich langen Werkzeugaufruf gemessen (wie D76-Nachtrag 2). Ergebnis als D93-Nachtrag. Erst danach wird gebaut. - Lesen und befragen (
#ai.mcp.core,#ai.mcp.read,#ai.mcp.inspect,#ai.mcp.auth):i18n.js,inspect.js, das Bündel im Jar,WerkbaumCore, Bearer-Filter, Resources,read,inspect,checkin beiden Formen. Nachweis: Claude Code liest über die.mcp.jsonden mitgelieferten Plan und bekommt 231 Knoten, 0 Warnungen, dieselben Stationen wie der Editor; ohne Token 401. - Schreiben (
#ai.mcp.write,#ai.mcp.guard):guard.js, die drei Basis-Schreib-Werkzeuge in beiden Formen. Nachweis gegen das lokale Backend mit offenem Editor: Agenten-Änderung erscheint mit „Claude Code“ in der Historie; veraltetebase⇒ Fehler mit aktuellem Text, nichts überschrieben;[^]abgelehnt;mixedGateabgelehnt,sizeConflictgeschrieben und gemeldet; Text-Form liefertblocking: trueund trotzdem den Text. - Knoten-Verben (
#ai.mcp.verbs):edit.jsmit Tests (Gegenprobe per Mutation wie beisetFoldMark), dann die vier Verben. Nachweis:move_nodenimmt Beschreibungsblock und Fortsetzungszeilen mit und lässt den Rest zeichengenau stehen. - Spiegel (
#ai.mcp.mirror):pull-doc --with-history. Nachweis in einem Wegwerf-Worktree: ein Agenten-Zug ergibt genau einen Commit mit Serverstand, Beteiligten im Rumpf und beiden Trailern; ein unveränderter Zug keinen. - Prompts (§5.8).
11. Entscheidungen
Drei Multiple-Choice-Runden am 2026-09-02, jede Frage mit der Option „zunächst offen halten“ — keine wurde gewählt.
Runde 1
| Frage | Entschieden | Anmerkung |
|---|---|---|
| Strenge der Warnungs-Leitplanke (§5.5) | Nur Struktur-Warnungen blockieren | Vorgeschlagen war die strenge Fassung; der Nutzer hat die Zwischenschritte eines Agenten höher gewichtet |
displayName (§5.6) |
Host-Name aus dem Handshake | Rückfall „Agent“ |
| Geteiltes Dokument und Git (§5.7) | Beides: Server ist Quelle, Git Archiv über pull-doc |
Nutzer-Idee, mitten in der Ausarbeitung |
| Schreib-Tools (§5.4) | Zusätzlich Knoten-Verben | Regeln in frontend/src/edit.js |
| Verteilung | Nur aus dem Repo | .mcp.json; npm-Frage entfällt mit Runde 3 ohnehin |
| SDK-Abhängigkeit für das Node-Paket | Ja | überholt durch Runde 3 (kein Node-Paket) |
| Ort der Baum-Serialisierung | im MCP-Paket | überholt: es gibt kein Paket; sie liegt als frontend/src/inspect.js — dort, wo #ai.dialog sie braucht |
Runde 2 (Nutzer: Architektur-Entscheidungen jetzt diskutieren)
| Frage | Entschieden | Anmerkung |
|---|---|---|
| Urheber im Spiegel-Commit (§5.7) | Historie im Rumpf + Trailer | Werkbaum-Changed-By:, Werkbaum-Version:; Git-Autor bleibt der Betreiber |
| HTTP: Ort | Im Backend, das die JS-Module per GraalJS ausführt | Die Frage „Kotlin kann das nicht?“ hat den Entwurf korrigiert: MCP braucht kein Node, nur der Parser erzwang es |
| HTTP: Zugang (§5.6) | Statisches Bearer-Token | OAuth 2.1 erst mit einem IdP |
| HTTP: Umfang | Nur Dokumente | kein Dateizugriff, kein Spiegel am Dienst |
Runde 3 (Nutzer: neue Technologien sind eine Dimension schwerer als Abhängigkeiten; erst die Beweggründe, dann die Entscheidung)
| Beweggrund | Antwort |
|---|---|
| Für wen? | Entwickler, die parallel an geteilten Plänen arbeiten und agentische KI einsetzen — Pläne im Dialog erstellen, Fortschritt tracken; der Agent läuft meist lokal (OpenCode, Claude Code, Codex); später ein Agent im Frontend |
| Welche Pläne? | Beides: .werkbaum-Dateien im Repo und geteilte Server-Dokumente |
| Darf der Text eines lokalen Plans zur Analyse an den eigenen Server? | Ja |
| Startzeit eines lokalen Prozesses? | Egal |
| Entscheidung | Entschieden | Anmerkung |
|---|---|---|
| Node als Laufzeit? | Nein — nur im Backend, Text-Form für Dateien (§5.2) | Alternative C (Jar als stdio) als Rückfall benannt |
| Backend-Abhängigkeiten | GraalJS + Spring-AI-Starter (spring-ai-starter-mcp-server-webmvc) |
Boot-4-Verträglichkeit beim Spike prüfen; Rückfall das MCP-Java-SDK direkt |
Weiterhin offen — Messungen und Kleinigkeiten, die sich erst beim Bauen stellen: der Spike (§5.10, Phase 0) mit seinen drei Messungen; die Prompts (§5.8).
12. Revisionsgeschichte
- Runde 1 — Node-Paket
mcp/mit stdio, Dateien und Server-Dokumente als Ablagen; SDK, Leitplanken, Verben, Spiegel entschieden. - Runde 2 — Urheber im Commit; Streamable HTTP im Backend per GraalJS statt als zweiter Node-Dienst — der Entwurf hatte angenommen, MCP erzwinge Node.
- Runde 3 — Der Nutzer benennt Node als neue Technologie, nicht
als Abhängigkeit (CLAUDE.md-Regel, D93-Nachtrag). Aus den Beweggründen
folgt: kein lokaler Prozess, alles im Backend, Text-Form für Dateien.
Das Node-Paket ist verworfen; seine Tool-Schicht lebt als
frontend/src/inspect.js,guard.js,edit.jsweiter — unverändert im Zuschnitt, nur ohne eigenes Paket.