From 1f186eddce9095d3bb18130fd2676502609f91fc Mon Sep 17 00:00:00 2001 From: mhoennig Date: Wed, 2 Sep 2026 10:04:11 +0200 Subject: [PATCH] =?UTF-8?q?docs(rfc):=20MCP-Server=20nur=20im=20Backend,?= =?UTF-8?q?=20kein=20Node=20als=20Laufzeit=20=E2=80=94=20dritte=20Runde;?= =?UTF-8?q?=20Hausregel:=20neue=20Technologie=20braucht=20ausdr=C3=BCcklic?= =?UTF-8?q?he=20Zustimmung?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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 --- CLAUDE.md | 7 + docs/CHANGELOG.md | 2 +- docs/DECISIONS.md | 56 +- docs/examples/werkbaum.werkbaum | 101 ++-- docs/rfc/001-mcp-server.md | 969 +++++++++++++++----------------- 5 files changed, 568 insertions(+), 567 deletions(-) diff --git a/CLAUDE.md b/CLAUDE.md index 79228b3..e9df8c9 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -36,6 +36,13 @@ Integrations-Backend. `I18N`-Objekt (frontend/index.html) anlegen, dann in alle Sprachen übersetzen. - Keine neuen Laufzeit-Abhängigkeiten ohne Rückfrage (gilt in beiden Teilen). +- **Eine neue Technologie ist eine Dimension schwerer als eine neue + Abhängigkeit** — Node.js als *Laufzeit* (auf dem Server oder als eigenes + Paket; bisher ist es nur Build-Werkzeug), eine weitere Sprache, ein + weiteres Laufzeitsystem. Das wird dem Entwickler **deutlich gemacht** und + seine **Zustimmung eingeholt**, als eigene benannte Entscheidung mit den + Alternativen innerhalb der vorhandenen Technologien (JVM, Vanilla JS im + Browser) — nie als Nebensatz eines Vorschlags (D93-Nachtrag). - Der Notationstext ist das führende Datenformat; kein Teil erfindet ein eigenes Speicherformat für die Struktur. - Im mitgelieferten Plan `docs/examples/werkbaum.werkbaum` bekommt **jeder** diff --git a/docs/CHANGELOG.md b/docs/CHANGELOG.md index 4b15fb1..57a8d94 100644 --- a/docs/CHANGELOG.md +++ b/docs/CHANGELOG.md @@ -19,7 +19,7 @@ reverse. ## 2026-09-02 -- An MCP server for AI agents recorded as planned: a Node package that reuses the one JS parser, hands the plan to Claude Code and other agents as resource and tools, writes changes as conflict-safe line diffs, and mirrors the shared document into git after every agent write — spelled out in an RFC before it is built +- An MCP server for AI agents recorded as planned: it lives in the backend, runs the one JS parser through GraalJS, hands plans to Claude Code and other agents as resources and tools over streamable HTTP, and writes changes as conflict-safe line diffs — no new runtime, spelled out in an RFC before it is built ## 2026-08-28 diff --git a/docs/DECISIONS.md b/docs/DECISIONS.md index ae6c989..da8c9ab 100644 --- a/docs/DECISIONS.md +++ b/docs/DECISIONS.md @@ -8765,6 +8765,58 @@ auch wenn sie erst spät gebaut werden — RFC §5.7 und §5.10): keinen Git-Spiegel; eine echte Teilmenge der lokalen Fassung mit denselben Verben. -Offen bleiben nur noch Messungen (GraalJS-Spike, SSE durch den Apache) -und die Prompts. Die Umsetzungsreihenfolge steht im RFC; gebaut ist nichts. +**Dritte Runde — Node ist eine Technologie, keine Abhängigkeit; der +Entwurf kippt (2026-09-02, später am Tag).** Nutzer-FYI, als wichtig +notiert: *„Bevor komplette neue Technologien eingeführt werden, hier z. B. +Node.js, sollte dies dem menschlichen Entwickler deutlich gemacht werden +und dessen Zustimmung eingeholt werden. Node.js einzuführen ist eine +Dimension schlimmer als eine neue Dependency für den Kotlin/Spring-Boot +Backend."* Das RFC hatte Node — bisher im Repo reines Build-Werkzeug +(Vite) — als **Laufzeit** vorgeschlagen, lokal als Paket und auf dem +Server als Dienst, und dabei nur die SDK-Abhängigkeit abgefragt; die +eigentliche Frage war nie sichtbar gestellt. Die Regel steht jetzt in +CLAUDE.md (Querschnitts-Konventionen): Eine neue Laufzeit oder Sprache wird +als eigene, benannte Entscheidung mit den Alternativen innerhalb der +vorhandenen Technologien vorgelegt — nie als Nebensatz. + +Nachgeholt in zwei Schritten — erst die **Beweggründe** (Nutzer: „die +Frage aufsplitten und mich mehr nach Beweggründen fragen, daraus die +eigentliche Entscheidung ableiten"), dann die Entscheidung: + +- **Beweggründe:** Entwickler arbeiten parallel an geteilten Plänen und + setzen agentische KI ein — Pläne im Dialog erstellen, Feature-Fortschritt + tracken; der Agent läuft meist lokal (OpenCode, Claude Code, Codex), + später auch im Frontend. Pläne liegen **beides**: als Datei im Repo und + als Server-Dokument. Der Text eines lokalen Plans **darf** zur Analyse + an den eigenen Server. Die Startzeit eines lokalen Prozesses ist egal. +- **Daraus folgt: kein lokaler Prozess, keine neue Laufzeit.** Der + MCP-Server lebt **nur im Backend** (Streamable HTTP, Bearer-Token per + Spring Security), die Werkbaum-Logik läuft dort per GraalJS aus den + unveränderten Frontend-Modulen, beim Bauen zu einem ES-Modul gebündelt — + Node bleibt Build-Werkzeug. Jedes Werkzeug hat zwei Formen: `text` (der + Agent hat die Datei selbst gelesen, bekommt den neuen Text zurück und + schreibt ihn selbst; die Leitplanken sind dort **Befund**) und `document` + (UUID eines Dokuments dieses Backends; Zeilen-Diff, Konfliktschutz, + Leitplanken als **Sperre**). `document` ist bewusst eine UUID, keine URL + — kein Fremdserver, keine SSRF-Frage. Der Git-Spiegel ist `pull-doc`, + vom Cron oder vom Agenten selbst aufgerufen; ein Spiegel am Dienst + entfällt. +- **Verworfen:** das Node-Paket (`#ai.mcp.node` `[-]`); als Rückfall + benannt, nicht gewählt: das Backend-Jar im stdio-Modus beim Entwickler + (keine neue Laufzeit, aber ohne Anlass, sobald der Text zum Server darf). +- **Backend-Abhängigkeiten, Rückfrage gestellt und beantwortet:** GraalJS + (`org.graalvm.polyglot`) und der **Spring-AI-Starter** + (`spring-ai-starter-mcp-server-webmvc`); Boot-4-Verträglichkeit ist beim + Spike zu prüfen, Rückfall das MCP-Java-SDK direkt. +- **Die Runde-1-Entscheidungen zum Node-Paket sind überholt** (SDK, + Serialisierung im Paket, npm); die Tool-Schicht lebt als + `frontend/src/inspect.js`, `guard.js`, `edit.js` — dort, wo `#ai.dialog` + sie ohnehin braucht. + +**Vor allem anderen steht jetzt der Spike** (`#ai.mcp.spike`): Läuft das +Bündel in GraalJS mit denselben Zahlen wie Vitest, was kostet ein Kontext +auf dem knappen Host (D76-Nachtrag 3), reicht der Apache den SSE-Strom +ungepuffert durch. Fällt er durch, ist die Antwort nicht Node, sondern eine +neue Frage an den Entwickler. Offen sonst nur die Prompts. Gebaut ist +nichts. diff --git a/docs/examples/werkbaum.werkbaum b/docs/examples/werkbaum.werkbaum index f03d273..8d0ce12 100644 --- a/docs/examples/werkbaum.werkbaum +++ b/docs/examples/werkbaum.werkbaum @@ -225,16 +225,18 @@ - [?] #ai.dialog: Edit the plan in a dialogue with the AI (L) - [?] #ai.key: Bring your own API key, no server in between (M) - [?] #ai.ground: llms.md grounds the model in the notation (S) - - [ ] #ai.mcp: An MCP server hands the plan to outside agents (XL) :#not.llms %% docs/rfc/001-mcp-server.md - - [ ] #ai.mcp.node: A Node package that reuses the one JS parser (S) %% no second parser, D14 - - [ ] #ai.mcp.read: Resources: the agent guide, a file, a server document (S) - - [ ] #ai.mcp.inspect: Tools that answer what the diagram answers (M) %% tree, warnings, cheapest path - - [ ] #ai.mcp.write: Writes go out as line diffs and are refused on conflict (M) :#col.live.diff - - [ ] #ai.mcp.guard: Guard rails: no broken notation, no [^] from an agent (S) - - [ ] #ai.mcp.verbs: Node verbs: add, move, size, remove — the rules live in the frontend (M) :#ai.mcp.write - - [ ] #ai.mcp.mirror: The shared document mirrors into git after every agent write (S) :#col.git.pull - + [?] #ai.mcp.http: Streamable HTTP, served by the backend running the JS modules (M) :#col.live.owner %% GraalJS spike first - - [-] #ai.mcp.kotlin: A server inside the backend with its own Kotlin parser (L) %% the second grammar D14 forbids + - [ ] #ai.mcp: An MCP server in the backend hands the plan to outside agents (XL) :#not.llms %% docs/rfc/001-mcp-server.md + - [ ] #ai.mcp.spike: GraalJS runs the one JS parser inside the JVM — measured before anything is built (S) %% memory, same numbers as vitest, SSE through Apache + - [ ] #ai.mcp.core: The tool layer as headless frontend modules, bundled into the jar (M) :#ai.mcp.spike + - [ ] #ai.mcp.read: Resources: the agent guide and the server documents (S) + - [ ] #ai.mcp.inspect: Tools that answer what the diagram answers, for a text or a document (M) %% tree, warnings, cheapest path + - [ ] #ai.mcp.write: Writes to documents go out as line diffs and are refused on conflict (M) :#col.live.diff + - [ ] #ai.mcp.guard: Guard rails: no broken notation, no [^] — a block on documents, a finding on text (S) + - [ ] #ai.mcp.verbs: Node verbs: add, move, size, remove — text to text, and on documents (M) :#ai.mcp.write + - [ ] #ai.mcp.auth: A static bearer token via Spring Security; OAuth once there is an IdP (S) + - [ ] #ai.mcp.mirror: pull-doc names who changed which version — run by cron or by the agent (S) :#col.git.pull + - [-] #ai.mcp.node: A local Node package as stdio server (L) %% a new runtime, and the text may go to the own server instead — D93 + - [-] #ai.mcp.kotlin: A parser of its own in Kotlin (L) %% the second grammar D14 forbids --- #wb @@ -1298,39 +1300,50 @@ #ai.mcp The other direction of the AI integration: not an LLM inside the editor, - but the plan handed to agents that live outside it — Claude Code, IDE - agents, desktop assistants — over the Model Context Protocol. Specified in + but the plan handed to agents that live outside it — Claude Code, + OpenCode, Codex, IDE agents — over the Model Context Protocol, served by + the backend over streamable HTTP. No local process, no new runtime: a + URL and a token in the host's configuration. Specified in docs/rfc/001-mcp-server.md before anything is built. -#ai.mcp.node - The server is a small Node package that imports the frontend's headless - modules (parser, model, live diff) as they are. That keeps D14 intact: the - one JS parser stays the only parser, and agents get the same warnings, - effective statuses and cheapest path the diagram shows. +#ai.mcp.spike + The whole design rests on the JVM running the frontend's JS modules + through GraalJS. Before a line is built, a spike measures it: the bundle + parses the shipped plan with the same numbers vitest reports, a context + costs a known amount of memory on the tight production host, and Apache + passes the SSE stream of a long tool call unbuffered. + +#ai.mcp.core + The tool layer — tree to JSON, guard rails, node edits — lives as headless + modules in frontend/src, tested with vitest, and is bundled at build time + into one ES module inside the jar. That keeps D14 intact: the one JS + parser stays the only parser, and Node stays what it is here, a build tool. #ai.mcp.read - What an agent can read: llms.md as the notation guide, a local .werkbaum - file, and a shared document on a Werkbaum backend by its URL — the same - three sources a person has. + What an agent can read: llms.md as the notation guide, and the documents + of this backend by UUID — with title, version and checksum. A local file + the agent reads itself and hands the text to the tools. #ai.mcp.inspect Tools that return the parsed tree with ids, statuses (own and effective), sizes (given and assumed), tags and dependencies, the warnings the editor would show, and the cheapest path with its stations and per-person load — - so the agent reasons on the model instead of re-implementing the SPEC. + for a text the agent passes in or a document it names — so the agent + reasons on the model instead of re-implementing the SPEC. #ai.mcp.write - Edits leave the server as the same line diffs the editor sends: against a - base version with checksum, refused on a real conflict, visible to every - open editor with "changed by" naming the agent. A local file is written - only if it is unchanged since it was read. + Edits to a document leave the server as the same line diffs the editor + sends: against a base version with checksum, refused on a real conflict, + visible to every open editor with "changed by" naming the agent's host. + For a text, the tool returns the new text and the agent writes the file. #ai.mcp.guard Guard rails on every write: the result must not break the notation (no new mixedGate, unknownStatus, descStray or duplicateId), and an agent never writes [^] — "in production" is a deploy's statement (D30), not a model's. - Content warnings like a size conflict are written and reported, so a human - sees them as the amber mark. Both rails can be lifted per call, never silently. + On a document that blocks the write; on a text it is a finding the agent + sees. Content warnings like a size conflict are written and reported, so a + human sees them as the amber mark. Both rails can be lifted per call. #ai.mcp.verbs Convenience for the model: add a node under a parent, move a subtree, set a @@ -1338,24 +1351,26 @@ gets wrong. The text-to-text rules live in frontend/src/edit.js beside the fold and short-id helpers, so exactly one place keeps knowing the line format. +#ai.mcp.auth + One static bearer token per installation, checked by Spring Security like + the master password; without it the endpoint is closed, not open. OAuth + 2.1, which MCP foresees for HTTP, waits for an identity provider. + #ai.mcp.mirror Both at once: the agent works on the shared document, where the people - are, and the same plan stays under git control. After every agent write - the server runs pull-doc --git-commit for a configured mirror file, naming - the agent in the commit — the server is the source, git the archive. The - reverse (git as the source) is a different node, #col.git. + are, and the same plan stays under git control. pull-doc --git-commit + archives the server state — run by a cron, or by the agent itself right + after its change — and with --with-history the commit names who changed + which version, as trailers, so blame never credits the agent with a + human's lines. The server is the source, git the archive. -#ai.mcp.http - Streamable HTTP as a second transport, so a hosted agent can reach the - server without a local process. Served by the Spring backend, which runs - the same JS modules through GraalJS — Kotlin is transport and access - (a static bearer token via Spring Security), the logic stays in one place. - A spike must first prove the modules run there and what they cost in - memory; if it fails, the Node package serves HTTP as its own service. - Documents only: no file store, no git mirror over the network. +#ai.mcp.node + A local Node package as stdio server was the first draft: files and the + git mirror right at the process. Discarded because it would make Node a + runtime — a new technology, one dimension heavier than a dependency — and + the motives did not need it: the text may go to the own server instead. #ai.mcp.kotlin - Building the server into the Spring backend with a parser of its own would - put it next to the documents — but a Kotlin parser is exactly the second - grammar D14 forbids. Discarded; the backend may host the transport - (#ai.mcp.http), never a second grammar. + A parser of its own in Kotlin would put the logic next to the documents — + but it is exactly the second grammar D14 forbids. Discarded; the backend + runs the JS parser instead of rebuilding it. diff --git a/docs/rfc/001-mcp-server.md b/docs/rfc/001-mcp-server.md index b500cd2..559807d 100644 --- a/docs/rfc/001-mcp-server.md +++ b/docs/rfc/001-mcp-server.md @@ -2,82 +2,99 @@ | | | |---|---| -| Status | **Abgestimmt** (2026-09-02, Entscheidungen in §11 und D93) — nichts gebaut | +| 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` (Verweis auf dieses RFC) | -| Berührt | `frontend/src/*` (headless-Module, lesend), neues Paket `mcp/`, `tools/pull-doc` (Aufrufer), README, `.mcp.json` | -| Berührt nicht | Notation (SPEC), `llms.md`, Deploy-Skripte; Backend-Code erst in Phase 6 (§7.3) | +| 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): ein kleines -Node-Paket, das KI-Agenten außerhalb des Editors — Claude Code, IDE-Agenten, -Desktop-Assistenten — einen Werkbaum-Plan als **Ressource** (Text) und als -**Werkzeuge** (Baum, Warnungen, günstigster Pfad, Schreiben) anbietet. +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. -Der Server **parst nicht selbst**: Er importiert die headless-Module des -Frontends (`parser.js`, `model.js`, `live.js`) unverändert. Damit bleibt D14 -gewahrt — es gibt weiterhin genau einen Parser — und ein Agent bekommt -wörtlich dieselben Warnungen, effektiven Status und Stationen, die das -Diagramm zeigt. +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. -Geschrieben wird über denselben Weg wie aus dem Editor: als **Zeilen-Diff -gegen eine Basisversion mit Prüfsumme** (D76). Fremde Änderungen werden nie -überschrieben, ein Konflikt kommt als Fehler zurück, und in jedem offenen -Editor steht „geändert von: *Claude Code*“. Zwei Leitplanken gelten auf -jedem Schreibvorgang: **keine beschädigte Notation** (Struktur-Warnungen -blockieren, inhaltliche werden gemeldet), **kein `[^]` von einem Agenten**. +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. -Der Server arbeitet bevorzugt auf dem **geteilten Dokument** — dort sitzen -die Menschen —, und derselbe Plan kann zugleich **unter Git-Kontrolle** -stehen: Nach jedem Agenten-Schreibvorgang committet der Server den -Serverstand über das vorhandene `tools/pull-doc --git-commit` (D88) in eine -Spiegel-Datei. Der Server ist die Quelle, Git das Archiv (§5.7). +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 die Module; keines ersetzt das -andere. +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 -Heute kann ein Agent mit einem Werkbaum-Plan dreierlei tun — und alles -davon nur halb: +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. -1. **Eine `.werkbaum`-Datei lesen und schreiben.** Das geht mit Claude Code - bereits, `llms.md` erklärt die Notation (D43). Aber der Agent muss die - **Semantik** selbst nachrechnen: Ist der Plan warnungsfrei? Welcher +Heute kann ein Agent damit dreierlei tun — und alles davon nur halb: + +1. **Eine `.werkbaum`-Datei lesen und schreiben.** Das geht mit Claude + Code bereits, `llms.md` erklä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 stattdessen. - Beim Größen-Konflikt (D62) oder der Closure-Rechnung (D42) ist Raten + 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. 2. **Ein Server-Dokument (`?live=`) anfassen.** Nur per `curl` gegen die - REST-API — mit Volltext-`PUT`, also ohne das Zeilen-Diff-Protokoll (D76). - Ein Agent, der so schreibt, überschreibt, was ein Mensch in der + 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. 3. **Den Plan im Editor ändern lassen** — `#ai.dialog` ist eine Idee ohne Entwurf. -MCP ist inzwischen die Verkehrssprache dafür: Claude Code, Claude Desktop, -Cursor, IntelliJ-Agenten und die Anthropic-API selbst (MCP-Connector) -sprechen es. Ein Server, der Werkbaum darüber anbietet, macht Punkt 1 und 2 -auf einen Schlag richtig — und zwar für **alle** diese Hosts, nicht nur für +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** (Datei oder Server-Dokument) und den - Notationsleitfaden dazu bekommen. -- Ein Agent kann den Plan **befragen**, ohne die SPEC nachzuimplementieren: +- 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. -- Ein Agent kann den Plan **ändern**, ohne fremde Arbeit zu überschreiben, - und jede Änderung ist im Editor als Agenten-Änderung erkennbar. -- Kein zweiter Parser, keine zweite Semantik (D14). + 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)** @@ -86,17 +103,18 @@ einen. API-Schlüssel. Das Modell sitzt im Host; `#ai.key` bleibt ein eigener Knoten. - **Keine neue Notation.** Nichts an SPEC oder `llms.md` ändert sich. -- **Kein Backend-Umbau.** Die vorhandene REST-API genügt (§7.3). +- **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** — der Server erbt das Zugriffsmodell des - Backends (unerratbare UUID, D76) und erfindet keines dazu. +- **Keine Rechteverwaltung** — ein Token je Installation; das + Dokument-Zugriffsmodell (unerratbare UUID, D76) bleibt. - **Kein Ersatz für `#ai.dialog`** und kein Chat im Editor. ## 4. Begriffe (soweit hier nötig) -MCP trennt **Hosts** (die Anwendung mit dem Modell: Claude Code, Desktop, -IDE) von **Servern** (Prozesse, die Fähigkeiten anbieten). Ein Server -bietet drei Dinge an: +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“). @@ -105,65 +123,73 @@ bietet drei Dinge an: - **Prompts** — vorgefertigte Anweisungen, die der Nutzer auswählt („zerlege diesen Knoten“). -Transporte: **stdio** (der Host startet den Server als Kindprozess — der -Normalfall für lokale Werkzeuge) und **Streamable HTTP** (ein -Netzwerk-Endpunkt; hat 2025 das ältere SSE-Verfahren abgelöst). Die -Protokollrevision wird vom SDK getragen; dieses RFC legt keine fest. +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 (Claude Code, IDE, Desktop) Werkbaum - ┌──────────────┐ stdio / JSON-RPC ┌──────────────────────────────┐ - │ Modell │◄────────────────────►│ mcp/server.js │ - │ + Werkzeuge │ │ ├─ resources (Text, Guide) │ - └──────────────┘ │ ├─ tools (inspect, │ - │ │ write) │ - │ └─ stores │ - │ ├─ file (fs) │ - │ └─ live (REST, D76) │ - │ importiert unverändert: │ - │ frontend/src/parser.js │ - │ frontend/src/model.js │ - │ frontend/src/live.js │ - └──────────────┬───────────────┘ - │ HTTP - ┌──────────────▼───────────────┐ - │ Backend /api/v1/documents/… │ - └──────────────────────────────┘ + 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 │ + └───────────────────────────────────────┘ ``` -Ein Paket `mcp/` neben `frontend/` und `backend/`, eigenes `package.json`, -ES-Module. Es hat **keine** Kopie von Werkbaum-Logik; alles Entscheidbare -kommt aus `frontend/src`. Was das Paket selbst beisteuert, ist Verdrahtung: -MCP-Rahmen, zwei Ablagen, JSON-Serialisierung des Baums und die -Leitplanken. +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 Ablagen +### 5.2 Zwei Formen je Werkzeug: Text und Dokument -Ein Plan wird über eine **Quelle** adressiert — dieselben drei, die ein -Mensch hat, minus `?sourceUrl=` (nur lesend, kommt bei Bedarf dazu): +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): -| Quelle | Adressform | Lesen | Schreiben | +| Form | Lesen | Ergebnis eines Schreib-Werkzeugs | Leitplanken (§5.5) | |---|---|---|---| -| Datei | absoluter Pfad oder relativ zum Arbeitsverzeichnis | `fs` | atomar (Temp-Datei + `rename`); **nur, wenn die Datei seit dem Lesen unverändert ist** (Prüfsumme) | -| Server-Dokument | `https://…/api/v1/documents/` oder der geteilte `?live=`-Link (ausgepackt wie bei `pull-doc`, D88) | `GET` | `PATCH /content` mit Zeilen-Diff, `baseVersion`, `checksum` (D76) | -| Leitfaden | `werkbaum://guide` | `frontend/public/llms.md` | — | +| `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 | -Der Dateizugriff ist auf **erlaubte Wurzeln** begrenzt (Startparameter -`--root`, Voreinstellung: das Arbeitsverzeichnis des Hosts). Ein Agent, der -einen Pfad außerhalb nennt, bekommt einen Fehler — nicht, weil ihm misstraut -wird, sondern weil der Plan-Text Fremddaten ist (§8). +`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://file/` | Notationstext der Datei | `text/plain` | -| `werkbaum://doc/` | Notationstext des Server-Dokuments, mit `version` als Metadatum | `text/plain` | +| `werkbaum://doc/` | 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 @@ -171,33 +197,31 @@ lesen. Die Semantik kommt aus den Tools. ### 5.4 Tools -Wenige Verben, sprechend benannt, alle mit `source` als erstem Parameter -(Pfad oder URL): +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` | `source` | Text, `version`/Prüfsumme, Zeilenzahl | Ablage | -| `werkbaum_inspect` | `source`, 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 Liste der nötigen Knoten, Stationen in Dokumentreihenfolge, `assigneeLoads`, `overloadedAssignee`, `exact` (D42) | `parse`, `effectiveStatus`, `computeCheapPlan`, `assigneeLoads`, `taigaSlugs`, `warningText` | -| `werkbaum_set_status` | `source`, `id`, `status` (Code der §4-Tabelle), `base` | neuer Stand (Version, Prüfsumme), Warnungs-Delta | `setStatusBox` (parser.js), Ablage | -| `werkbaum_apply_ops` | `source`, `base`, `ops[]` (`insert`/`replace`/`delete` mit `index`, `count`, `lines` — **dasselbe Schema wie `LineOperation` der API**) | wie oben | `applyOps` (live.js), Ablage | -| `werkbaum_write` | `source`, `base`, `text` (Volltext) | wie oben | `computeOps` (live.js) → Diff → Ablage | -| `werkbaum_add_node` | `source`, `base`, `parent` (ID), `line` (die Knotenzeile ohne Einrückung, z. B. `- [ ] #auth.token: Token prüfen (S)`), optional `after` (ID eines Geschwisters) | wie oben | `addNodeLine` (edit.js) | -| `werkbaum_move_node` | `source`, `base`, `id`, `parent` (neue Eltern-ID), optional `after` | wie oben; der Teilbaum samt Beschreibungs- und Fortsetzungszeilen wandert mit, Einrückung wird angepasst | `moveSubtree` (edit.js) | -| `werkbaum_set_size` | `source`, `base`, `id`, `size` (`XS`…`XXL` oder `null` zum Entfernen) | wie oben | `setSizeToken` (edit.js) | -| `werkbaum_remove_node` | `source`, `base`, `id`, `discard: true\|false` | wie oben — `discard` setzt `[-]` (die Notation für „bewusst nicht“, §4), `false` löscht den Teilbaum samt seinen Blöcken | `setStatusBox` bzw. `removeSubtree` (edit.js) | +| `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** (Nutzer-Entscheidung, §11) sind Bequemlichkeit für -das Modell: `apply_ops` könnte jedes davon ausdrücken, aber ein Agent, der -Zeilenindizes ausrechnen muss, verrechnet sich — ein Verb mit ID ist die +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 später auch für -`#ai.dialog` da (§7.1). Das MCP-Paket verdrahtet sie nur. - -**`base` ist Pflicht bei jedem Schreiben** — `{version, checksum}` vom -letzten Lesen. Ohne `base` wird nicht geschrieben. Das ist die -Idempotenz- und Konfliktregel aus D76, unverändert übernommen. +`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, @@ -205,130 +229,114 @@ 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 Schreib-Leitplanken +### 5.5 Leitplanken -Auf jedem der drei Schreib-Tools, vor dem Schreiben: +Auf jedem Schreib-Werkzeug, vor dem Schreiben (Dokument) bzw. als Befund +(Text): 1. **Keine beschädigte Notation.** Der Ergebnis-Text wird geparst; trägt er **Struktur-Warnungen**, die der Ausgangstext nicht hatte — - `mixedGate`, `unknownStatus`, `descStray`, `duplicateId` —, wird - **nicht geschrieben**, und die Antwort nennt sie. Das sind die - Warnungen, bei denen der Text als Notation kaputt ist: eine Zeile, die - der Parser anders liest, als sie gemeint war. Schalter - `allowWarnings: true` je 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, genau wie bei einer eigenen Änderung - (Nutzer-Entscheidung, §11; die strengere Fassung „jede neue Warnung - blockiert“ war der Vorschlag). Verschwindende Warnungen sind immer - erlaubt, und jede Antwort trägt das vollständige Warnungs-Delta. + `mixedGate`, `unknownStatus`, `descStray`, `duplicateId` —, ist das + **blockierend**: Ein Dokument wird nicht geschrieben, die Antwort nennt + sie; ein Text kommt mit `blocking: true` zurück. Das sind die Warnungen, + bei denen der Parser eine Zeile anders liest, als sie gemeint war. + Schalter `allowWarnings: true` je 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. 2. **Kein `[^]` von einem Agenten.** `in Produktion` ist die Aussage eines - Deploys (D30), nicht die eines Modells. `set_status` lehnt `prod` ab, - `apply_ops`/`write` lehnen ein Ergebnis ab, in dem ein Knoten neu `[^]` - trägt. Schalter `allowProd: true` je Aufruf — mit derselben Begründung: - ausdrücklich, nie still. -3. **Konflikt ist ein Fehler, keine Entscheidung.** Bei `409` (Server) oder - geänderter Datei kommt der **aktuelle** Text mit zurück; der Agent — und - im Zweifel der Mensch hinter ihm — entscheidet. Der Server rebased nichts - selbst über das hinaus, was das Backend ohnehin tut (D76: nicht - überlappende Ops verschiebt der Server; das bleibt). + Deploys (D30), nicht die eines Modells. Blockierend wie oben; Schalter + `allowProd: true` je Aufruf — ausdrücklich, nie still. +3. **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 der `LiveEditingService` ohnehin 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_check` mit `against` sagt ihm vorher, ob sein + Ergebnis Befunde hat. -Alle drei sind reine Verdrahtung um vorhandene Regeln; der Parser selbst -bleibt fehlertolerant, wie SPEC §4 es verlangt. +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 Identität und Sichtbarkeit +### 5.6 Zugang und Identität -Jeder Schreibvorgang an ein Server-Dokument geht mit `clientId` -`mcp-` (je Serverprozess, sessionStorage-Äquivalent: je Prozess -eine laufende `seq`, D76-Nachtrag 7) und einem `displayName`, der den -**Host nennt**: den Namen, den der Host im MCP-Handshake mitschickt -(`clientInfo.name`, etwa „Claude Code“), mit Rückfall **„Agent“**, wenn er -keinen nennt; `--name` übersteuert beides (Nutzer-Entscheidung, §11). In -jedem offenen Editor steht damit in der Historie „geändert von: Claude -Code“ (D86) — der Mensch sieht, welches Werkzeug es war, und kann es über -die Meilensteine zurückholen. Der Name bleibt eine Behauptung, kein -Nachweis — dieselbe Einordnung wie beim Anzeigenamen eines Menschen (D86). +**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 --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-` (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 -Die Frage, an der die Ablage-Wahl hängt: 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). Das sind zwei -Wahrheiten für einen Text, und die Antwort ist nicht „eine davon“, sondern -eine **Richtung**: Der Server ist die Quelle, Git ist das Archiv. +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 schon:** `tools/pull-doc --git-commit` (D88, +**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, mit Titel und Server-Version in der Nachricht. -Aus dem Cron heraus archiviert sich ein Plan damit selbst. Der MCP-Server -setzt genau darauf auf: +datiert, nur bei Änderung. Zwei Aufrufer: -- **Spiegel-Datei (`--mirror `):** Ist für ein Dokument eine - Spiegel-Datei konfiguriert, ruft der Server nach **jedem erfolgreichen - Agenten-Schreibvorgang** `pull-doc --git-commit` dafür auf. Die - Commit-Nachricht nennt zusätzlich den Urheber („via Agent: Claude Code“) - — die Git-Historie kann Agenten-Änderungen dann so auseinanderhalten, wie - es die Server-Historie über `displayName` tut (D86). Menschliche - Änderungen dazwischen fängt weiterhin der Cron; wer keinen hat, bekommt - sie spätestens mit dem nächsten Agenten-Zug ins Git, denn `pull-doc` - committet den **ganzen** Serverstand, nicht nur die Agenten-Zeilen. -- **Die Spiegel-Datei ist für den Agenten lesbar, aber nicht der - Schreibweg.** Ein Agent in Claude Code sieht die Datei im Repo und könnte - sie direkt editieren — beim nächsten `pull-doc` wäre das überschrieben. - Deshalb: `werkbaum_write`/`apply_ops`/`set_status` auf eine Datei, die - als Spiegel eines Dokuments konfiguriert ist, werden **umgeleitet** an - das Dokument (der Server kennt die Zuordnung) und die Antwort sagt es. - Ein Agent, der die Datei mit seinen eigenen Werkzeugen (Editor, `sed`) - anfasst, ist außerhalb des MCP-Servers — dagegen hilft nur die - D88-Regel, die schon da ist: Eine schmutzige Spiegel-Datei wird beim - nächsten Commit **trotzdem** vom Serverstand überschrieben, und der - Commit macht die Abweichung im Diff sichtbar statt sie zu verstecken. -- **Nicht gebaut wird die Umkehrung** — Git als Quelle und der Server als - Spiegel (`#col.git.pr` für Pläne, die sich wöchentlich ändern, oder - `#col.git.auto`, bei dem das Backend selbst committet). Beides sind - eigene Knoten mit eigenen Fragen (Konfliktmarker mitten in der Notation, - ROADMAP „Gemeinsam an einem Diagramm arbeiten“); der MCP-Server soll sie - weder vorwegnehmen noch ausschließen. Kommt `#col.git.auto`, wird - `--mirror` schlicht überflüssig — die Schnittstelle des Servers ändert - sich dadurch nicht. - -Im Plan steht das als `#ai.mcp.mirror` mit Abhängigkeit auf -`#col.git.pull`. +- **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 + ` 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 Prompt + `werkbaum/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. „Der Agent hat diesen Commit gemacht“ ist -also nur die halbe Wahrheit, und die Form der Urheber-Angabe entscheidet, -ob `git blame` später lügt. Vier Formen, mit dem, was sie aussagen: +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** des Commits — `blame` schreibt ihm **jede** Zeile darin zu, auch die, die Anna getippt hat | Falsche Blame-Auskunft; und Git verlangt eine E-Mail-Adresse, die es nicht gibt | -| **b) `Co-Authored-By:`-Trailer** | Die Konvention, die das Repo für Claude-Commits schon nutzt: „hat beigetragen“, ohne Autor zu sein; GitHub zeigt Co-Autoren an | Braucht ebenfalls `Name `; 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`, nennt den Auslöser | Nicht maschinenlesbar; nennt nur den Auslöser, nicht die Beteiligten | -| **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 ein Trailer `Werkbaum-Changed-By: `, dazu `Werkbaum-Version: 8` | Ein zweiter `GET` je Commit; eigene Trailer statt der GitHub-Konvention | +| 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 `; 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: `, dazu `Werkbaum-Version: 8` | Ein zweiter `GET` je Commit; eigene Trailer statt der GitHub-Konvention | -**Vorschlag: d, mit c im Betreff.** Es ist die einzige Form, die die -Wahrheit des Commits trägt: Git-Autor bleibt, wer den Prozess betreibt -(das ist buchstäblich richtig — er hat committet), die Beteiligten stehen -mit Namen im Rumpf, maschinenlesbar per `git interpret-trailers`, und der -`Werkbaum-Version:`-Trailer gibt `pull-doc` beim nächsten Lauf die -Untergrenze für „seit dem letzten Commit“ — ohne ihn müsste der Betreff -geparst werden. Damit wird der **Cron-Commit** (D88) im selben Zug -ehrlicher: Auch dort stehen dann die Menschen, die seither geändert haben. -Der Schalter dafür (`--with-history`) kommt ins Skript, nicht in den -Aufrufer (D77-Nachtrag); der MCP-Server setzt ihn immer. Die Anzeigenamen -bleiben, was sie in der Server-Historie sind: Behauptungen, keine -Nachweise (D86) — im Commit steht deshalb „Changed-By“, nicht „Author“. +**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“. -Verworfen: **a** (Blame lügt) und **b** allein (erfundene E-Mail-Adressen, -und ein Trailer ohne Versionsbezug sagt nicht, *was* der Beitrag war). -Entscheidung: §11. - -### 5.8 Prompts (optional, Phase 6) +### 5.8 Prompts (optional, Phase 5) Zwei vorgefertigte Anweisungen, die ein Host dem Nutzer anbietet: @@ -336,374 +344,293 @@ Zwei vorgefertigte Anweisungen, die ein Host dem Nutzer anbietet: Zerlegung ab M, unklare Zuständigkeiten“ (nutzt `inspect`, schreibt nichts). - `werkbaum/decompose` — „Zerlege Knoten `#id` in Teilpakete mit Größen“ - (endet in `apply_ops`, mit den Leitplanken). + (endet in den Verben, mit den Leitplanken; bei einem Dokument mit dem + Hinweis auf `pull-doc`). -Beides sind Texte, keine Logik; sie leben im Paket und nicht in `llms.md` -(das bleibt Notation, D43). +Beides sind Texte, keine Logik; sie leben im Backend-Paket und nicht in +`llms.md` (das bleibt Notation, D43). -### 5.9 Konfiguration und Start +### 5.9 Konfiguration -``` -node mcp/server.js [--root ]… [--server ] [--name ] - [--mirror =]… -``` +Serverseitig (Umgebungsdatei des Dienstes, D77): -- `--root`: erlaubte Dateiwurzeln (mehrfach). Ohne Angabe: `cwd`. -- `--server`: Voreinstellung für relative Dokument-Angaben (`` statt - voller URL). Ohne Angabe muss die URL vollständig sein. -- `--name`: `displayName` für Server-Schreibvorgänge. -- `--mirror`: Spiegel-Datei je Dokument (§5.7); die Datei muss in einem - git-Worktree unter einer erlaubten Wurzel liegen — `pull-doc` prüft das - ohnehin. -- `--http []`: statt stdio ein Streamable-HTTP-Endpunkt (§5.10); - schließt `--root` und `--mirror` aus. +- `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). -### 5.10 Streamable HTTP — der entfernte Transport +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). -Für lokale Hosts genügt stdio: Der Host startet den Server als -Kindprozess, der Prozess hat die Rechte des Nutzers, die Dateiwurzeln sind -seine. Ein **gehosteter** Agent (Anthropic-API-MCP-Connector, Claude -Desktop mit entferntem Server, ein Managed Agent) kann keinen Kindprozess -starten; er braucht eine URL. Das ist `#ai.mcp.http`, und drei Fragen -hängen daran — Ort, Zugang, Umfang. Sie sind Architektur und werden jetzt -entschieden, gebaut wird die Phase erst, wenn ein solcher Host wirklich -ansteht. +### 5.10 Die JVM führt die JS-Module aus -**Ort — wo läuft der HTTP-Server?** Vorweg, weil die Frage kam: MCP -verlangt **kein** Node. Das Protokoll ist JSON-RPC über stdio oder HTTP, -und es gibt offizielle SDKs für TypeScript, Python, **Kotlin und Java** -sowie einen Spring-AI-Starter für MCP-Server. Das Backend *kann* also einen -MCP-Server anbieten. Was Node im ersten Entwurf erzwang, war nicht das -Protokoll, sondern der **Parser**: Jedes nützliche Tool braucht -`parser.js`/`model.js`, und D14 verbietet eine zweite Grammatik. Daraus -folgen drei echte Optionen: +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“). -| Option | Aussage | Preis | -|---|---|---| -| **a) Dasselbe Node-Paket als eigener Dienst** auf dem Server-Host: systemd-User-Unit wie das Backend (D77), lauscht auf `127.0.0.1`, Apache reicht `/mcp/` per `RewriteRule … [P]` durch — dieselbe Zeile wie für `/api/` | Ein Paket, zwei Transporte; die Infrastruktur aus D77 wird wiederverwendet; das Paket läuft **neben** dem Backend und spricht es über `127.0.0.1` an | Node auf dem Server (bisher nur JDK, D77 — per nvm ins Home, dasselbe Muster wie das JDK); ein zweiter Dienst zu betreiben (`remote mcp …` als Ziel); das Bearer-Token prüft der Node-Dienst selbst | -| b1) Im Kotlin-Backend, mit eigenem Parser | ein Dienst | **D14 — verworfen** (`#ai.mcp.kotlin`) | -| **b2) Im Kotlin-Backend, das die JS-Module selbst ausführt** — GraalJS (`org.graalvm.polyglot`) lädt `frontend/src/*.js` als ES-Module in der JVM; Kotlin ist nur Transport (offizielles Kotlin-SDK oder Spring-AI-Starter) und Zugang (Spring Security, das es schon gibt) | **Ein** Deployment, kein Node auf dem Server, kein zweiter Dienst; das Bearer-Token ist ein Spring-Security-Filter neben dem Master-Passwort; die Werkbaum-Logik bleibt **einmal** vorhanden — dieselbe Technik, die der Plan für das IDE-Plugin vorsieht (`#idea.drift.js`: „den einen JS-Parser im IDE laufen lassen“) | Zwei neue Backend-Abhängigkeiten (MCP-SDK, GraalJS); Speicher auf dem knappen Host (D76-Nachtrag 3 — GraalJS im Interpreter-Modus auf Stock-OpenJDK kostet nach Erfahrungswerten einige zehn MB, **zu messen**); ein Spike, der beweist, dass die ESM-Module dort laufen (`crypto.subtle` in `live.js` gibt es in GraalJS nicht — die Prüfsumme muss dort aus Kotlin kommen); und die Tool-Schicht muss so geschnitten sein, dass **dieselbe** JS-Datei in Node (stdio) und in GraalJS (HTTP) läuft | -| c) Gar nicht; entfernte Hosts nutzen eine stdio-Brücke (`mcp-remote` o. Ä.) | nichts zu bauen | Löst das Problem nicht: Die Brücke braucht selbst einen Prozess beim Nutzer, und ein Managed Agent hat keinen | - -Für die **lokalen** Agenten (Claude Code mit Dateien und Git-Spiegel) -bleibt stdio in jedem Fall Node — b2 ersetzt nicht das Paket, sondern nur -den entfernten Transport. Damit die beiden Hosts nicht auseinanderlaufen, -wird die Tool-Schicht (`inspect`, `guard`, `verbs`) als reine ESM-Datei -ohne Node-APIs geschnitten und von beiden Seiten aufgerufen; Ablagen und -Transport sind je Host eigen. Das ist bei b2 Pflicht, bei a nur Ordnung — -und es kostet nichts, es von Anfang an so zu tun. - -**Vorschlag: b2, unter Vorbehalt eines Spikes; a als Rückfall.** b2 ist -die bessere Architektur — ein Dienst, die vorhandene Zugangsschicht, kein -zweites Laufzeitsystem auf dem Server —, hängt aber an zwei Messungen, die -vor Phase 6 stehen: Laufen die Module in GraalJS (ESM-Laden, kein -`crypto.subtle`, kein `TextEncoder`-Unterschied), und was kostet es an -Speicher auf der Zielumgebung. Fällt eine der beiden durch, ist a ohne -Umbau der Tool-Schicht möglich. Unabhängig von der Wahl gilt: Streamable -HTTP läuft im **zustandslosen** Modus (keine Sitzungs-Ids, die einen -Neustart nicht überleben), und ob der Apache der Zielumgebung den -SSE-Strom langer Werkzeugaufrufe **ungepuffert** durchreicht, ist zu -messen (die D17-Nachtrag-4-Lehre) — Long Polling ist gemessen -(D76-Nachtrag 2), ein Strom nicht. - -**Zugang — wer darf?** Die MCP-Spezifikation sieht für HTTP-Transporte -OAuth 2.1 vor (der Server als Resource Server, Discovery über -Protected-Resource-Metadaten). Werkbaum hat aber weder Nutzerkonten noch -einen Identity Provider; der Zugriff auf Dokumente ist die **unerratbare -UUID** (D76), und die REST-API steht damit heute schon jedem offen, der -eine kennt. - -| Option | Aussage | Preis | -|---|---|---| -| a) **Kein Zugang nötig** — der Server tut über HTTP nichts, was die REST-API nicht auch tut (Dokument per UUID lesen/schreiben, `llms.md` ist öffentlich) | konsequent zum UUID-Modell; nichts zu verwalten | Ein offener Rechen-Endpunkt (`inspect` parst beliebig große Texte) lädt zum Missbrauch ein; und sobald `#col.live.owner` existiert, braucht der Server einen Weg, das Owner-Passwort **je Aufrufer** zu bekommen — ohne Zugangsschicht gibt es den nicht | -| **b) Statisches Bearer-Token** je Installation (`WERKBAUM_MCP_TOKEN` in der Umgebung, wie das Master-Passwort D77); Hosts geben es als Header mit (Claude Code: `--header "Authorization: Bearer …"`, der API-MCP-Connector: `authorization_token`) | Eine Zeile Konfiguration, keine Konten; hält den Rechen-Endpunkt zu; das Token reist über HTTPS (Apache), nie im Transkript — es steht in der Host-Konfiguration | Ein Token für alle Aufrufer: keine Unterscheidung, wer schreibt (den Namen liefert weiterhin der Handshake — eine Behauptung); Rotation von Hand | -| c) OAuth 2.1 von Anfang an | spezifikationsgemäß; Hosts wie Claude.ai-Connectoren erwarten es | Braucht einen Authorization Server, den es nicht gibt — entweder selbst bauen (weit über den Anlass hinaus) oder an einen IdP hängen; die Taiga-Instanz wechselt auf OIDC (D91-Nachtrag 1), das wäre der naheliegende, aber fremde Anker | - -**Vorschlag: b jetzt, c dann, wenn es einen IdP gibt.** Das Bearer-Token -ist die Zugangsschicht, die `#col.live.owner` ohnehin braucht: Das -Owner-Passwort eines Dokuments kommt später **nicht** als Tool-Parameter -(Transkript), sondern über dieselbe Umgebung des Dienstes — je Dokument -eine Zuordnung, die der Betreiber pflegt. OAuth 2.1 wird nicht -ausgeschlossen, nur nicht vor dem IdP gebaut; die Schnittstelle der Tools -ändert sich dadurch nicht. - -**Umfang — was bietet der entfernte Server an?** Weniger als der lokale: -**keine Datei-Ablage** (`--root` entfällt — der Dienst hat kein -Nutzer-Dateisystem, und ein Pfad-Parameter über HTTP wäre die Einladung, -die §8 verbietet) und **keinen Git-Spiegel** (`--mirror` entfällt — der -Cron-`pull-doc` auf dem Server-Host übernimmt das Archiv für alle, ob der -Schreibende lokal oder entfernt war). Übrig bleiben Guide, Dokumente und -alle Tools auf Dokumenten. Damit ist die entfernte Fassung eine **echte -Teilmenge** der lokalen, und ein Agent, der beides kennt, merkt keinen -Unterschied in den Verben. - -Entscheidung: §11. - -Für Claude Code: - -```bash -claude mcp add werkbaum -- node /pfad/zu/werkbaum/mcp/server.js --root . -``` - -Im Repository selbst liegt eine `.mcp.json`, damit Claude Code hier den -Server von selbst hat — der Plan `docs/examples/werkbaum.werkbaum` ist damit -das erste Dokument, an dem der Server benutzt wird (Dogfooding, dieselbe -Logik wie D27). +- **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.js` + samt allem, was es importiert, zu **einem** ES-Modul + `werkbaum-core.mjs` in 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 in + `live.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-`Context` ist **nicht nebenläufig**: ein kleiner + Pool von Kontexten mit gecachter `Source`, 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.js` als 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 — Server im Kotlin-Backend (Spring AI MCP) +### A — Kotlin-Server mit eigenem Parser -*Pro:* ein Deployment, Streamable HTTP frei Haus, sitzt direkt an den -Dokumenten. -*Contra:* Jedes nützliche Tool braucht den Parser. Entweder entsteht ein -Kotlin-Parser — **genau die zweite Grammatik, die D14 verbietet** — oder -der Server bietet nur Dokument-CRUD an, und das kann ein Agent per REST -schon heute. Dazu die erste Spring-AI-Abhängigkeit im Backend. -**Verworfen** (`#ai.mcp.kotlin` als `[-]` im Plan). +*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 (Vorschlag) +### B — Node-Paket, das die Frontend-Module importiert (Entwurf der Runden 1–2) -*Pro:* ein Parser, eine Semantik; die Module sind headless und getestet -(D54-Nachtrag 3 ist die Hausregel dafür); stdio ist der Transport, den alle -lokalen Hosts können; kein Backend-Umbau. -*Contra:* ein zweites Node-Paket im Repo; die Dependency-Frage (§7.4); -Remote-Zugriff erst mit Phase 6. -**Gewählt.** +*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 — Nichts bauen: Agenten lesen `llms.md` und editieren Dateien +### 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 (der Agent rät Warnungen, Pfad, effektiven -Status), keine Server-Dokumente außer per `curl`, Schreiben an geteilte -Dokumente ohne Konfliktschutz. Das ist der Ausgangszustand aus §2. +*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. -### D — OpenAPI der REST-API den Agenten geben +### F — OpenAPI der REST-API den Agenten geben -*Pro:* null neuer Code, die Spec existiert. -*Contra:* dieselbe Semantik-Lücke wie C, und die API spricht Volltext-`PUT` -oder Zeilen-Ops mit Prüfsumme — ein Modell, das die Prüfsumme selbst -rechnen soll, ist eine Fehlerquelle mit Ansage. -**Verworfen.** +*Contra:* dieselbe Semantik-Lücke wie E, und die Prüfsumme müsste das +Modell selbst rechnen. **Verworfen.** -### E — `#ai.dialog` zuerst (Modell im Editor) +### G — `#ai.dialog` zuerst (Modell im Editor) -*Pro:* der Nutzer bleibt im Werkzeug. -*Contra:* andere Frage (welches Modell, wessen Schlüssel, welche UI); bringt -den Plan nicht zu den Agenten, die Menschen heute schon benutzen. -**Nicht konkurrierend** — B liefert die Module, die E später auch braucht -(JSON-Baum, Leitplanken). +**Nicht konkurrierend** — D liefert die Module, die G später braucht. ## 7. Impact auf den bestehenden Code -Grundsatz: **Das Frontend wird gelesen, nicht umgebaut.** Wo doch etwas zu -ändern ist, ist es eine Verschiebung ohne Verhaltensänderung. - -### 7.1 `frontend/src/` — Änderungen +### 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`. Die Texte leben heute im UI-Modul, das DOM voraussetzt. Eine Kopie im MCP-Paket wäre die Drift, vor der D33-Nachtrag warnt. Reine Verschiebung; der Vite-Bundle bleibt gleich. | -| `warnings.js` | unverändert | importiert nur `esc` aus `render.js`; `render.js` ist headless bis zum Aufruf. | -| `parser.js` | unverändert | `parse`, `setStatusBox`, `STATUS_BY_CODE`, `SIZE_RANK` sind exportiert. | -| `model.js` | unverändert | `effectiveStatus`, `computeCheapPlan`, `assigneeLoads`, `overloadedAssignee`, `taigaSlugs`, `assumedSize` sind exportiert. | -| `live.js` | `checksum()` läuft in Node unverändert (`globalThis.crypto.subtle` gibt es ab Node 20). Prüfen, sonst Rückfall auf `node:crypto` **im MCP-Paket**, nicht in `live.js`. | Die Prüfsumme ist Pflichtfeld des Patches (D76). | -| `taiga.js` | `ticketRefOf`, `collectTicketRefs` nur lesend für `inspect` (`ticketRef`, `taigaSlug` je Knoten); `appendToken` für `set_size` | kein Taiga-Zugriff, nur die Auskunft, was im Text steht. | -| **neu** `edit.js` | Text→Text-Regeln der Knoten-Verben: `addNodeLine(text, parentId, line, afterId)`, `moveSubtree(text, id, parentId, afterId)`, `setSizeToken(text, id, size)`, `removeSubtree(text, id)`. Zeilenweise, zeichengenau, mit Tests wie `setFoldMark`/`expandShortIds`. Kennt Einrückung (§2), Fortsetzungs- und Beschreibungszeilen (§1: die wandern mit ihrem Knoten) und lässt Kommentare stehen. | Die eine Stelle für „Zeile eines Knotens umbauen“; `app.js` benutzt sie zunächst nicht, `#ai.dialog` später schon. Bewusst **nicht** in `parser.js`: Der wird sonst zum Sammelbecken. | +| `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`). | -Was **nicht** ins Frontend zurückfließt: die JSON-Serialisierung des Baums -(`mcp/inspect.js`) — sie ist eine Sicht für Agenten, keine Editor-Logik -(Nutzer-Entscheidung, §11). Sollte `#ai.dialog` sie später brauchen, -wandert sie dann. +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 Neues Paket `mcp/` +### 7.2 Backend -``` -mcp/ - package.json name: werkbaum-mcp, type: module, bin: server.js - server.js MCP-Rahmen: Resources, Tools, Prompts registrieren; stdio - stores/file.js lesen, Prüfsumme, atomar schreiben, Wurzel-Prüfung - stores/live.js GET / PATCH gegen /api/v1/documents (D76), clientId/seq - core/inspect.js Baum → JSON, Warnungen → {type, line, text}, Pfad → Stationen - core/guard.js Leitplanken (§5.5): Struktur-Warnungen, [^]-Sperre - core/verbs.js Knoten-Verben → Aufrufe von frontend/src/edit.js (Text → Text) - tests/*.test.js Vitest gegen einen In-Memory-Transport des SDK; Fixtures = SPEC §10 und der mitgelieferte Plan -``` +| 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) | -`core/` ist die **gemeinsame Tool-Schicht**: reine ES-Module ohne -Node-APIs (kein `fs`, kein `process`, keine `crypto`), Text hinein, JSON -oder Text heraus. Genau diese Dateien führt in Phase 6 auch das Backend in -GraalJS aus (§5.10, b2) — Ablagen und Transport sind je Host eigen, die -Regeln nicht. Die Regeln sind damit headless testbar — die Hausregel aus -D54-Nachtrag 3 gilt auch hier; `server.js` verdrahtet nur. +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 Backend - -**Keine Änderung nötig.** Genutzt werden `GET /documents/{id}`, -`PATCH /documents/{id}/content` und `GET /info` — alle vorhanden. Zwei -Dinge sind zu **prüfen**, nicht zu bauen: - -- Der Server sendet `displayName` — steht in der Historie (D86), gemessen - gegen ein lokales Backend. -- `clientId`-Präfix `mcp-`: Das Backend behandelt es wie jeden Client; ein - späterer Filter („nur menschliche Änderungen zeigen“) wäre ein eigener - Wunsch. - -**Falls §5.10 auf b2 fällt** (HTTP-Transport im Backend), kommt in -Phase 6 dazu — und erst dann: ein Paket `de.werkbaum.integration.mcp` mit -dem MCP-Endpunkt unter `/api/v1/mcp` (Kotlin-SDK oder Spring-AI-Starter), -ein GraalJS-Kontext, der `frontend/src/*.js` und die gemeinsame -Tool-Schicht lädt (die Dateien wandern beim Bauen ins Jar als Ressourcen — -dieselbe Quelle, keine Kopie), die Prüfsumme aus `java.security` -statt `crypto.subtle`, ein Bearer-Token-Filter in `SecurityConfiguration` -neben dem Master-Passwort, `taiga`/`mcp` in `GET /info`, und -`build.gradle.kts` mit den zwei Abhängigkeiten (Rückfragepflicht, -CLAUDE.md — mit §11 gestellt). Der Speicher-Nachweis gehört in den -DECISIONS-Nachtrag, gemessen wie in D77. - -Perspektivisch berührt: `#col.live.owner`. Sobald Verwaltungs-Aktionen an -ein Owner-Passwort gebunden sind, braucht der MCP-Server einen Weg, es zu -übergeben — **nie** als Tool-Parameter (Prozessliste, Transkript), sondern -als Umgebungsvariable des Serverprozesses. Das ist heute keine Arbeit, nur -eine Vormerkung. - -### 7.4 Abhängigkeit: `@modelcontextprotocol/sdk` - -CLAUDE.md: keine neuen Laufzeit-Abhängigkeiten ohne Rückfrage. Das ist eine. - -- **Für:** Das offizielle SDK trägt Protokollrevision, Capability-Handshake, - Schema-Validierung und beide Transporte; eine Handschrift des JSON-RPC - wäre ~200 Zeilen, die bei jeder Revision nachgezogen werden müssten. -- **Dagegen:** Es ist die erste Laufzeit-Abhängigkeit im Repo überhaupt. -- **Einordnung:** Sie liegt im **MCP-Paket**, nicht im Frontend — der - gebündelte Editor bleibt abhängigkeitsfrei (D11/D19/D20). Dieselbe - Grenze, die D76 für Spring Security gezogen hat: die Abhängigkeit dort, - wo sie den Kern nicht berührt. - -**Entschieden (Nutzer, §11): SDK** — als bewusste Ausnahme in D93 -festgehalten. Version wird beim Bauen gepinnt (`package-lock.json` -eingecheckt, wie im Frontend). - -### 7.5 Übriges +### 7.3 Übriges | Ort | Änderung | |---|---| -| `.mcp.json` (Repo-Wurzel) | Server-Eintrag für Claude Code (Dogfooding) | -| `README.md` / `README.de.md` | Abschnitt „Für KI-Agenten: der MCP-Server“ — Installation, die drei Quellen, die Leitplanken | +| `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 | -| `.github/workflows/*` | `npm --prefix mcp test` neben dem Frontend-Test; kein Deploy (lokales Werkzeug) | -| `tools/pull-doc` | **unverändert im Verhalten**; ein neuer Schalter `--with-history` (§5.7): holt `GET /history`, listet im Rumpf, wer welche Version seit dem letzten Spiegel-Commit geändert hat, und setzt die Trailer `Werkbaum-Changed-By:` und `Werkbaum-Version:` — der Schalter kommt ins Skript, nicht in den Aufrufer (D77-Nachtrag); der MCP-Server setzt ihn immer, der Cron darf | -| `scripts/deploy-*.sh`, `tools/remote` | **unverändert** — nichts davon läuft auf dem Server | -| `frontend/public/llms.md` | **unverändert** (Notation, D43); ein Verweis auf den Server gehört in `llms.txt` (Wegweiser, D43-Nachtrag 2) | +| `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; Resources sind als Inhalt, nie als - Anweisung deklariert. Der Host trägt die Prompt-Injection-Frage — der - Server macht sie nur nicht schlimmer: keine Shell, kein Netz außer den - zwei konfigurierten Zielen (Dateiwurzeln, Backend). -- **Kein Geheimnis im Transkript.** Master-Passwort (D76-Nachtrag 6) geht - **nie** über MCP — `GET /documents` (die Liste) wird deshalb nicht - angeboten; der Agent bekommt Dokument-URLs vom Menschen. Ein späteres - Owner-Passwort kommt als Umgebungsvariable (§7.3). Taiga-Token ebenso - (§9). -- **Dateizugriff nur unter erlaubten Wurzeln**; Pfade werden aufgelöst - (`realpath`) und gegen die Wurzeln geprüft — Symlinks nach draußen - zählen als draußen. -- **Schreiben nur mit `base`**; kein Volltext-`PUT`. Was der Editor an - Netzen hat (Konflikt-Band, Rettungs-Sicherung, D89), hat der Agent in - Form des zurückgelieferten aktuellen Textes — er verliert nichts, er - muss neu ansetzen. + nichts aus, was im Text steht — GraalJS läuft **ohne** Host-Zugriff + (kein `HostAccess`, 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 — `document` ist 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 `base`** auf 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` | -| 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, wenn gewünscht: über den vorhandenen Proxy, Token aus der Umgebung | -| Dokumentenliste (`GET /documents`) | Master-Passwort | — (der Mensch nennt die URL) | -| Anlegen/Löschen/Umbenennen von Server-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 ohnehin | — | -| Änderungsfeed / Long Polling | Ein Agent arbeitet in Zügen, nicht live; `read` vor jedem Schreiben genügt | — | -| Git als **Quelle** (Server folgt dem Repo) | Umkehrung von §5.7; Konfliktmarker mitten in der Notation, eigene Fragen | `#col.git.pr`, `#col.git.auto` | -| `?sourceUrl=`-Quellen | nur lesend; der Agent kann die URL selbst holen | Nachtrag bei Bedarf | -| Streamable HTTP | Phase 6, hängt an der Authentifizierungsfrage | `#ai.mcp.http` | -| Ein Kotlin-Server **mit eigenem Parser** | D14 — das Backend darf den Transport tragen (§5.10, GraalJS), nie eine zweite Grammatik | `#ai.mcp.kotlin` (`[-]`) | +| 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]`. -1. **Lesen** (`#ai.mcp.node`, `#ai.mcp.read`): Paket, stdio, Resources - für Guide und Datei, `werkbaum_read`. Nachweis: Claude Code liest den - mitgelieferten Plan über die `.mcp.json`. Vorher: SDK-Entscheidung - (§7.4). -2. **Befragen** (`#ai.mcp.inspect`): `i18n.js`-Verschiebung im Frontend, - `werkbaum_inspect` mit Baum, Warnungen, Pfad. Nachweis: Für den - mitgelieferten Plan liefert das Tool dieselbe Stationszahl und dieselben - Warnungen wie der Editor (Vergleich gegen die Zahlen, die die - DECISIONS-Einträge ohnehin nennen). -3. **Schreiben** (`#ai.mcp.write`, `#ai.mcp.guard`): Server-Ablage mit - `PATCH /content`, Datei-Ablage atomar, die drei Basis-Schreib-Tools, die - Leitplanken. Nachweis gegen ein lokales Backend: Agenten-Änderung - erscheint im offenen Editor mit „Claude Code“ in der Historie; ein - Konflikt (Mensch tippt dieselbe Zeile) kommt als Fehler mit aktuellem - Text zurück, nichts wird überschrieben; ein `[^]` wird abgelehnt; ein - `mixedGate` wird abgelehnt, ein `sizeConflict` geschrieben und gemeldet. -4. **Knoten-Verben** (`#ai.mcp.verbs`): `frontend/src/edit.js` mit Tests - (Gegenprobe per Mutation wie bei `setFoldMark`), dann die vier Verben - im Paket. Nachweis: Ein `move_node` nimmt Beschreibungsblock und - Fortsetzungszeilen mit und lässt den Rest des Texts zeichengenau stehen - (Vergleich der übrigen Zeilen vorher/nachher). -5. **Spiegel ins Git** (`#ai.mcp.mirror`): `--mirror`, Aufruf von - `pull-doc --git-commit` nach jedem Agenten-Schreibvorgang, Umleitung - von Schreibzugriffen auf die Spiegel-Datei. Nachweis in einem - Wegwerf-Worktree: ein Agenten-Zug ergibt genau einen Commit mit - Serverstand und Urheber; ein unveränderter Zug keinen; eine von Hand - verschmutzte Spiegel-Datei wird vom nächsten Commit überschrieben und - die Abweichung steht im Diff. -6. **Prompts und HTTP** (`#ai.mcp.http`, optional): die zwei Prompts; - Streamable HTTP erst nach `#col.live.owner`, und davor die zwei - Messungen aus §5.10 — der GraalJS-Spike (laufen `parser.js`, - `model.js` und `mcp/core/*` als ES-Module in der JVM, was kostet der - Kontext auf der Zielumgebung) und der SSE-Strom durch den Apache. Fällt - der Spike durch, trägt das Node-Paket den Transport (`--http`) als - eigener Dienst. Nachweis: derselbe Werkzeugaufruf liefert über stdio - und über HTTP dasselbe JSON; ohne Bearer-Token 401; ein `--root`-Pfad - wird über HTTP gar nicht erst angeboten. +0. **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.** +1. **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`, `check` in beiden Formen. Nachweis: Claude Code liest über + die `.mcp.json` den mitgelieferten Plan und bekommt 231 Knoten, + 0 Warnungen, dieselben Stationen wie der Editor; ohne Token 401. +2. **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; veraltete `base` ⇒ Fehler mit aktuellem + Text, nichts überschrieben; `[^]` abgelehnt; `mixedGate` abgelehnt, + `sizeConflict` geschrieben und gemeldet; Text-Form liefert + `blocking: true` und trotzdem den Text. +3. **Knoten-Verben** (`#ai.mcp.verbs`): `edit.js` mit Tests (Gegenprobe + per Mutation wie bei `setFoldMark`), dann die vier Verben. Nachweis: + `move_node` nimmt Beschreibungsblock und Fortsetzungszeilen mit und + lässt den Rest zeichengenau stehen. +4. **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. +5. **Prompts** (§5.8). -## 11. Entscheidungen (Multiple-Choice-Runde, 2026-09-02) +## 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 | |---|---|---| -| SDK-Abhängigkeit `@modelcontextprotocol/sdk` (§7.4) | **Ja, SDK** | erste Laufzeit-Abhängigkeit, isoliert im MCP-Paket; D93 | -| Strenge der Warnungs-Leitplanke (§5.5) | **Nur Struktur-Warnungen blockieren** | `mixedGate`, `unknownStatus`, `descStray`, `duplicateId`; alles Inhaltliche wird geschrieben und gemeldet. 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“, `--name` übersteuert | -| Geteiltes Dokument und Git (§5.7) | **Server committet je Agenten-Zug** | über `pull-doc --git-commit`; Cron für menschliche Änderungen dazwischen | -| Ort der Baum-Serialisierung (§7.1) | **Im MCP-Paket** | wandert, wenn `#ai.dialog` sie braucht | -| Schreib-Tools (§5.4) | **Zusätzlich Knoten-Verben** | `add_node`, `move_node`, `set_size`, `remove_node`; Regeln in `frontend/src/edit.js`, nicht im Paket | -| Verteilung | **Nur aus dem Repo** | `.mcp.json`; npm erst auf Nachfrage von außen | +| 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 | -**Zweite Runde** (Nutzer: Architektur-Entscheidungen jetzt diskutieren, -auch wenn sie erst später gebaut werden — §5.7 und §5.10): +**Runde 2** (Nutzer: Architektur-Entscheidungen jetzt diskutieren) | Frage | Entschieden | Anmerkung | |---|---|---| -| Urheber im Spiegel-Commit (§5.7) | **Historie im Rumpf + Trailer** | `Werkbaum-Changed-By:` je Beteiligtem, `Werkbaum-Version:`; Git-Autor bleibt der Betreiber; gilt auch für den Cron-Commit (`pull-doc --with-history`) | -| HTTP: Ort (§5.10) | **Im Backend, das die JS-Module per GraalJS ausführt** — Spike vorbehalten | Kotlin ist Transport und Zugang, die Logik bleibt einmal vorhanden; fällt der Spike durch (ESM in GraalJS, Speicher auf dem Host), Rückfall auf den Node-Dienst hinter Apache ohne Umbau der Tool-Schicht | -| HTTP: Zugang (§5.10) | **Statisches Bearer-Token** | `WERKBAUM_MCP_TOKEN`; OAuth 2.1 erst mit einem IdP | -| HTTP: Umfang (§5.10) | **Nur Dokumente** | keine Datei-Ablage, kein Git-Spiegel über HTTP; echte Teilmenge der lokalen Fassung | +| 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 | -**Weiterhin offen** (stellt sich erst beim Bauen oder danach): +**Runde 3** (Nutzer: neue Technologien sind eine Dimension schwerer als +Abhängigkeiten; erst die Beweggründe, dann die Entscheidung) -1. **Prompts** (§5.8) — ob zwei genügen und wie sie heißen; Phase 6. -2. **Der GraalJS-Spike** — laufen `parser.js`/`model.js` und die - Tool-Schicht als ES-Module in der JVM, und was kostet der Kontext an - Speicher auf der Zielumgebung (D76-Nachtrag 3)? Messung vor Phase 6; - das Ergebnis entscheidet zwischen b2 und a (§5.10) und gehört als - Nachtrag zu D93. -3. **SSE durch den Apache** — Messung vor Phase 6 (§5.10). +| 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.js` weiter — unverändert + im Zuschnitt, nur ohne eigenes Paket.