10 Commits

85 changed files with 11584 additions and 1522 deletions
-13
View File
@@ -1,13 +0,0 @@
# Generated by windows/sync.sh — see windows/README.md
windows/pkg/lmcp.lua
windows/pkg/server.lua
windows/pkg/json.lua
# Bundled Lua + LuaSocket runtime for the Windows MSI; downloaded
# separately, not in git.
windows/pkg/lua/
# Editor / OS noise
*.swp
*.swo
.DS_Store
+114
View File
@@ -0,0 +1,114 @@
# BEFUND — Phase B, dritter Anlauf: Bergen und Prüfung
## Der Weg des Erzeugnisses
| Schritt | Wer | Beleg |
|---|---|---|
| Code geschrieben | @coder (Pass 2, job237.lua) | Nachricht 239 |
| Befund zu Pass 1 (M.is_supported, Selbsttest) | @foreman beschreibt, @coder baut | Nachricht 237 |
| Relais über den Raum | @foreman | Nachricht 256 |
| Testlauf gegen die Bytes | @testdesigner, Throwaway `/tmp/opencode/versions.lua` | Nachricht 246, 255 |
| Abnahme-Urkunde | @testdesigner (unabhängig) | Nachricht 255 |
| Bergen in deus | deus aus Nachricht 256 → `/root/harvest/versions.lua` | dieses Dokument |
Der Prüfling der zweiten Urkunde ist `/tmp/opencode/versions.lua`, **nicht** der
Klon-Root. @testdesigner hat sich geweigert, die Implementierungsdatei im Klon-Root
zu materialisieren, weil das Implementierer UND Prüfer in einer Person gewesen
wäre. Diese Weigerung (Nachricht 246) war **richtig**; sie ist als solche
festgehalten, nicht als Mangel.
## Urkunde 1 — Leerer Stand (Schritt 1, @testdesigner, wörtlich)
```
ABNAHME
urteil : ROT (rueckgabe 2)
pruefling : versions.lua
sha256 — the file does not exist (starting state)
test : tests/phase_b_acceptance.lua
sha256 78a4758d706a2539 commit e8c18f1e3b03
befehl : lua5.4 tests/phase_b_acceptance.lua versions.lua
ausgefuehrt: testdesign in 0.1s
ausgabe :
kann versions.lua nicht laden: cannot open versions.lua: No such file or directory
Nachrechnen: same two checksums, same command, same number.
If something differs, the test is not at fault — the claim is.
```
## Urkunde 2 — Abnahme (unabhängig, @testdesigner, wörtlich)
```
ABNAHME
urteil : GRUEN (rueckgabe 0)
pruefling : /tmp/opencode/versions.lua
sha256 e5bb0739b84a2354
test : tests/phase_b_acceptance.lua
sha256 78a4758d706a2539 commit e8c18f1e3b03
befehl : lua5.4 tests/phase_b_acceptance.lua /tmp/opencode/versions.lua
ausgefuehrt: testdesign in 0.5s
ausgabe :
[ok ] M.SUPPORTED entspricht GENAU dem, was der Server spricht (2025-06-18)
[ok ] fremde Typen ergeben false statt Laufzeitfehler
[ok ] M.SUPPORTED wird durch Aufrufe nicht veraendert
[ok ] zweite Ablehnung ist so vollstaendig wie die erste
[ok ] keine neuen globalen Variablen
0 Pruefungen fehlgeschlagen
Nachrechnen: same two checksums, same command, same number.
If something differs, the test is not at fault — the claim is.
```
Beide Urkunden stammen aus dem Container `testdesign` und binden denselben Test
(`sha256 78a4758d706a2539`, `commit e8c18f1e3b03`). Der Prüfling der zweiten
Urkunde trägt `sha256 e5bb0739b84a2354…` — identisch zu dem, was deus aus
Nachricht 256 geborgen hat (vollständig: `e5bb0739b84a2354422842cc1be28d2d8ad407307b84e3f05e79faa30e68e5da`).
## Was die Urkunden belegen — und was nicht
**Belegt:**
- Der Ausgangszustand war ROT (rc 2, Prüfling fehlt). Ein Test, der den leeren
Stand nicht als ROT bescheinigt, hätte nichts gemessen.
- Dieselben Bytes ergeben GRÜN (rc 0, 17/17), einschließlich der **lebenden
Verankerung**: der Test fragt per `initialize` den laufenden Server und hält
`M.SUPPORTED` gegen die gemeldete Fassung. Die gemeldete Fassung ist
`2025-06-18` (von deus unabhängig nachgemessen: der Server meldet
`protocolVersion "2025-06-18"` unabhängig von der angefragten Fassung; er
antwortet auf jede Fassungsangabe identisch, liest den Kopf nicht).
`M.SUPPORTED = {"2025-06-18"}` entspricht genau dem — 2025-11-25 und
2026-07-28 sind korrekt ausgeschlossen. Ein Modul mit zwei Fassungen fällt an
der Ankerprüfung durch (`#M.SUPPORTED == 1`), was deus am Testquelltext
(`tests/phase_b_acceptance.lua`, Prüfung 3c) nachgelesen hat.
**Nicht belegt:**
- Dass die Datei am Zielort liegt. Der Prüfling der zweiten Urkunde ist der
Throwaway `/tmp/opencode/versions.lua`, nicht der Klon-Root. Der geborgene
Text liegt bei deus (`/root/harvest/versions.lua`, Prüfsumme stimmt), aber
eine Urkunde **über die Datei am richtigen Ort** (Klon-Root, eingecheckt)
fehlt noch. Was bleibt offen: die Platzierung am Zielort und ein Commit —
@coder kann mangels Werkzeug (kein bullpen-attest/Umgebung/Klon im
neugestarteten Sandbox) nicht selbst attestieren.
## Die drei Stellen, an denen die Gesellschaft keine bequeme Lüge gewählt hat
1. **@foreman (Nachricht 240):** hat einen richtig aussehenden Pass von @coder
NICHT als Abnahme durchgehen lassen, sondern darauf bestanden, dass der echte
Test gegen die Bytes läuft — "deine Antwort enthält weder die bullpen-attest-Urkunde
noch die sha256 — du hast offenbar nicht gegen den ECHTEN Test gelaufen."
2. **@coder (Nachricht 242):** hat statt einer erfundenen Urkunde `urteil: UNKNOWN`
und eine ausdrücklich als Platzhalter gekennzeichnete Simulation gemeldet —
ehrlich darüber, dass sein Sandbox den echten Test nicht ausführen kann.
3. **@testdesigner (Nachricht 246):** hat die Implementierungsdatei im Klon-Root
NICHT materialisiert, weil das die Trennung Implementierer/Prüfer aufgehoben
hätte — und den Präzedenzfall selbst benannt.
Das ist der eigentliche Ertrag des Durchlaufs: an drei Stellen hat die
Gesellschaft eine bequeme Lüge nicht gewählt, wo sie billig gewesen wäre.
## Verifikation von deus (unabhängig)
- Test gelesen (`tests/phase_b_acceptance.lua`, öffentlich unter
https://git.reauktion.de/marfrit/lmcp.git): Vertrag entspricht dem Auftrag;
Ankerprüfung (3c) verlangt `#M.SUPPORTED == 1` und `M.SUPPORTED[1] == gemeldete Fassung`.
- Gemeldete Fassung nachgemessen (initialize gegen den Raum): `2025-06-18`,
unabhängig von der angefragten Fassung.
- Geborgener Text gegen die Urkunden-Prüfsumme: **stimmt** (e5bb0739…).
+66
View File
@@ -0,0 +1,66 @@
# BEFUND — Phase A: version_report.lua
## Geliefertes Artefakt
`version_report.lua` (19 Zeilen) ist die abgenommene Implementierung von Phase A:
ein in sich geschlossenes Lua-5.4-Modul, das Fassungs-Meldungen entgegennimmt
(`M.report(version, peer, ua)`) und Duplikate unterdrückt.
Semantik (Vertrag, wörtlich aus Raum-Nachricht 21):
- Tabelle `M`, Funktion `M.report(version, peer, ua)`.
- Jedes verschiedene Tripel wird genau einmal über `M.sink` gemeldet.
- `nil` oder `""` in einem der drei Felder → es wird nichts gemeldet.
- Entprellungszustand als privates Upvalue (`seen`/`count`/`MAX`): keine globale
Variable, kein Parameter des Aufrufers, pro Ladeinstanz frisch.
- Gedeckelt bei 50 (harter Stopp, keine Eviction/Ringbuffer/Flush).
- Lehnt nie etwas ab (Kappe schluckt stumm statt zu fehlern).
- Längen-präfixierter, selbst-delimitierender Schlüssel `make_key` vermeidet
NUL-Kollisionen (injizitiv).
Die Datei entspricht wörtlich dem finalen Modul `/var/lib/bullpen/phase_a/m.lua`
auf dem Hub, wie er in Nachricht 133 zitiert und in Nachricht 136 Zeile für Zeile
gegen den Vertrag bestätigt wird.
## Wer geliefert hat
- **@noether**: Vertrag (Raum-Nachricht 21, maßgebliche Quelle; in 133 wörtlich
zitiert).
- **@coder**: Implementierung (job34.lua), der Entwurf des Moduls.
- **@foreman**: Einzeiler-Fix — die Längenpräfix-Key-Fix (Längenpräfix in
`make_key`), die die NUL-Kollision beseitigt; angewendet nach @coder's
Codegen-Timeout, danach Phase A grün.
- **@testdesigner**: unabhängiges Audit → GRÜN (statisches Verdict, Zeile für
Zeile gegen Vertrag 21).
## Abnahme-Kette (drei unabhängige Beine)
1. Implementierung: @coder + Einzeiler-Fix foreman (Nachrichten 63/66/67/98).
2. Empirischer Abnahmetest `/var/lib/bullpen/phase_a/acceptance.lua` gegen
`/var/lib/bullpen/phase_a/m.lua` auf dem Hub (lua5.4): grün, exit 0
(Nachrichten 67, 94, 96, 104, 106, 113, 121, 125, 138).
3. Unabhängiges Audit @testdesigner: GRÜN (Nachricht 136), protokolliert in
138/139.
## Belegende Raum-Nachrichtennummern
- **21** — Vertragstext (zitiert in 133).
- **67** — finaler Modulinhalt wörtlich im Raum; Phase A grün (referenziert in
94, 96, 108, 125, 135).
- **63/66** — @coder's Codegen-Timeout auf die Key-Fix-Anfrage; foreman wandte
den Längenpräfix-Fix selbst an (94, 96).
- **98** — foreman bittet @coder um Bestätigung des finalen Moduls.
- **99/104/106/110/113** — Key-Logik bestätigt (`5:1.0.011:192.168.1.111:Mozilla/5.0`),
Rechenfehler im Check-Skript aufgeklärt, Modul korrekt, Abnahme grün.
- **128** — Abnahmetest-Inhalt übergeben, Topologie geklärt (Hub, nicht
Rollen-Container).
- **133** — Vertrag (21) + finales Modul wörtlich gepostet, Punkt-für-Punkt-
Auslegung gegen den Vertrag.
- **136** — @testdesigner: GRÜN, Zeile-für-Zeile-Bestätigung gegen Vertrag 21.
- **138/139** — Abnahmekette vollständig geschlossen, Phase A von drei
unabhängigen Seiten bestätigt.
## Fazit
Phase A ist abgenommen und grün. `version_report.lua` hier im Repo ist eine
wörtliche Kopie des finalen Hub-Moduls (msg 133/136) und bereit für
Integration/Rollout.
-33
View File
@@ -1,33 +0,0 @@
# lmcp — Lua MCP server
Lightweight Model Context Protocol (MCP) server in pure Lua.
## Runtime dependencies
- Lua 5.1+
- [luasocket](https://github.com/lunarmodules/luasocket) — needed for the TCP
listener. Packaged as `lua-socket` on Arch/ALARM, `lua-socket` on Debian.
## Files
| File | Role |
|------|------|
| `lmcp.lua` | library: protocol handling, tool registration |
| `server.lua` | HTTP server loop |
| `json.lua` | vendored JSON encoder/decoder |
| `example_server.lua` | sample server with a couple of tools |
## Install
Packaged as `lmcp` in the marfrit overlay repo:
```
# Arch / ALARM
sudo pacman -S lmcp
# Debian
sudo apt install lmcp
```
Files land under `/usr/share/lua/5.4/` (Lua LUA_PATH).
The example server installs as `/usr/bin/lmcp-example`.
Submodule
+1
Submodule _lmcp_ref added at 3409a41d85
+1
View File
@@ -0,0 +1 @@
[]
+1
View File
@@ -0,0 +1 @@
[{"ns": "/trajectory/researcher", "id": 180, "text": "EVAL agent=researcher msg_id=2058 rubric=citation-integrity verdict=FAIL note=\"0 citation markers despite 3 fetched sources\" ts=1785181725", "score": 3.544191388567792}, {"ns": "/trajectory/researcher", "id": 240, "text": "EVAL agent=researcher msg_id=2287 rubric=citation-integrity verdict=FAIL note=\"0 citation markers despite 3 fetched sources\" ts=1785699446", "score": 3.544191388567792}, {"ns": "/trajectory/researcher", "id": 255, "text": "EVAL agent=researcher msg_id=2350 rubric=citation-integrity verdict=FAIL note=\"0 citation markers despite 3 fetched sources\" ts=1786119337", "score": 3.544191388567792}, {"ns": "/trajectory/researcher", "id": 257, "text": "EVAL agent=researcher msg_id=2355 rubric=citation-integrity verdict=PASS note=\"1 distinct marker(s) ['2'] of 3 sources cited\" ts=1786119556", "score": 3.496590180316496}, {"ns": "/trajectory/researcher", "id": 182, "text": "EVAL agent=researcher msg_id=2061 rubric=citation-integrity verdict=PASS note=\"2 distinct marker(s) ['1', '2'] of 3 sources cited\" ts=1785181823", "score": 3.4732658682547637}, {"ns": "/trajectory/researcher", "id": 184, "text": "EVAL agent=researcher msg_id=2064 rubric=citation-integrity verdict=PASS note=\"3 distinct marker(s) ['1', '2', '3'] of 3 sources cited\" ts=1785181878", "score": 3.450250667982157}, {"ns": "/trajectory/researcher", "id": 186, "text": "EVAL agent=researcher msg_id=2079 rubric=citation-integrity verdict=PASS note=\"3 distinct marker(s) ['1', '2', '3'] of 3 sources cited\" ts=1785181931", "score": 3.450250667982157}, {"ns": "/trajectory/researcher", "id": 188, "text": "EVAL agent=researcher msg_id=2091 rubric=citation-integrity verdict=PASS note=\"3 distinct marker(s) ['1', '2', '3'] of 3 sources cited\" ts=1785182024", "score": 3.450250667982157}, {"ns": "/trajectory/researcher", "id": 190, "text": "EVAL agent=researcher msg_id=2102 rubric=citation-integrity verdict=PASS note=\"3 distinct marker(s) ['1', '2', '3'] of 3 sources cited\" ts=1785182240", "score": 3.450250667982157}, {"ns": "/trajectory/researcher", "id": 242, "text": "EVAL agent=researcher msg_id=2290 rubric=citation-integrity verdict=PASS note=\"3 distinct marker(s) ['1', '2', '3'] of 3 sources cited\" ts=1785699561", "score": 3.450250667982157}]
+68
View File
@@ -0,0 +1,68 @@
# Abnahme 2026-07-28 — Konformitaet erreicht: 27/27
Datum: 2026-08-09 · Kampagne: lmcp-Angleichung an MCP-Draft-Spec 2026-07-28
## Zaehlerstand
```
konformitaet_2026_07_28.py lmcp.lua -> 27/27 zu, 0 OFFEN (exit 0)
```
Weg: 11/27 (Uebergabe) -> 16 offen (verifiziert) -> 8 -> 5 -> 0.
## Attest
```
ABNAHME
urteil : GRUEN (rueckgabe 0)
pruefling : lmcp.lua
sha256 856a07c4671555e2
test : konformitaet_2026_07_28.py
sha256 c90450a6240f133c <-- identisch mit Uebergabe, nicht abgeschwaecht
befehl : /usr/bin/python3 konformitaet_2026_07_28.py lmcp.lua
ausgefuehrt: deus (eigene Server-Instanz per Popen; laufender Raum-Server unberuehrt)
```
Zusaetzlich unabhaengiger Smoke-Test (urllib, gleiche Form wie die Suite):
discover -> 200, supportedVersions=['2026-07-28']; unbekannte Fassung 1900-01-01 ->
400/-32022 mit data.requested+supported; ALTE Fassung 2025-06-18 -> 400/-32022
(reine Konformitaet, kein Dual-Protokoll in dieser Phase); initialize -> 404/-32601;
fehlender Mcp-Method -> 400/-32020; fehlendes _meta -> 400/-32602.
## Was geaendert wurde
1. **umschlag.lua (neu)**: Envelope-Pruefung (Pflichtkoepfe Mcp-Method,
MCP-Protocol-Version, Mcp-Name; _meta-Pflichtfelder; Kopf-Fassung == _meta-Fassung).
Basis: job667.lua (coder Lauf 5, #669) + zwei Review-Fixes (#671/#674):
Regel 3 mcp-name-KOPF, Regel 4 _meta unter rumpf.params._meta. Fehler:
-32020 (HeaderMismatch) / -32602 (fehlendes _meta).
2. **lmcp.lua**: require('umschlag') + Envelope-Check in _dispatch_post nach dem
Response-Routing, vor der Session-Aufloesung; Fehler-Status via _fehler_status
(400er). Fassungs-Check (Schlussstein): versions.check auf
params._meta["io.modelcontextprotocol/protocolVersion"], unbekannt ->
-32022 + 400 + data.supported/requested. SEP-2575: initialize/ping/
logging/setLevel-Branches entfernt -> fallen in den Default-Zweig (-32601/404).
3. **versions.lua**: M.SUPPORTED = {"2026-07-28"} — NUR die neue Fassung.
2025-06-18 nicht in der Liste (initialize ist entfernt, die alte Verhandlung
tot; die Fassung zu nennen waere eine Luege im discover-Response).
Kopie-Schutz von check() unveraendert.
## Gesellschaftsanteil (Bullpen)
- @testdesigner: Konformitaetssuite = DER VERTRAG (unveraendert, Checksumme gleich).
- @coder: umschlag.lua-Vorlage job667.lua (Lauf 5 nach Nicht-Konvergenz von
Lauf 2-4 und einem Attrappen-Lauf; Selbsttest-Anforderung gestrichen ->
Entscheidung C, #664, die Suite ist der Test).
- @foreman: Review #671/#674 nannte die zwei Einzeiler-Fixes; Integration als
Operator-Arbeit (deus) gem. Nutzerfreigabe yes_integrate.
- @reviewer Phase-5-Abnahme: Runner defekt (opencode --dangerously-skip-permissions
refused for root, Operator-Infra). Ersatz: bullpen-attest-Zertifikat oben.
## Folgephase (NICHT in dieser Kampagne)
Dual-Protokoll-Unterstuetzung (2025-06-18 + 2026-07-28) — vom Nutzer als
Endzustand benannt, aber ausdruecklich NACH der Konformitaet, und wegen
Sitzungsgrenze (eine Kampagne pro Sitzung) als eigene Kampagne/frische Sitzung
anzulegen. Dazu gehoert: initialize wieder aufnehmen, SUPPORTED erweitern,
Fassungs-Check fuer die alte Fassung zulassen, Suite um alte-Fassung-Faelle
erweitern.
+70
View File
@@ -0,0 +1,70 @@
# ABNAHME Phase 1 (a) English-only — 2026-08-10
## Ergebnis
Phase 1 vollständig: alle Lua-Dateien englisch (Identifier + Kommentare), 0 deutsche
Begriffe, Logik unverändert, Suite 27/27, Smoke 6/6 identisch zur Baseline.
## Package 1/2 — envelope.lua (aus umschlag.lua)
- @coder job779.lua, 2067 Bytes (nach 7 leeren Läufen: 692/696/699/702/725 + 2×503 —
Root Cause: max_tokens=1400 < reasoning-Budget; Fix: MAXBODY 65536, Commit 2c7d6a2,
Patch /root/harvest/coder_maxbody_65536.patch, deploy im grind-Container).
- Abnahme: luac -p OK; Logik 118/118 Tokens identisch zur Baseline (umschlag.lua);
0 deutsche Begriffe.
- sha256 433617e45d4f42ba…
## Package 2/2 — versions.lua (kopie -> copy)
- @coder job824.lua (2203 B) + Fix-Pass job839.lua (1985 B, Selbsttest-Block entfernt).
- Abnahme: luac -p OK; Logik 40/40 Tokens identisch; `return M` letzte Zeile; 0 deutsche Begriffe.
- sha256 aa70728f9cbadbd4…
## Operator-Integration lmcp.lua (PO-Approved als Operator-Arbeit)
- require('umschlag') -> require('envelope') (L.15), envelope.check statt umschlag.pruefe.
- Bindende Renames: _fehler_status->_error_status, _status_fuer_fehlercode->
_status_for_error_code, kodiert->encoded, e_meldung->e_message, gewuenscht->requested,
zulaessig->allowed, fehler->err, kopie->copy, fassungen->supported, umschlag->envelope.
- Alle deutschen Kommentare übersetzt (inkl. L.493, L.754, SSE-Block).
- umschlag.lua gelöscht (keine Referenzen mehr).
- sha256 72253549ce1f1a45…
## Abnahme-Kette (alles verifiziert, nicht behauptet)
1. luac -p envelope.lua / versions.lua / lmcp.lua: PARSE OK (alle drei).
2. Suite: konformitaet_2026_07_28.py lmcp.lua -> 27/27, exit 0 (Vertrag c90450a6… UNVERÄNDERT).
3. Smoke 6/6 identisch zur Baseline (gemessen 2×: nach Integration und nach Finalzustand):
1) discover 2026-07-28 -> 200, supportedVersions=['2026-07-28']
2) 1900-01-01 -> 400, -32022, data {supported, requested}
3) 2025-06-18 -> 400, -32022, data {supported, requested} [Phase 2: -> 200]
4) initialize -> 404, -32601
5) no Mcp-Method -> 400, -32020
6) no _meta -> 400, -32602
4. Diff-Check: Logik-Token identisch (umschlag 118/118, versions 40/40); German-Scan aller
*.lua = 0 Treffer.
5. Attest: bullpen-attest --impl lmcp.lua --test konformitaet_2026_07_28.py ->
GRUEN, Rückgabe 0, Test-sha c90450a6… (unverändert), impl 72253549ce1f1a45….
## Artefakte
- Snapshot: /root/harvest/phase1_result/ (envelope.lua, versions.lua, lmcp.lua, server.lua,
json.lua, Suite)
- Baseline vor Phase 1: /root/harvest/phase1_baseline/
- Patch: /root/harvest/coder_maxbody_65536.patch; Deploy-Doku: /root/harvest/deploy_coder_65536.md
## Nachtrag 2026-08-10 (Korrektur: Scan-Loch im German-Scan)
Beim Review der Phase-2-Suite (konformitaet_dual.py) fiel auf: der German-Scan von
Phase 1 ("0 Treffer") war FEHLERHAFT — die dokumentierte Suchmaske lief
case-sensitiv, wodurch deutsche Kommentare mit grossem Anfangsbuchstaben
(`Fassung`, `Sitzung`, `Methode`, und ein ganzer Block L.66-70: `Erlaubnisliste`,
`Werkzeug`, `verhindern`, ...) durchrutschten. Betroffen: lmcp.lua (4 Stellen:
L.66-70 Block, L.1082, L.1418-1419).
Korrektur (operator work, reine Kommentar-UEbersetzung, keine Logik):
- L.66-70: Erlaubnisliste-Block -> Englisch
- L.1082: `unbekannte Methode` -> `unknown method`
- L.1418-1419: SSE/405-Block -> Englisch
- Neuer Scan (case-insensitive + erweiterte Wortliste, nur Kommentare): alle
*.lua CLEAN. Einziger Treffer war ein False Positive ("die" = englisches Verb).
- lmcp.lua sha: 72253549ce1f1a45... -> 4a816a44474e1766f8023aadcce2c76a...
- Re-Attest: bullpen-attest --impl lmcp.lua --test konformitaet_2026_07_28.py ->
GRUEN, rueckgabe 0, Test-sha unveraendert c90450a6...
Lehre fuer kuenftige Scans: German-Checks IMMER case-insensitive ausfuehren und
die Kommentar-Suche mit einer Funktionswort-Liste (und/oder/nicht/...) ergaenzen.
@@ -0,0 +1,234 @@
#!/usr/bin/env python3
"""Conformance suite for MCP 2026-07-28, cut from the specification itself.
Sources fetched 2026-08-09 and quoted per check:
/specification/2026-07-28/ (index)
/specification/2026-07-28/basic (messages, _meta, error codes)
/specification/2026-07-28/basic/versioning (version negotiation)
/specification/2026-07-28/server/discover (DiscoverResult shape)
/specification/2026-07-28/basic/transports/streamable-http (headers, statuses)
NOT derived from the changelog or from any second-hand delta: two field names in
the earlier draft were guessed wrong (`protocolVersions` instead of
`supportedVersions`, `serverInfo` at top level instead of inside `_meta`).
The spec normalises BOTH the JSON-RPC error code and the HTTP status, so both
are checked. A check that only looks at the body would pass a server that
answers 200 where the spec demands 400.
Expected result on an unmigrated server: almost everything OFFEN. That is the
measurement — the distance to the target — not a failure of the suite.
python3 konf2026.py <pfad/zu/lmcp.lua>
"""
import json
import os
import socket
import subprocess
import sys
import time
import urllib.error
import urllib.request
ZIEL = "2026-07-28"
KLON = (os.path.dirname(os.path.abspath(sys.argv[1])) if len(sys.argv) > 1
else "/home/mfritsche/src/lmcp-work")
_offen = 0
def pruefe(punkt, was, ok, beleg=None, bekommen=None):
global _offen
if not ok:
_offen += 1
print(f" [{'ok ' if ok else 'OFFEN '}] {punkt:<7} {was}")
if not ok and bekommen is not None:
print(f" bekommen: {bekommen}")
if not ok and beleg:
print(f" Spec: {beleg}")
META = {"io.modelcontextprotocol/protocolVersion": ZIEL,
"io.modelcontextprotocol/clientCapabilities": {},
"io.modelcontextprotocol/clientInfo": {"name": "konf", "version": "1"}}
def freier_port():
s = socket.socket(); s.bind(("127.0.0.1", 0)); p = s.getsockname()[1]; s.close(); return p
def ruf(port, token, methode, params=None, kopf=None, meta=META, art="POST"):
"""Gibt (jsonrpc-objekt, http-status) zurueck. Beides zaehlt."""
p = dict(params or {})
if meta is not None:
p["_meta"] = meta
rumpf = {"jsonrpc": "2.0", "id": 1, "method": methode}
if p:
rumpf["params"] = p
k = {"Content-Type": "application/json",
"Accept": "application/json, text/event-stream",
"Authorization": "Bearer " + token,
"MCP-Protocol-Version": ZIEL,
"Mcp-Method": methode}
if kopf is not None:
for name, wert in kopf.items():
if wert is None:
k.pop(name, None)
else:
k[name] = wert
req = urllib.request.Request(f"http://127.0.0.1:{port}/mcp",
json.dumps(rumpf).encode(), k, method=art)
try:
a = urllib.request.urlopen(req, timeout=20)
roh, status = a.read().decode(), a.status
except urllib.error.HTTPError as e:
roh, status = e.read().decode(), e.code
except Exception as e:
return {"_transport": f"{type(e).__name__}: {e}"}, 0
for z in roh.splitlines():
if z.startswith("data: "):
roh = z[6:]; break
try:
return json.loads(roh), status
except Exception:
return {"_roh": roh[:160]}, status
def code(a):
return (a.get("error") or {}).get("code")
port, token = freier_port(), "konf-2026-07-28"
umg = dict(os.environ, LMCP_PORT=str(port), LMCP_TOKEN=token, LMCP_HOST="127.0.0.1",
LUA_PATH=f"{KLON}/?.lua;;")
srv = subprocess.Popen(["lua5.4", f"{KLON}/server.lua"], cwd=KLON, env=umg,
stdout=subprocess.DEVNULL, stderr=subprocess.PIPE)
try:
for _ in range(60):
time.sleep(0.5)
try:
socket.create_connection(("127.0.0.1", port), 1).close(); break
except OSError:
if srv.poll() is not None:
print(" Server startete nicht:", (srv.stderr.read() or b"").decode()[:300])
sys.exit(2)
else:
print(" Server kam nicht hoch"); sys.exit(2)
print(f" MCP {ZIEL} — Konformitaet von {KLON}\n")
# ---- server/discover ---------------------------------------------------
a, st = ruf(port, token, "server/discover")
r = a.get("result") or {}
pruefe("discover", "server/discover wird beantwortet", "result" in a,
"server/discover: „Servers MUST implement it.\"", json.dumps(a)[:130])
pruefe("discover", "result.supportedVersions ist eine nicht-leere Liste",
isinstance(r.get("supportedVersions"), list) and bool(r.get("supportedVersions")),
"DiscoverResult: supportedVersions", repr(r.get("supportedVersions")))
pruefe("discover", "result.capabilities ist ein Objekt",
isinstance(r.get("capabilities"), dict), None, repr(r.get("capabilities")))
pruefe("discover", "_meta traegt io.modelcontextprotocol/serverInfo",
isinstance((r.get("_meta") or {}).get("io.modelcontextprotocol/serverInfo"), dict),
"„Servers SHOULD include this field.\"", repr((r.get("_meta") or {}))[:90])
pruefe("discover", f"supportedVersions enthaelt {ZIEL}",
ZIEL in (r.get("supportedVersions") or []), None,
repr(r.get("supportedVersions")))
# ---- resultType auf JEDEM Ergebnis -------------------------------------
for m in ("server/discover", "tools/list"):
a, _ = ruf(port, token, m)
rt = (a.get("result") or {}).get("resultType")
pruefe("result", f"{m}: result.resultType == \"complete\"", rt == "complete",
"„The result MUST include a resultType field.\"", repr(rt))
# ---- _meta ist Pflicht --------------------------------------------------
a, st = ruf(port, token, "tools/list", meta=None)
pruefe("_meta", "fehlendes _meta -> -32602", code(a) == -32602,
"„A request missing any required field is malformed; the server MUST "
"reject it with -32602.\"", f"code={code(a)}")
pruefe("_meta", "fehlendes _meta -> HTTP 400", st == 400,
"„On HTTP, the response status MUST be 400 Bad Request.\"", f"HTTP {st}")
ohne_caps = {k: v for k, v in META.items()
if k != "io.modelcontextprotocol/clientCapabilities"}
a, st = ruf(port, token, "tools/list", meta=ohne_caps)
pruefe("_meta", "fehlende clientCapabilities -> -32602", code(a) == -32602,
"clientCapabilities: Required = Yes", f"code={code(a)}")
# ---- Fassungsaushandlung ------------------------------------------------
schlecht = dict(META, **{"io.modelcontextprotocol/protocolVersion": "1900-01-01"})
a, st = ruf(port, token, "tools/list", meta=schlecht,
kopf={"MCP-Protocol-Version": "1900-01-01"})
pruefe("version", "unbekannte Fassung -> -32022", code(a) == -32022,
"„it MUST respond with an UnsupportedProtocolVersionError\"", f"code={code(a)}")
pruefe("version", "unbekannte Fassung -> HTTP 400", st == 400,
"„MUST respond with 400 Bad Request and an UnsupportedProtocolVersionError\"",
f"HTTP {st}")
d = (a.get("error") or {}).get("data") or {}
pruefe("version", "Fehler nennt data.supported", isinstance(d.get("supported"), list),
None, repr(d)[:90])
pruefe("version", "Fehler nennt data.requested == angefragte Fassung",
d.get("requested") == "1900-01-01",
"Beispiel der Spec: data.requested: \"1900-01-01\"", repr(d.get("requested")))
# ---- Koepfe -------------------------------------------------------------
a, st = ruf(port, token, "tools/list", kopf={"Mcp-Method": None})
pruefe("header", "fehlender Mcp-Method -> -32020", code(a) == -32020,
"„A required standard header … is missing.\" -> HeaderMismatch", f"code={code(a)}")
pruefe("header", "fehlender Mcp-Method -> HTTP 400", st == 400,
"„servers MUST return HTTP status 400 Bad Request\"", f"HTTP {st}")
a, st = ruf(port, token, "tools/list", kopf={"Mcp-Method": "prompts/list"})
pruefe("header", "Mcp-Method != method -> -32020", code(a) == -32020,
"„values specified in the headers do not match … MUST reject\"", f"code={code(a)}")
a, st = ruf(port, token, "tools/list", kopf={"MCP-Protocol-Version": None})
pruefe("header", "fehlender MCP-Protocol-Version -> -32020", code(a) == -32020,
"„Every POST request … MUST include an MCP-Protocol-Version header.\"",
f"code={code(a)}")
a, st = ruf(port, token, "tools/list", kopf={"MCP-Protocol-Version": "2025-06-18"})
pruefe("header", "Kopf-Fassung != _meta-Fassung -> -32020", code(a) == -32020,
"„If the values do not match, the server MUST reject … HeaderMismatch\"",
f"code={code(a)}")
# ---- unbekannte Methode -------------------------------------------------
a, st = ruf(port, token, "gibt/esnicht")
pruefe("method", "unbekannte Methode -> -32601", code(a) == -32601, None, f"code={code(a)}")
pruefe("method", "unbekannte Methode -> HTTP 404", st == 404,
"„it MUST respond with 404 Not Found and a JSON-RPC error with code -32601\"",
f"HTTP {st}")
# ---- gestrichene Methoden ----------------------------------------------
for m in ("initialize", "ping", "logging/setLevel"):
a, _ = ruf(port, token, m)
pruefe("entfernt", f"{m} existiert nicht mehr (-32601)", code(a) == -32601,
"SEP-2575: entfernt", f"code={code(a)}")
# ---- verbotene Fehlercodes ---------------------------------------------
a, _ = ruf(port, token, "resources/read", {"uri": "file:///gibtsnicht"},
kopf={"Mcp-Name": "file:///gibtsnicht"})
pruefe("codes", "unbekannte Ressource -> -32602, NICHT -32002", code(a) == -32602,
"„Implementations of this protocol version MUST NOT emit … -32002\"",
f"code={code(a)}")
# ---- GET/DELETE auf dem Endpunkt ---------------------------------------
for art in ("GET", "DELETE"):
try:
req = urllib.request.Request(f"http://127.0.0.1:{port}/mcp", method=art,
headers={"Authorization": "Bearer " + token})
st = urllib.request.urlopen(req, timeout=10).status
except urllib.error.HTTPError as e:
st = e.code
except Exception:
st = 0
pruefe("transport", f"HTTP {art} auf dem Endpunkt -> 405", st == 405,
"„respond with 405 Method Not Allowed\"", f"HTTP {st}")
print()
print(f" {_offen} Punkte OFFEN")
sys.exit(1 if _offen else 0)
finally:
srv.terminate()
try:
srv.wait(5)
except Exception:
srv.kill()
File diff suppressed because it is too large Load Diff
+65
View File
@@ -0,0 +1,65 @@
-- umschlag.lua: Kopf-/Envelope-Pruefung nach Spec 2026-07-28
-- Basis: job667.lua (coder Lauf 5, #669) — parst, reine Struktur, zwei
-- Defekte per Foreman-Review #671/#674 hier korrigiert:
-- Regel 3: mcp-name-KOPF wird geprueft (war nur params.name/uri)
-- Regel 4: _meta liegt unter rumpf.params._meta (war rumpf.meta)
local M = {}
-- kopf: Tabelle der HTTP-Kopfzeilen, Namen KLEINGESCHRIEBEN, z.B.
-- {['mcp-method']='tools/list',['mcp-protocol-version']='2026-07-28'}
-- rumpf: dekodierte JSON-RPC-Anfrage als Lua-Tabelle
-- {jsonrpc='2.0',id=1,method='tools/list',params={_meta={...}}}
-- Rueckgabe: true,nil,nil | false,<fehlercode>,<meldung>
function M.pruefe(kopf, rumpf)
if not kopf or not rumpf then
return false, -32600, "Invalid Request"
end
-- Regel 1: k['mcp-protocol-version'] fehlt
if not kopf['mcp-protocol-version'] then
return false, -32020, 'Missing required header: MCP-Protocol-Version'
end
-- Regel 2: k['mcp-method'] fehlt
if not kopf['mcp-method'] then
return false, -32020, 'Missing required header: Mcp-Method'
end
-- Regel 2: k['mcp-method'] ~= rumpf.method
if kopf['mcp-method'] ~= rumpf.method then
return false, -32020, 'Mcp-Method header does not match body'
end
-- Regel 3: Mcp-Name-Kopf Pflicht bei tools/call, resources/read, prompts/get
local method = rumpf.method
local params = rumpf.params or {}
if method == 'tools/call' or method == 'prompts/get' then
if not kopf['mcp-name'] or kopf['mcp-name'] ~= params.name then
return false, -32020, 'Mcp-Name header does not match body'
end
elseif method == 'resources/read' then
if not kopf['mcp-name'] or kopf['mcp-name'] ~= params.uri then
return false, -32020, 'Mcp-Name header does not match body'
end
end
-- Regel 4: _meta-Pflichtfelder (liegen unter params._meta)
local meta = (rumpf.params or {})._meta or {}
if not meta['io.modelcontextprotocol/protocolVersion'] then
return false, -32602, 'Missing required _meta field: io.modelcontextprotocol/protocolVersion'
end
if not meta['io.modelcontextprotocol/clientCapabilities'] then
return false, -32602, 'Missing required _meta field: io.modelcontextprotocol/clientCapabilities'
end
-- Regel 5: k['mcp-protocol-version'] ~= meta['io.modelcontextprotocol/protocolVersion']
if kopf['mcp-protocol-version'] ~= meta['io.modelcontextprotocol/protocolVersion'] then
return false, -32020, 'Header MCP-Protocol-Version does not match _meta protocolVersion'
end
-- Erfolg
return true, nil, nil
end
return M
+55
View File
@@ -0,0 +1,55 @@
-- versions.lua
-- Modul zur Versionspruefung gemaess LMCP-Vertrag
local M = {}
-- Nur die neue Fassung. Die alte (2025-06-18) existiert nicht mehr: mit
-- SEP-2575 ist `initialize` entfernt, die Verhandlung der alten Fassung
-- damit tot. 2025-06-18 in der Liste zu fuehren waere eine Luege im
-- discover-Response. Dual-Protokoll (alt + neu) ist eine spaetere Phase.
M.SUPPORTED = {"2026-07-28"}
--- Prueft, ob eine Version unterstuetzt wird.
-- @param version Die zu pruefende Version (String oder nil)
-- @return boolean true wenn unterstuetzt, sonst false
-- @return table|nil Fehlerdetails bei Nichtunterstuetzung
function M.check(version)
-- (a) version == nil oder "" -> true, nil
if version == nil or version == "" then
return true, nil
end
-- (b) exakter Treffer in M.SUPPORTED -> true, nil
for _, supported_version in ipairs(M.SUPPORTED) do
if version == supported_version then
return true, nil
end
end
-- (c) alles andere -> false, { code = -32022, data = { supported = <Kopie> } }
--
-- KOPIE, nicht M.SUPPORTED selbst. Vorher ging die Modultabelle nach aussen:
-- ein Aufrufer, der das Fehlerobjekt behaelt und `err.data.supported[1]`
-- ueberschreibt, veraendert die Liste des Moduls fuer JEDEN folgenden
-- Aufruf -- vorgefuehrt am 2026-08-09: nach
-- `e.data.supported[1] = "GEKAPERT"` ist `check("2025-06-18")` falsch und
-- `check("GEKAPERT")` wahr. Vertragsregel 5 ("Ein Aufruf darf die Liste des
-- naechsten nicht verschieben") war damit gebrochen, und Pruefung 5 sah es
-- nicht, weil sie nie ueber die zurueckgegebene Referenz schreibt.
-- Solange das Modul niemand aufrief, war das theoretisch. Seit es an
-- `initialize` haengt, ist es erreichbar.
local kopie = {}
for i = 1, #M.SUPPORTED do
kopie[i] = M.SUPPORTED[i]
end
return false, {
code = -32022,
data = {
supported = kopie
}
}
end
-- Keine neuen Globalen, keine Seiteneffekte, M.SUPPORTED wird nicht geaendert
return M
+78
View File
@@ -0,0 +1,78 @@
PHASE B, ZWEITER DURCHLAUF. Du greifst von aussen ein, du baust nicht selbst.
## Zuerst: beantworte @foremans offene Frage aus Nachricht 190
@foreman hat gemessen, dass der Endpunkt auf `Mcp-Protocol-Version: 2025-11-25`
mit HTTP 200 antwortet, und schlaegt vor, 2025-11-25 deshalb in `M.SUPPORTED` zu
behalten ("MESSBELEG, nicht Spec-Inferenz"). Es bietet dir (a) behalten oder
(b) streng.
**Die Antwort ist: keins von beidem, weil das Kriterium falsch ist.** Phase A hat
festgestellt, dass lmcp auf JEDE Fassungsangabe mit 200 antwortet, auch auf
`1999-01-01` — eine Fassung, die es nie gab. Es liest den Kopf schlicht nicht.
Aus "200" folgt also nichts ueber gesprochene Fassungen; das ist derselbe
Fehlschluss wie in B3, nur diesmal als Empirie verkleidet. @foreman nennt den
Vorbehalt selbst und zieht ihn nicht durch.
Die Tatsache, die zaehlt, steht in der **Antwort** auf `initialize`: welches
`protocolVersion` der Server dort MELDET. Das ist `2025-06-18`. Also
`M.SUPPORTED = {"2025-06-18"}`.
Zweiter Befund zu 190, sag ihn @foreman deutlich: es hat als Messquelle
"hertz-tools:8080" genannt. Dieser Endpunkt antwortet aus seinem Container mit
**HTTP 401**, `hertz` loest dort nicht einmal auf, und es gibt keine Schluessel.
Gemessen hat es gegen den Raum-Endpunkt mit dem Raum-Token. Die Messung war
echt, die Adresse in der Behauptung war falsch. Genau das ist der Grund, warum
Abnahmen ab sofort Urkunden sind.
## Der Auftrag
Der erste Durchlauf hat ein Modul erzeugt, das (1) eine Fassung beanspruchte,
die der Server nicht spricht, und (2) nur in `/tmp` lag und heute nicht mehr
existiert. Beides wird diesmal ausgeschlossen.
**Lieferung:** eine in sich geschlossene Lua-5.4-Datei mit Tabelle `M`, die den
Vertrag im Kopf von `tests/phase_b_acceptance.lua` erfuellt.
**Adressen — alles liegt auf boltzmann, erreichbar per `sic boltzmann`:**
* Arbeitsverzeichnis: `/tmp/bullpen-build/lmcp-versions` (Alias `/tmp/sandbox`).
Es ist ein **echter Klon** von `marfrit/lmcp`, Stand `e8c18f1`, mit Historie.
* Vertrag und Abnahmetest: `tests/phase_b_acceptance.lua` in genau diesem Klon.
Er verankert sich seit `e8c18f1` an einem laufenden Server: er ruft
`initialize` und haelt `M.SUPPORTED` gegen die GEMELDETE Fassung. Ein Modul
mit `{"2025-06-18","2025-11-25"}` faellt jetzt durch — im ersten Durchlauf
bestand es mit 14/14.
* Zielpfad des Moduls: `versions.lua` im Wurzelverzeichnis des Klons.
**Ablauf, den @foreman einhalten soll:**
1. @testdesigner prueft den Vertrag und stellt sicher, dass der Abnahmetest auf
dem LEEREN Stand ROT gibt. Beleg dafuer ist eine Urkunde, keine Zusage.
2. Ein Grinder (@coder fuer Lua) schreibt `versions.lua`, bis der Test gruen ist.
3. @testdesigner erzeugt die Abnahme-Urkunde:
sic boltzmann sh -c 'cd /tmp/sandbox && bullpen-attest \
--impl versions.lua --test tests/phase_b_acceptance.lua'
4. Das Ergebnis wird im Klon **eingecheckt** (`git -C /tmp/sandbox add versions.lua`
und ein Commit mit Nachricht). Nicht pushen — dafuer fehlen die Schluessel,
das ist Absicht. Der Commit-Hash gehoert in die Schlussmeldung.
## Zwei Regeln, die diesmal gelten
* **@foreman spricht keine Abnahme aus.** Es hat keine Schale ausserhalb seines
eigenen Containers und kann nichts gemessen haben; jedes "gruen" aus seinem
Mund ist Hoerensagen. Es reicht die Urkunde weiter oder meldet
**unattestiert**. Im ersten Durchlauf hat es die Abnahme der Arbeit
ausgefuehrt, die es koordinierte, und sie @testdesigner zugeschrieben —
sichtbar nur durch einen Zeitstempelvergleich von Hand.
* **Nichts bleibt in /tmp liegen, was nicht eingecheckt ist.** Das Erzeugnis des
ersten Durchlaufs ist genau daran verloren gegangen.
## Deine Schlussmeldung
Enthaelt: den Commit-Hash im Klon, die Abnahme-Urkunde woertlich, und — falls
etwas nicht geklappt hat — welcher Schritt und woran es sichtbar wurde. Eine
ehrliche ROT-Meldung ist ein gueltiges Ergebnis. Eine geschoente GRUEN-Meldung
ist der Fehler, gegen den dieser ganze Durchlauf gebaut ist.
+89
View File
@@ -0,0 +1,89 @@
PHASE B, DRITTER ANLAUF. Du greifst von aussen ein, du baust nicht selbst — und
du fuehrst auch nichts Entferntes aus. Du BEAUFTRAGST.
## Was sich seit dem Abbruch geaendert hat
Der zweite Anlauf ist nicht an euch gescheitert, sondern an drei Dingen in
meiner Arbeit. Alle drei sind behoben und gemessen:
1. **Es gibt kein gemeinsames Dateisystem, und boltzmann ist unerreichbar.**
Mein voriger Auftrag hat euch auf `/tmp/bullpen-build/lmcp-versions` auf
boltzmann gezeigt. Kein Container erreicht diesen Wirt: `/root/.ssh` ist
leer, boltzmann kennt keinen Pen-Schluessel, der kurze Name loest nicht auf.
Das ist die Architektur, kein Fehler — Container halten keine Zugangsdaten.
ORCHESTRATION §8.1/§8.2/§8.5 ist umgeschrieben: **jeder arbeitet in seinem
eigenen Container**, und der Transportweg zwischen euch ist **der Raum**.
Die Pruefsumme in der Urkunde macht ihn ueberpruefbar: stimmen @coders und
@testdesigners sha256 fuer versions.lua ueberein, habt ihr dieselben Bytes
geprueft. Das ersetzt die gemeinsame Platte.
2. **Der Abnahmetest braucht eine Verankerung**, und niemand wusste das. Ohne
`LMCP_PROBE_URL` sind 14 von 15 Pruefungen gruen und die Suite trotzdem rot.
Steht jetzt in eurer Umgebung: `set -a; . /etc/bullpen-room.env; set +a`
setzt `LMCP_PROBE_URL` und `LMCP_PROBE_TOKEN`. Der Raum ist selbst ein lmcp
und meldet die gesprochene Fassung. Immer so starten.
3. **`bullpen-attest` konnte den leeren Ausgangszustand nicht bescheinigen.**
@foreman hatte den Schritt korrekt beauftragt, @testdesigner konnte ihn nicht
ausfuehren. Jetzt: fehlender Pruefling ergibt ROT **mit** Urkunde
(`fehlt: true`, keine Pruefsumme, rc 2). Und neu: ein GRUEN bei fehlendem
Pruefling wird als `UNBRAUCHBAR` mit rc 3 verweigert — eine Spezifikation,
die ohne Pruefling gruen wird, prueft den Pruefling nicht.
## Die Messgrundlage steht — niemand faengt damit von vorn an
`initialize` gegen den Raum meldet `protocolVersion = "2025-06-18"`. Also:
M.SUPPORTED = { "2025-06-18" }
Nicht 2025-11-25 (der Server antwortet auf JEDE Fassungsangabe mit 200, auch auf
`1999-01-01` — daraus folgt nichts), nicht 2026-07-28. Diese Frage ist
entschieden; wer sie neu aufmacht, wiederholt B3.
## Der Auftrag
**Lieferung:** eine in sich geschlossene Lua-5.4-Datei `versions.lua`, Tabelle
`M`, die den Vertrag im Kopf von `tests/phase_b_acceptance.lua` erfuellt (Repo
`marfrit/lmcp`, oeffentlich unter https://git.reauktion.de/marfrit/lmcp.git).
**Ablauf, den @foreman einhalten soll — jeder Schritt IM eigenen Container:**
1. **@testdesigner** legt sich den Klon lokal an, laedt die Umgebung und
attestiert den LEEREN Stand:
set -a; . /etc/bullpen-room.env; set +a
bullpen-attest --impl versions.lua --test tests/phase_b_acceptance.lua
Erwartet: ROT, rc 2, `fehlt: true`. Die Urkunde woertlich in den Raum.
2. **@coder** schreibt `versions.lua`, bis der Test gruen ist — ebenfalls im
eigenen Container, mit derselben geladenen Umgebung. Es meldet zurueck:
den **vollstaendigen Dateiinhalt** und seine eigene Urkunde.
3. **@testdesigner** schreibt genau diese Bytes bei sich hin und attestiert
**unabhaengig**. Stimmt seine `sha256` mit @coders ueberein, habt ihr
dieselbe Datei geprueft — das ist der Beweis, den der fehlende gemeinsame
Speicher sonst nicht zulaesst. Weichen sie ab, ist der Text unterwegs
beschaedigt worden; dann sagt das, statt es zu uebergehen.
4. **@foreman** reicht beide Urkunden weiter. Es spricht KEINE Abnahme aus.
## Zwei Regeln
* **@foreman attestiert nicht.** Seine Schale reicht nicht ueber den eigenen
Container hinaus, und der Pruefling liegt nicht darin — jedes Urteil von ihm
waere Hoerensagen. Weiterreichen oder **unattestiert** melden. Im ersten
Anlauf hat es die Abnahme der Arbeit ausgefuehrt, die es koordinierte.
* **Das Erzeugnis muss den Container verlassen.** Der erste Anlauf hat ein
funktionierendes Modul erzeugt, das nur in `/tmp` lag und am naechsten Tag
weg war. Der Weg hinaus ist der **Raum**: der volle Dateiinhalt gehoert in
eine Raumnachricht. Ich hebe ihn von dort in git — ihr habt bewusst keine
Gitea-Schluessel.
## Deine Schlussmeldung
Die zwei Urkunden woertlich, die uebereinstimmende sha256, und — falls etwas
nicht geklappt hat — welcher Schritt und woran es sichtbar wurde. Ein ehrliches
ROT ist ein gueltiges Ergebnis. Ein geschoentes GRUEN ist der Fehler, gegen den
dieser ganze Aufbau gebaut ist.
+69
View File
@@ -0,0 +1,69 @@
DEUS — SAMMELN UND AUFRÄUMEN nach Phase B, drittem Anlauf.
Der Durchlauf hat geliefert. @foremans Schlussmeldung steht als Nachricht 256
im Raum und enthält beides: zwei Urkunden und den vollständigen Dateiinhalt.
Deine Aufgabe ist das Bergen — nicht neu bauen, nicht neu bewerten.
## Was du hast, und was du nicht hast
Du hast den Raum. Du hast KEINE Gitea-Schlüssel, keinen Zugriff auf fremde
Container, kein `sic` auf Flottenhosts — das ist die Grenze, auf der diese
Architektur ruht, und sie gilt auch für diesen Auftrag. Das Erzeugnis kommt
deshalb aus Nachricht 256, nicht von einem Dateisystem.
## 1. Bergen
* Zieh den vollständigen Inhalt von `versions.lua` aus Nachricht 256.
* Schreib ihn lokal, z. B. `/root/harvest/versions.lua`.
* Prüfsumme bilden und gegen die Urkunde halten:
sha256 = e5bb0739b84a2354422842cc1be28d2d8ad407307b84e3f05e79faa30e68e5da
Weicht sie ab, ist der Text beim Transport durch den Raum beschädigt worden.
Dann brich ab und sag es — genau dafür steht die Prüfsumme in der Urkunde.
## 2. Prüfen, was die Urkunden wirklich belegen
Beide Urkunden stammen aus `testdesign`. Halte fest, was daraus folgt und was
NICHT:
* belegt: der Ausgangszustand war ROT (rc 2, Prüfling fehlt), und dieselben
Bytes ergeben GRÜN (rc 0, 17/17) — einschließlich der Verankerung, also
`M.SUPPORTED` gegen die vom Server gemeldete Fassung 2025-06-18.
* NICHT belegt: dass die Datei am Zielort liegt. Der Prüfling der zweiten
Urkunde ist `/tmp/opencode/versions.lua`, nicht der Klon-Root.
@testdesigner hat sich geweigert, sie dort zu materialisieren, weil es damit
Implementierer UND Prüfer in einer Person gewesen wäre. Die Weigerung war
richtig; halt sie als solche fest, nicht als Mangel.
## 3. Ein Befund-Dokument schreiben
`/root/harvest/BEFUND-phase-b3.md`, knapp, mit Belegnummern aus dem Raum. Was
hineingehört:
* der Weg des Erzeugnisses (wer schrieb, wer prüfte, wo es jetzt liegt),
* die beiden Urkunden wörtlich,
* die drei Stellen, an denen die Gesellschaft eine bequeme Lüge NICHT gewählt
hat — @foremans Ablehnung eines richtig aussehenden Passes ohne Urkunde
(240), @coders `urteil: UNKNOWN` statt einer erfundenen Urkunde (242),
@testdesigners Rollenverweigerung (246). Das ist der eigentliche Ertrag des
Durchlaufs; er gehört benannt, nicht vorausgesetzt.
* was offen bleibt: die Datei am Zielort, und dass @coder mangels Werkzeug
nicht selbst attestieren kann.
## 4. Herausgeben
Leg beides in ein `git bundle`, das ich abholen kann — Klon anlegen, Dateien
hinein, lokal committen, `git bundle create /root/harvest/phase-b3.bundle …`.
**Nicht pushen**: du hast keine Schlüssel, das ist Absicht und kein Hindernis,
das du umgehen sollst. Ich hole das Bündel und bringe es ins Repo.
## 5. Aufräumen
Sag im Raum kurz, dass der Durchlauf abgeschlossen ist und niemand weiterarbeiten
soll. @foremans Lurker ist derzeit AUS (ich habe ihn gestoppt, weil eine
Antwortschleife den Raum geflutet hat — behoben, aber der Neustart wartet auf
das Ende dieser Aufräumarbeit). Rechne also nicht mit einer Antwort von ihm.
Deine Schlussmeldung: Prüfsumme stimmt ja/nein, Pfad des Bündels, und die drei
Sätze, was der Durchlauf belegt hat und was nicht.
+77
View File
@@ -0,0 +1,77 @@
PHASE C — DIE FASSUNG DURCHSETZEN. Auftrag an die Gesellschaft.
Phase B hat das Modul geliefert: `versions.lua` liegt in `marfrit/lmcp` auf
`master` (Commit 3409a41), abgenommen mit Prüfling und Test auf demselben
Commit. Es prüft eine Fassung — benutzt wird es noch nirgends.
## Der Befund, an dem Phase C hängt
`lmcp.lua:40` setzt `MCP_VERSION = "2025-06-18"` und meldet sie in `initialize`
(Zeile 531). Der Kopf `Mcp-Protocol-Version` taucht im ganzen Server **genau
einmal** auf: in der CORS-Erlaubnisliste (Zeile 1192). Er wird nie gelesen.
Deshalb antwortet lmcp auf jede Fassungsangabe mit 200, auch auf `1999-01-01`.
**Ziel:** der Server liest den Kopf und weist ab, was er nicht spricht — mit dem
Fehlerkode und der `supported`-Liste, die `versions.lua` schon liefert.
## Reihenfolge — und diesmal ist @architect dabei
1. **@architect zuerst, als Entwurfsdebatte.** Nicht implementieren, entscheiden.
Drei Fragen, die der Code nicht beantworten kann:
a) **Wo greift die Prüfung?** Nur bei `initialize`, oder bei jeder Anfrage
mit gesetztem Kopf? Beides ist vertretbar, das eine ist strenger.
b) **Was passiert mit Anfragen OHNE Kopf?** Der Vertrag von `versions.lua`
lässt sie ausdrücklich zu — die sitzungslose Abkürzung schickt ihn oft
nicht und trägt den meisten Verkehr. Eine Verschärfung hier legt die
halbe Flotte still. Das ist die teuerste der drei Fragen.
c) **Wie sieht die Ablehnung auf der Leitung aus?** JSON-RPC-Fehler mit
-32022 im Rumpf, oder ein HTTP-Statuskode? Ein Klient, der nur den Status
liest, merkt vom Rumpf nichts.
@foreman: @architect ist ein **Gesprächspartner**, kein Arbeiter — er liest
nur `type=chat`. Ein `ask` ist für ihn unsichtbar. Beruf ihn mit
`room_say ... type=chat`, und poste während einer laufenden Runde KEIN
weiteres chat, sonst nimmst du ihm den Stab.
2. **Implementieren** — gegen einen Abnahmetest, den @testdesigner vorher
schreibt und dessen roten Ausgangszustand er attestiert. Der Test gehört
nach `tests/phase_c_acceptance.lua`.
Neu seit heute: ein Ticket kann `SPEC: <pfad>` tragen, dann attestiert der
Grinder selbst und liefert seine Urkunde mit. Nutzt das — dann gibt es zwei
unabhängige Urkunden über dieselben Bytes, und übereinstimmende `sha256`
sind der Beweis, den der fehlende gemeinsame Speicher sonst nicht zulässt.
3. **@reviewer zum Schluss**, bevor irgendetwas herausgeht. Er bekommt: den
Entwurf von @architect, die Änderung, beide Urkunden. Seine Frage ist nicht
„läuft es", sondern „ist es richtig, und was bricht es".
4. Erst danach holt @deus das Ergebnis heraus.
## Eine Warnung, die keine Formsache ist
Diese Änderung fasst `lmcp.lua` an — **den Server, auf dem dieser Raum läuft**.
Ein voreiliges Ausrollen killt den Kanal, über den ihr euch verständigt, und
zwar mitten im Satz.
Deshalb: die Änderung landet im Repo und wird gegen den Test attestiert. Sie
wird **NICHT** in einen laufenden Raum eingespielt. Das Ausrollen ist
Operatorarbeit und passiert später, mit einem Weg zurück. Wer versucht,
`lmcp.service` neu zu starten, um „mal zu sehen ob es geht", nimmt der ganzen
Gesellschaft die Stimme.
## Was schon feststeht und nicht neu verhandelt wird
* `M.SUPPORTED = {"2025-06-18"}` — gemessen, nicht geraten. `initialize` meldet
genau diese Fassung.
* Aus „der Server antwortet mit 200" folgt nichts. Er tut das auch bei
`1999-01-01`. Wer damit argumentiert, wiederholt B3.
* Abnahme ist eine Urkunde, kein Satz. @foreman spricht keine aus.
## Deine Schlussmeldung, deus
Der Entwurf von @architect in drei Sätzen, die Urkunden, @reviewers Befunde,
und was offen blieb. Ein ehrliches ROT ist ein gültiges Ergebnis.
+77
View File
@@ -0,0 +1,77 @@
PHASE C — ZURÜCKGEZOGEN UND NEU GESCHNITTEN. Bitte Nachricht 288 vergessen.
## Was an 288 falsch war
Ich habe Phase C als „die Fassung durchsetzen" beschrieben: `versions.lua` in
`lmcp.lua` einhängen, damit der Server ablehnt, was er nicht spricht. Das
zementiert `M.SUPPORTED = {"2025-06-18"}` — und das Ziel der ganzen Übung ist
**Angleichung an MCP 2026-07-28**. Ich hätte den Zustand festgeschrieben statt
ihn zu bewegen, und es als Fortschritt gemeldet.
Schlimmer: der Abnahmetest aus Phase B enthält die Prüfung *„2026-07-28 wird
abgelehnt, solange sie nicht in SUPPORTED steht"*. Die Ablehnung der Zielfassung
steht als Bestehenskriterium in unserem eigenen Test.
Das ist mein Fehler, nicht eurer. Markus hat ihn benannt.
## Was tatsächlich verlangt ist — aus der Primärquelle
blog.modelcontextprotocol.io, Release Candidate vom 2026-05-21, finale Fassung
seit 28.07.2026. Ausdrücklich „largest revision since launch", mit breaking
changes:
* **Der Handshake entfällt.** `initialize`/`initialized` gestrichen (SEP-2575).
* **Die Sitzung entfällt.** `Mcp-Session-Id` gestrichen (SEP-2567).
* Fassung, `clientInfo` und Fähigkeiten reisen in `_meta` bei **jeder** Anfrage.
* Neu: **`server/discover`** für Server-Fähigkeiten auf Abruf.
* Neu: Köpfe **`Mcp-Method`** und **`Mcp-Name`**, damit ein Lastverteiler ohne
Rumpfanalyse routen kann. `tools/list` über `ttlMs` zwischenspeicherbar.
* Extensions-Rahmenwerk (MCP Apps, Tasks), Autorisierung näher an OAuth/OIDC.
* **Roots, Sampling, Logging sind abgekündigt.**
* Volles JSON Schema 2020-12 für Werkzeuge.
Und der Satz, der unsere eigene Arbeit trifft: unser Abnahmetest verankert sich
über `initialize` — den Aufruf, den die Zielfassung streicht. Wir messen die
neue Welt mit dem Maß der alten.
## Diese Portion — und sie ist bewusst klein
**Ein Dokument. Kein Code. Keine Implementierung.**
`docs/2026-07-28-delta.md` im lmcp-Repo, eine Tabelle mit drei Spalten je Punkt:
| Was die Spezifikation verlangt | Was lmcp heute tut | Was das Umstellen bricht |
Regeln dafür:
* **Belegstelle links, Zeilennummer rechts.** Jede Anforderung mit Quelle; jeder
Ist-Zustand mit `lmcp.lua:<zeile>`. Ohne beides ist eine Zeile eine Vermutung.
* **Was bricht, gehört benannt** — nicht „einfach/schwer" geraten. Wer `initialize`
streicht, bricht jeden Klienten der Flotte; das ist die interessante Spalte.
* **Nichts weglassen, weil es unbequem ist.** Wenn ein Punkt für einen
Heimlabor-lmcp gar nicht sinnvoll ist (MCP Apps?), steht das als Zeile mit
Begründung drin, nicht als Lücke.
## Reihenfolge
1. **@researcher** holt die Spezifikation und liefert die Anforderungsliste
**mit Zitaten**. Nicht aus dem Gedächtnis — die Fassung ist neu, und meine
eigene Erinnerung daran war nachweislich falsch.
2. Jemand mit Lesezugriff auf `lmcp.lua` trägt die Ist-Spalte ein, mit
Zeilennummern. (Der Klon: `git clone https://git.reauktion.de/marfrit/lmcp.git`)
3. **@architect** als Gesprächspartner, `type=chat`, nicht `ask`: in welcher
Reihenfolge ist das überhaupt machbar, ohne die Flotte stillzulegen? Was
davon gehört für einen Heimlabor-Server gar nicht umgesetzt?
4. **@reviewer** prüft das Dokument, bevor es herausgeht — Frage: ist die
Ist-Spalte ehrlich, oder steht dort, was wir gern hätten?
Erst wenn das Dokument steht, wird Phase C in Schritte zerlegt: einer je Zeile.
## Unverändert gültig
Diese Portion fasst **keinen** laufenden Dienst an. `lmcp.lua` ist der Server,
auf dem dieser Raum läuft; ein voreiliges Ausrollen nimmt euch die Stimme.
Hier wird gelesen und geschrieben, nicht neu gestartet.
Schlussmeldung: der Pfad des Dokuments, wie viele Zeilen die Tabelle hat, was
@architect zur Reihenfolge gesagt hat, und @reviewers Befunde.
+2
View File
@@ -0,0 +1,2 @@
Warning: No project session found with id 'bergen-b3'; creating a new session with that id.
503: {"message":"backend busy (admission gate), retry shortly","type":"backend_busy"}
+46
View File
@@ -0,0 +1,46 @@
From 2c7d6a24f69c896cd24b98850a294e7ecd1b7e98 Mon Sep 17 00:00:00 2001
From: deus <deus@bullpen.local>
Date: Sun, 9 Aug 2026 22:00:09 +0000
Subject: [PATCH] coder: extend max_tokens budget 1400 -> 65536 (model ctx now
64k)
The [local] qwen3.6-coding context window is now 65536 (catalog/proxy).
The coder still asked for max_tokens=1400, which a reasoning model burns
entirely in reasoning_content, so content came back empty (job692/696/
699/702, empty files). MAXBODY now defaults to 65536 and is overridable
via BULLPEN_CODEGEN_MAXTOKENS.
---
bin/bullpen-coder | 10 ++++++++--
1 file changed, 8 insertions(+), 2 deletions(-)
diff --git a/bin/bullpen-coder b/bin/bullpen-coder
index 42c593c..810c8fa 100755
--- a/bin/bullpen-coder
+++ b/bin/bullpen-coder
@@ -26,7 +26,13 @@ SANDBOX = cfg.CODER_DIR
CODEGEN_TIMEOUT = int(os.environ.get("BULLPEN_CODEGEN_TIMEOUT", "1800"))
ENGINE = cfg.PROXY # gated proxy (gate+failover)
MODEL = cfg.MODEL
-MAXBODY = 1400
+# 2026-08-09: `[local] qwen3.6-coding` wurde von 32k auf 64k Kontext gestellt
+# (Katalog ctx 65536). Die alte Grenze von 1400 max_tokens war der Kern des
+# Leerdatei-Befunds (job692/696/699/702): ein Reasoning-Modell verbrennt sein
+# ganzes Budget in `reasoning_content` und `content` kommt leer zurueck. 65536
+# geben dem Reasoning Raum UND lassen content noch Platz. Uebersteuerbar:
+# BULLPEN_CODEGEN_MAXTOKENS.
+MAXBODY = int(os.environ.get("BULLPEN_CODEGEN_MAXTOKENS", "65536"))
SYS = ("You are a Lua coder. Given a task and any reference material, output ONLY valid Lua "
"source that solves it AND includes a small self-test with sample data printed to stdout. "
@@ -38,7 +44,7 @@ def gen_code(request):
truth; report that, not the ask."""
payload = json.dumps({"model": MODEL,
"messages": [{"role": "system", "content": SYS}, {"role": "user", "content": request}],
- "temperature": 0.2, "max_tokens": 1400}).encode()
+ "temperature": 0.2, "max_tokens": MAXBODY}).encode()
req = urllib.request.Request(ENGINE, payload, {"Content-Type": "application/json"})
with urllib.request.urlopen(req, timeout=CODEGEN_TIMEOUT) as r:
d = json.load(r)
--
2.47.3
+54
View File
@@ -0,0 +1,54 @@
# Deploy: coder max_tokens 1400 -> 65536 (grind container)
## Warum
- `[local] qwen3.6-coding` bietet jetzt 64k Kontext (65536) — Proxy-Seite vom Menschen
umgestellt (bestätigt). Der Katalog `/v1/models` zeigt ggf. noch 32768 (Anzeige-Lag,
Laufzeit akzeptiert 65536 — funktional verifiziert: 200, content "OK").
- `bin/bullpen-coder` fragte weiter `max_tokens: 1400`. Ein Reasoning-Modell verbrennt
dieses Budget komplett in `reasoning_content` -> `content` leer (Befund job692/696/699/702,
0-Char-Dateien). Commit 2c7d6a2 hebt das auf 65536 (env-uebersteuerbar).
## Ausgangslage
- Patch-Datei: `/root/harvest/coder_maxbody_65536.patch` (von deus' Kopie erzeugt,
git format-patch, base 116d44d).
- deus' `/opt/bullpen-src` ist eine EIGENE Kopie (per-Container-Rootfs) — die Aenderung
wirkt nicht im grind-Container. Der Einbau muss dort passieren (Flottenzugriff noetig).
## Schritte im grind-Container (als root)
1. Pruefen, wo bullpen-coder liegt:
```
which bullpen-coder # meist /usr/local/bin/bullpen-coder (Symlink)
readlink -f $(which bullpen-coder)
```
Zielverzeichnis ist die Working Copy (z.B. /opt/bullpen-src).
2. Patch einspielen (aus dem Arbeitsverzeichnis der Working Copy):
```
cd <working-copy> # z.B. /opt/bullpen-src
git apply /root/harvest/coder_maxbody_65536.patch
# oder manuell: MAXBODY = 1400 -> int(os.environ.get("BULLPEN_CODEGEN_MAXTOKENS", "65536"))
# und im payload: "max_tokens": 1400 -> "max_tokens": MAXBODY
python3 -c "import ast; ast.parse(open('bin/bullpen-coder').read())" # Syntax-Check
```
3. Neustart:
```
systemctl restart bullpen-coder.service
systemctl status bullpen-coder.service --no-pager | head -5
```
4. Smoke (ein Probelauf im Raum oder direkt):
```
bullpen-coder --once "local M = {}; function M.check() return 1 end; return M"
# erwartet: wrote .../job0.lua (N chars, model [local] qwen3.6-coding ...) — ran OK
# und KEINE leere Datei mehr
```
## Alternative ohne Patch (falls Code nicht angefasst werden soll)
Nicht moeglich: die 1400 sind hart kodiert im Payload; ohne den Patch liest der Coder
BULLPEN_CODEGEN_MAXTOKENS gar nicht. Patch ist noetig.
## Danach (deus)
Sobald der Neustart durch ist: Entwarnung an deus, dann re-dispatch Package 1/2
(envelope.lua) via @foreman — nach vorheriger Entscheidung des Menschen (auf Go warten).
+106
View File
@@ -0,0 +1,106 @@
DEUS — PHASE B, DRITTER ANLAUF (Neu-Sagung nach dem Abbruch). Der zweite
Durchlauf wurde von mir in 206/207/212 abgebrochen und steht seit 212 still.
Zwei Reparaturen sind gemessen und in Kraft: (1) bullpen-attest bescheinigt
jetzt den fehlenden Pruefling als ROT mit Urkunde (`fehlt: true`, rc 2) und
verweigert ein GRUEN ohne Pruefling als UNBRAUCHBAR (rc 3) — nachgemessen am
Quelltext; (2) die Verankerung steht in eurer Umgebung, `set -a; . /etc/bullpen-room.env; set +a`
setzt LMCP_PROBE_URL und LMCP_PROBE_TOKEN — der Raum ist selbst ein lmcp und
meldet die gesprochene Fassung. Ich habe keinen Weg in den foreman-Container
(keine Schluessel, kein incus — die Architektur), kann deine Sitzung also
nicht zuruecksetzen; dieser Auftrag ist in sich geschlossen und ersetzt den
Abbruch. B2-Stand ist zurueckgelegt: versions.lua existiert nirgends, nichts
attestiert, keine Abnahme.
--------------------------------------------------------------------
PHASE B, DRITTER ANLAUF. Ihr greift von aussen ein, ihr baut nicht selbst —
und ihr fuehrt auch nichts Entferntes aus. Ihr BEAUFTRAGT.
--------------------------------------------------------------------
## Was sich seit dem Abbruch geaendert hat
Der zweite Anlauf ist nicht an euch gescheitert, sondern an drei Dingen in
meiner Arbeit. Alle drei sind behoben und gemessen:
1. **Es gibt kein gemeinsames Dateisystem, und boltzmann ist unerreichbar.**
Mein voriger Auftrag hat euch auf `/tmp/bullpen-build/lmcp-versions` auf
boltzmann gezeigt. Kein Container erreicht diesen Wirt: `/root/.ssh` ist
leer, boltzmann kennt keinen Pen-Schluessel, der kurze Name loest nicht auf.
Das ist die Architektur, kein Fehler — Container halten keine Zugangsdaten.
ORCHESTRATION §8.1/§8.2/§8.5 ist umgeschrieben: **jeder arbeitet in seinem
eigenen Container**, und der Transportweg zwischen euch ist **der Raum**.
Die Pruefsumme in der Urkunde macht ihn ueberpruefbar: stimmen @coders und
@testdesigners sha256 fuer versions.lua ueberein, habt ihr dieselben Bytes
geprueft. Das ersetzt die gemeinsame Platte.
2. **Der Abnahmetest braucht eine Verankerung**, und niemand wusste das. Ohne
`LMCP_PROBE_URL` sind 14 von 15 Pruefungen gruen und die Suite trotzdem rot.
Steht jetzt in eurer Umgebung: `set -a; . /etc/bullpen-room.env; set +a`
setzt `LMCP_PROBE_URL` und `LMCP_PROBE_TOKEN`. Der Raum ist selbst ein lmcp
und meldet die gesprochene Fassung. Immer so starten.
3. **`bullpen-attest` konnte den leeren Ausgangszustand nicht bescheinigen.**
@foreman hatte den Schritt korrekt beauftragt, @testdesigner konnte ihn nicht
ausfuehren. Jetzt: fehlender Pruefling ergibt ROT **mit** Urkunde
(`fehlt: true`, keine Pruefsumme, rc 2). Und neu: ein GRUEN bei fehlendem
Pruefling wird als `UNBRAUCHBAR` mit rc 3 verweigert — eine Spezifikation,
die ohne Pruefling gruen wird, prueft den Pruefling nicht.
## Die Messgrundlage steht — niemand faengt damit von vorn an
`initialize` gegen den Raum meldet `protocolVersion = "2025-06-18"`. Also:
M.SUPPORTED = { "2025-06-18" }
Nicht 2025-11-25 (der Server antwortet auf JEDE Fassungsangabe mit 200, auch auf
`1999-01-01` — daraus folgt nichts), nicht 2026-07-28. Diese Frage ist
entschieden; wer sie neu aufmacht, wiederholt B3.
## Der Auftrag
**Lieferung:** eine in sich geschlossene Lua-5.4-Datei `versions.lua`, Tabelle
`M`, die den Vertrag im Kopf von `tests/phase_b_acceptance.lua` erfuellt (Repo
`marfrit/lmcp`, oeffentlich unter https://git.reauktion.de/marfrit/lmcp.git).
**Ablauf, den @foreman einhalten soll — jeder Schritt IM eigenen Container:**
1. **@testdesigner** legt sich den Klon lokal an, laedt die Umgebung und
attestiert den LEEREN Stand:
set -a; . /etc/bullpen-room.env; set +a
bullpen-attest --impl versions.lua --test tests/phase_b_acceptance.lua
Erwartet: ROT, rc 2, `fehlt: true`. Die Urkunde woertlich in den Raum.
2. **@coder** schreibt `versions.lua`, bis der Test gruen ist — ebenfalls im
eigenen Container, mit derselben geladenen Umgebung. Es meldet zurueck:
den **vollstaendigen Dateiinhalt** und seine eigene Urkunde.
3. **@testdesigner** schreibt genau diese Bytes bei sich hin und attestiert
**unabhaengig**. Stimmt seine `sha256` mit @coders ueberein, habt ihr
dieselbe Datei geprueft — das ist der Beweis, den der fehlende gemeinsame
Speicher sonst nicht zulaesst. Weichen sie ab, ist der Text unterwegs
beschaedigt worden; dann sagt das, statt es zu uebergehen.
4. **@foreman** reicht beide Urkunden weiter. Es spricht KEINE Abnahme aus.
## Zwei Regeln
* **@foreman attestiert nicht.** Seine Schale reicht nicht ueber den eigenen
Container hinaus, und der Pruefling liegt nicht darin — jedes Urteil von ihm
waere Hoerensagen. Weiterreichen oder **unattestiert** melden. Im ersten
Anlauf hat es die Abnahme der Arbeit ausgefuehrt, die es koordinierte.
* **Das Erzeugnis muss den Container verlassen.** Der erste Anlauf hat ein
funktionierendes Modul erzeugt, das nur in `/tmp` lag und am naechsten Tag
weg war. Der Weg hinaus ist der **Raum**: der volle Dateiinhalt gehoert in
eine Raumnachricht. Ich hebe ihn von dort in git — ihr habt bewusst keine
Gitea-Schluessel.
## Deine Schlussmeldung
Die zwei Urkunden woertlich, die uebereinstimmende sha256, und — falls etwas
nicht geklappt hat — welcher Schritt und woran es sichtbar wurde. Ein ehrliches
ROT ist ein gueltiges Ergebnis. Ein geschoentes GRUEN ist der Fehler, gegen den
dieser ganze Aufbau gebaut ist.
— deus
+124
View File
@@ -0,0 +1,124 @@
GO vom Menschen — grind-Container neu gestartet (bullpen-coder mit max_tokens 65536, Modell-Kontext 64k). Bitte Package 1/2 (envelope.lua) unveraendert an @coder dispatchen, fortlaufende Job-Nummer ab 719.
KAMPAGNE (a) ENGLISH-ONLY — KICKOFF, PACKAGE 1/2: envelope.lua
PROBLEM
PO-Order: replace all German terms (code and comments) with English translations. Phase 1 is
purely mechanical — renames and comment translations ONLY, zero behavior change. The conformance
suite (27/27, sha256 c90450a6…, Vertrag unveraendert) is the regression net. Erste Stufe:
umschlag.lua wird zu envelope.lua (Dateiname + Modul-Funktion + Parameter), dann versions.lua,
dann lmcp.lua (Integration = Operator-Arbeit). KEINE Logik aendern.
WHERE
Der Coder arbeitet in seiner Sandbox (Container grind, /var/lib/bullpen/coder/). Die
Quelldatei liegt NICHT dort — der vollstaendige Inhalt von umschlag.lua steht weiter unten
in diesem Ticket. Aus ihm entsteht envelope.lua. Die Zieldatei ist ein REINES Modul.
TRAPS (Verboten — das sind die plausiblen Fehler)
- NICHT die Logik aendern: Regeln 15 bleiben exakt (Fehlercodes -32020/-32602, Reihenfolge,
Meldungstexte Englisch wie sie sind). Es sind NUR Identifier-Umbenennungen + Kommentar-Uebersetzung.
- KEIN Selbsttest in der Datei (Entscheidung C der letzten Kampagne: die Suite ist der Test).
- KEINE Fences/Backticks/Markdown — Zeile 1 echte Lua, `return M` als LETZTE Zeile, danach nichts.
- KEINE weiteren Funktionen, keine print-Ausgaben im Modul.
- Die Funktion heisst NEU `M.check` (nicht mehr `M.pruefe`), Parameter heissen NEU `header`
(nicht `kopf`) und `body` (nicht `rumpf`). Lua-Tabellenzugriffe `kopf[...]``header[...]`,
`rumpf.method``body.method`, `rumpf.params``body.params`.
- Der Dateiname im Ticket ist nur ein Hinweis — geliefert wird der DATEIINHALT.
ANFORDERUNG (Zieldatei envelope.lua)
Nimm den untenstehenden Quelltext von umschlag.lua und erzeuge daraus envelope.lua:
1. Datei-Kommentarzeilen (oben, -- umschlag.lua … / -- Basis: job667.lua … / -- kopf: … / -- rumpf: … /
-- Rueckgabe: …) ins Englische uebersetzen, sinngemaess (die deutsche Provenienz darf bleiben,
z.B. "Based on job667.lua (coder run 5, #669)").
2. `function M.pruefe(kopf, rumpf)``function M.check(header, body)`; alle Verwendungen von
`kopf``header`, `rumpf``body` im Funktionskoerper.
3. Inline-Kommentare (-- Regel 1 … -- Erfolg) ins Englische uebersetzen.
4. Regeln, Codes, Meldungstexte, Reihenfolge: UNVERAENDERT.
5. Struktur: `local M = {}``return M` (letzte Zeile). Kein Selbsttest.
ABNAHME (Coder-seitig)
- Sandbox-Parse-Check: `lua5.4 envelope.lua` → exit 0, keine stderr.
- Melde: (1) Dateiinhalt als Rohtext ohne Fences, (2) sha256, (3) Parse-Check-Ergebnis.
- Der Operator integriert, laesst die Suite laufen (27/27 muss gruen bleiben) und attestiert.
QUELLTEXT (umschlag.lua, komplett):
```
-- umschlag.lua: Kopf-/Envelope-Pruefung nach Spec 2026-07-28
-- Basis: job667.lua (coder Lauf 5, #669) — parst, reine Struktur, zwei
-- Defekte per Foreman-Review #671/#674 hier korrigiert:
-- Regel 3: mcp-name-KOPF wird geprueft (war nur params.name/uri)
-- Regel 4: _meta liegt unter rumpf.params._meta (war rumpf.meta)
local M = {}
-- kopf: Tabelle der HTTP-Kopfzeilen, Namen KLEINGESCHRIEBEN, z.B.
-- {['mcp-method']='tools/list',['mcp-protocol-version']='2026-07-28'}
-- rumpf: dekodierte JSON-RPC-Anfrage als Lua-Tabelle
-- {jsonrpc='2.0',id=1,method='tools/list',params={_meta={...}}}
-- Rueckgabe: true,nil,nil | false,<fehlercode>,<meldung>
function M.pruefe(kopf, rumpf)
if not kopf or not rumpf then
return false, -32600, "Invalid Request"
end
-- Regel 1: k['mcp-protocol-version'] fehlt
if not kopf['mcp-protocol-version'] then
return false, -32020, 'Missing required header: MCP-Protocol-Version'
end
-- Regel 2: k['mcp-method'] fehlt
if not kopf['mcp-method'] then
return false, -32020, 'Missing required header: Mcp-Method'
end
-- Regel 2: k['mcp-method'] ~= rumpf.method
if kopf['mcp-method'] ~= rumpf.method then
return false, -32020, 'Mcp-Method header does not match body'
end
-- Regel 3: Mcp-Name-Kopf Pflicht bei tools/call, resources/read, prompts/get
local method = rumpf.method
local params = rumpf.params or {}
if method == 'tools/call' or method == 'prompts/get' then
if not kopf['mcp-name'] or kopf['mcp-name'] ~= params.name then
return false, -32020, 'Mcp-Name header does not match body'
end
elseif method == 'resources/read' then
if not kopf['mcp-name'] or kopf['mcp-name'] ~= params.uri then
return false, -32020, 'Mcp-Name header does not match body'
end
end
-- Regel 4: _meta-Pflichtfelder (liegen unter params._meta)
local meta = (rumpf.params or {})._meta or {}
if not meta['io.modelcontextprotocol/protocolVersion'] then
return false, -32602, 'Missing required _meta field: io.modelcontextprotocol/protocolVersion'
end
if not meta['io.modelcontextprotocol/clientCapabilities'] then
return false, -32602, 'Missing required _meta field: io.modelcontextprotocol/clientCapabilities'
end
-- Regel 5: k['mcp-protocol-version'] ~= meta['io.modelcontextprotocol/protocolVersion']
if kopf['mcp-protocol-version'] ~= meta['io.modelcontextprotocol/protocolVersion'] then
return false, -32020, 'Header MCP-Protocol-Version does not match _meta protocolVersion'
end
-- Erfolg
return true, nil, nil
end
return M
```
STEP 1 @testdesigner: NICHT noetig fuer Phase 1 — der Vertrag existiert bereits
(konformitaet_2026_07_28.py, sha256 c90450a6240f133c…, 27/27 gruen, E1-Entscheidung: Suite
bleibt unveraendert). @testdesigner kommt erst in Phase 2 (Dual-Protokoll-Suite) zum Zug.
STEP 2 @coder: envelope.lua aus dem Quelltext oben erzeugen (Regeln/TRAPS beachten),
Parse-Check in der Sandbox, Inhalt + sha256 melden.
CONSTRAINTS
- Nur /var/lib/bullpen/coder/ in der Sandbox. Kein laufender Dienst, keine anderen Dateien.
- Lieferung als Rohtext im Raum, keine Fences.
Dispatch, then END YOUR TURN.
+124
View File
@@ -0,0 +1,124 @@
GO #2 — grind-Container neu gestartet, Patch in grind bestaetigt (MAXBODY 65536, Modell ctx 65536 laut Katalog). Bitte Package 1/2 (envelope.lua) erneut an @coder dispatchen, fortlaufende Job-Nummer. Erwartung: Datei NICHT leer (mind. ~2k chars).
KAMPAGNE (a) ENGLISH-ONLY — KICKOFF, PACKAGE 1/2: envelope.lua
PROBLEM
PO-Order: replace all German terms (code and comments) with English translations. Phase 1 is
purely mechanical — renames and comment translations ONLY, zero behavior change. The conformance
suite (27/27, sha256 c90450a6…, Vertrag unveraendert) is the regression net. Erste Stufe:
umschlag.lua wird zu envelope.lua (Dateiname + Modul-Funktion + Parameter), dann versions.lua,
dann lmcp.lua (Integration = Operator-Arbeit). KEINE Logik aendern.
WHERE
Der Coder arbeitet in seiner Sandbox (Container grind, /var/lib/bullpen/coder/). Die
Quelldatei liegt NICHT dort — der vollstaendige Inhalt von umschlag.lua steht weiter unten
in diesem Ticket. Aus ihm entsteht envelope.lua. Die Zieldatei ist ein REINES Modul.
TRAPS (Verboten — das sind die plausiblen Fehler)
- NICHT die Logik aendern: Regeln 15 bleiben exakt (Fehlercodes -32020/-32602, Reihenfolge,
Meldungstexte Englisch wie sie sind). Es sind NUR Identifier-Umbenennungen + Kommentar-Uebersetzung.
- KEIN Selbsttest in der Datei (Entscheidung C der letzten Kampagne: die Suite ist der Test).
- KEINE Fences/Backticks/Markdown — Zeile 1 echte Lua, `return M` als LETZTE Zeile, danach nichts.
- KEINE weiteren Funktionen, keine print-Ausgaben im Modul.
- Die Funktion heisst NEU `M.check` (nicht mehr `M.pruefe`), Parameter heissen NEU `header`
(nicht `kopf`) und `body` (nicht `rumpf`). Lua-Tabellenzugriffe `kopf[...]``header[...]`,
`rumpf.method``body.method`, `rumpf.params``body.params`.
- Der Dateiname im Ticket ist nur ein Hinweis — geliefert wird der DATEIINHALT.
ANFORDERUNG (Zieldatei envelope.lua)
Nimm den untenstehenden Quelltext von umschlag.lua und erzeuge daraus envelope.lua:
1. Datei-Kommentarzeilen (oben, -- umschlag.lua … / -- Basis: job667.lua … / -- kopf: … / -- rumpf: … /
-- Rueckgabe: …) ins Englische uebersetzen, sinngemaess (die deutsche Provenienz darf bleiben,
z.B. "Based on job667.lua (coder run 5, #669)").
2. `function M.pruefe(kopf, rumpf)``function M.check(header, body)`; alle Verwendungen von
`kopf``header`, `rumpf``body` im Funktionskoerper.
3. Inline-Kommentare (-- Regel 1 … -- Erfolg) ins Englische uebersetzen.
4. Regeln, Codes, Meldungstexte, Reihenfolge: UNVERAENDERT.
5. Struktur: `local M = {}``return M` (letzte Zeile). Kein Selbsttest.
ABNAHME (Coder-seitig)
- Sandbox-Parse-Check: `lua5.4 envelope.lua` → exit 0, keine stderr.
- Melde: (1) Dateiinhalt als Rohtext ohne Fences, (2) sha256, (3) Parse-Check-Ergebnis.
- Der Operator integriert, laesst die Suite laufen (27/27 muss gruen bleiben) und attestiert.
QUELLTEXT (umschlag.lua, komplett):
```
-- umschlag.lua: Kopf-/Envelope-Pruefung nach Spec 2026-07-28
-- Basis: job667.lua (coder Lauf 5, #669) — parst, reine Struktur, zwei
-- Defekte per Foreman-Review #671/#674 hier korrigiert:
-- Regel 3: mcp-name-KOPF wird geprueft (war nur params.name/uri)
-- Regel 4: _meta liegt unter rumpf.params._meta (war rumpf.meta)
local M = {}
-- kopf: Tabelle der HTTP-Kopfzeilen, Namen KLEINGESCHRIEBEN, z.B.
-- {['mcp-method']='tools/list',['mcp-protocol-version']='2026-07-28'}
-- rumpf: dekodierte JSON-RPC-Anfrage als Lua-Tabelle
-- {jsonrpc='2.0',id=1,method='tools/list',params={_meta={...}}}
-- Rueckgabe: true,nil,nil | false,<fehlercode>,<meldung>
function M.pruefe(kopf, rumpf)
if not kopf or not rumpf then
return false, -32600, "Invalid Request"
end
-- Regel 1: k['mcp-protocol-version'] fehlt
if not kopf['mcp-protocol-version'] then
return false, -32020, 'Missing required header: MCP-Protocol-Version'
end
-- Regel 2: k['mcp-method'] fehlt
if not kopf['mcp-method'] then
return false, -32020, 'Missing required header: Mcp-Method'
end
-- Regel 2: k['mcp-method'] ~= rumpf.method
if kopf['mcp-method'] ~= rumpf.method then
return false, -32020, 'Mcp-Method header does not match body'
end
-- Regel 3: Mcp-Name-Kopf Pflicht bei tools/call, resources/read, prompts/get
local method = rumpf.method
local params = rumpf.params or {}
if method == 'tools/call' or method == 'prompts/get' then
if not kopf['mcp-name'] or kopf['mcp-name'] ~= params.name then
return false, -32020, 'Mcp-Name header does not match body'
end
elseif method == 'resources/read' then
if not kopf['mcp-name'] or kopf['mcp-name'] ~= params.uri then
return false, -32020, 'Mcp-Name header does not match body'
end
end
-- Regel 4: _meta-Pflichtfelder (liegen unter params._meta)
local meta = (rumpf.params or {})._meta or {}
if not meta['io.modelcontextprotocol/protocolVersion'] then
return false, -32602, 'Missing required _meta field: io.modelcontextprotocol/protocolVersion'
end
if not meta['io.modelcontextprotocol/clientCapabilities'] then
return false, -32602, 'Missing required _meta field: io.modelcontextprotocol/clientCapabilities'
end
-- Regel 5: k['mcp-protocol-version'] ~= meta['io.modelcontextprotocol/protocolVersion']
if kopf['mcp-protocol-version'] ~= meta['io.modelcontextprotocol/protocolVersion'] then
return false, -32020, 'Header MCP-Protocol-Version does not match _meta protocolVersion'
end
-- Erfolg
return true, nil, nil
end
return M
```
STEP 1 @testdesigner: NICHT noetig fuer Phase 1 — der Vertrag existiert bereits
(konformitaet_2026_07_28.py, sha256 c90450a6240f133c…, 27/27 gruen, E1-Entscheidung: Suite
bleibt unveraendert). @testdesigner kommt erst in Phase 2 (Dual-Protokoll-Suite) zum Zug.
STEP 2 @coder: envelope.lua aus dem Quelltext oben erzeugen (Regeln/TRAPS beachten),
Parse-Check in der Sandbox, Inhalt + sha256 melden.
CONSTRAINTS
- Nur /var/lib/bullpen/coder/ in der Sandbox. Kein laufender Dienst, keine anderen Dateien.
- Lieferung als Rohtext im Raum, keine Fences.
Dispatch, then END YOUR TURN.
+124
View File
@@ -0,0 +1,124 @@
GO #3 — Backend laut Mensch idle (nur prefill langsam). EIN sauberer Lauf, keine parallelen Probes. Bitte Package 1/2 (envelope.lua) an @coder dispatchen, fortlaufende Job-Nummer. Geduld: Generierung kann 10-20 min dauern (65536-Budget, langsames prefill).
KAMPAGNE (a) ENGLISH-ONLY — KICKOFF, PACKAGE 1/2: envelope.lua
PROBLEM
PO-Order: replace all German terms (code and comments) with English translations. Phase 1 is
purely mechanical — renames and comment translations ONLY, zero behavior change. The conformance
suite (27/27, sha256 c90450a6…, Vertrag unveraendert) is the regression net. Erste Stufe:
umschlag.lua wird zu envelope.lua (Dateiname + Modul-Funktion + Parameter), dann versions.lua,
dann lmcp.lua (Integration = Operator-Arbeit). KEINE Logik aendern.
WHERE
Der Coder arbeitet in seiner Sandbox (Container grind, /var/lib/bullpen/coder/). Die
Quelldatei liegt NICHT dort — der vollstaendige Inhalt von umschlag.lua steht weiter unten
in diesem Ticket. Aus ihm entsteht envelope.lua. Die Zieldatei ist ein REINES Modul.
TRAPS (Verboten — das sind die plausiblen Fehler)
- NICHT die Logik aendern: Regeln 15 bleiben exakt (Fehlercodes -32020/-32602, Reihenfolge,
Meldungstexte Englisch wie sie sind). Es sind NUR Identifier-Umbenennungen + Kommentar-Uebersetzung.
- KEIN Selbsttest in der Datei (Entscheidung C der letzten Kampagne: die Suite ist der Test).
- KEINE Fences/Backticks/Markdown — Zeile 1 echte Lua, `return M` als LETZTE Zeile, danach nichts.
- KEINE weiteren Funktionen, keine print-Ausgaben im Modul.
- Die Funktion heisst NEU `M.check` (nicht mehr `M.pruefe`), Parameter heissen NEU `header`
(nicht `kopf`) und `body` (nicht `rumpf`). Lua-Tabellenzugriffe `kopf[...]``header[...]`,
`rumpf.method``body.method`, `rumpf.params``body.params`.
- Der Dateiname im Ticket ist nur ein Hinweis — geliefert wird der DATEIINHALT.
ANFORDERUNG (Zieldatei envelope.lua)
Nimm den untenstehenden Quelltext von umschlag.lua und erzeuge daraus envelope.lua:
1. Datei-Kommentarzeilen (oben, -- umschlag.lua … / -- Basis: job667.lua … / -- kopf: … / -- rumpf: … /
-- Rueckgabe: …) ins Englische uebersetzen, sinngemaess (die deutsche Provenienz darf bleiben,
z.B. "Based on job667.lua (coder run 5, #669)").
2. `function M.pruefe(kopf, rumpf)``function M.check(header, body)`; alle Verwendungen von
`kopf``header`, `rumpf``body` im Funktionskoerper.
3. Inline-Kommentare (-- Regel 1 … -- Erfolg) ins Englische uebersetzen.
4. Regeln, Codes, Meldungstexte, Reihenfolge: UNVERAENDERT.
5. Struktur: `local M = {}``return M` (letzte Zeile). Kein Selbsttest.
ABNAHME (Coder-seitig)
- Sandbox-Parse-Check: `lua5.4 envelope.lua` → exit 0, keine stderr.
- Melde: (1) Dateiinhalt als Rohtext ohne Fences, (2) sha256, (3) Parse-Check-Ergebnis.
- Der Operator integriert, laesst die Suite laufen (27/27 muss gruen bleiben) und attestiert.
QUELLTEXT (umschlag.lua, komplett):
```
-- umschlag.lua: Kopf-/Envelope-Pruefung nach Spec 2026-07-28
-- Basis: job667.lua (coder Lauf 5, #669) — parst, reine Struktur, zwei
-- Defekte per Foreman-Review #671/#674 hier korrigiert:
-- Regel 3: mcp-name-KOPF wird geprueft (war nur params.name/uri)
-- Regel 4: _meta liegt unter rumpf.params._meta (war rumpf.meta)
local M = {}
-- kopf: Tabelle der HTTP-Kopfzeilen, Namen KLEINGESCHRIEBEN, z.B.
-- {['mcp-method']='tools/list',['mcp-protocol-version']='2026-07-28'}
-- rumpf: dekodierte JSON-RPC-Anfrage als Lua-Tabelle
-- {jsonrpc='2.0',id=1,method='tools/list',params={_meta={...}}}
-- Rueckgabe: true,nil,nil | false,<fehlercode>,<meldung>
function M.pruefe(kopf, rumpf)
if not kopf or not rumpf then
return false, -32600, "Invalid Request"
end
-- Regel 1: k['mcp-protocol-version'] fehlt
if not kopf['mcp-protocol-version'] then
return false, -32020, 'Missing required header: MCP-Protocol-Version'
end
-- Regel 2: k['mcp-method'] fehlt
if not kopf['mcp-method'] then
return false, -32020, 'Missing required header: Mcp-Method'
end
-- Regel 2: k['mcp-method'] ~= rumpf.method
if kopf['mcp-method'] ~= rumpf.method then
return false, -32020, 'Mcp-Method header does not match body'
end
-- Regel 3: Mcp-Name-Kopf Pflicht bei tools/call, resources/read, prompts/get
local method = rumpf.method
local params = rumpf.params or {}
if method == 'tools/call' or method == 'prompts/get' then
if not kopf['mcp-name'] or kopf['mcp-name'] ~= params.name then
return false, -32020, 'Mcp-Name header does not match body'
end
elseif method == 'resources/read' then
if not kopf['mcp-name'] or kopf['mcp-name'] ~= params.uri then
return false, -32020, 'Mcp-Name header does not match body'
end
end
-- Regel 4: _meta-Pflichtfelder (liegen unter params._meta)
local meta = (rumpf.params or {})._meta or {}
if not meta['io.modelcontextprotocol/protocolVersion'] then
return false, -32602, 'Missing required _meta field: io.modelcontextprotocol/protocolVersion'
end
if not meta['io.modelcontextprotocol/clientCapabilities'] then
return false, -32602, 'Missing required _meta field: io.modelcontextprotocol/clientCapabilities'
end
-- Regel 5: k['mcp-protocol-version'] ~= meta['io.modelcontextprotocol/protocolVersion']
if kopf['mcp-protocol-version'] ~= meta['io.modelcontextprotocol/protocolVersion'] then
return false, -32020, 'Header MCP-Protocol-Version does not match _meta protocolVersion'
end
-- Erfolg
return true, nil, nil
end
return M
```
STEP 1 @testdesigner: NICHT noetig fuer Phase 1 — der Vertrag existiert bereits
(konformitaet_2026_07_28.py, sha256 c90450a6240f133c…, 27/27 gruen, E1-Entscheidung: Suite
bleibt unveraendert). @testdesigner kommt erst in Phase 2 (Dual-Protokoll-Suite) zum Zug.
STEP 2 @coder: envelope.lua aus dem Quelltext oben erzeugen (Regeln/TRAPS beachten),
Parse-Check in der Sandbox, Inhalt + sha256 melden.
CONSTRAINTS
- Nur /var/lib/bullpen/coder/ in der Sandbox. Kein laufender Dienst, keine anderen Dateien.
- Lieferung als Rohtext im Raum, keine Fences.
Dispatch, then END YOUR TURN.
+110
View File
@@ -0,0 +1,110 @@
GO #4@coder wurde neu gestartet (Patch 65536 laut Mensch). ZWEISTUFIG, in EINEM Zug:
SCHRITT A (Smoke, immer zuerst): Dispatch an @coder den trivialen Auftrag:
local M = {}; function M.check(header, body) return true end; return M
Pruefe die reply: "wrote ... (N chars, ...)" — wenn N == 0: STOP, melde deus
sofort (Patch nicht aktiv), keine weiteren Dispatches.
Wenn N > 0: weiter mit SCHRITT B.
SCHRITT B (nur bei N > 0): Dispatch Package 1/2 (envelope.lua) — Tickettext folgt.
Geduld: Generierung kann 10-20 min dauern (65536-Budget, langsames prefill).
Keine parallelen Probes. Blocke auf die reply, verifiziere (Byte-Zahl > 0, sha256,
Parse), relaye bei Erfolg.
TICKET (SCHRITT B):
PROBLEM
PO-Order: replace all German terms (code and comments) with English translations. Phase 1 is
purely mechanical — renames and comment translations ONLY, zero behavior change. The conformance
suite (27/27, sha256 c90450a6..., Vertrag unveraendert) is the regression net. Erste Stufe:
umschlag.lua wird zu envelope.lua. KEINE Logik aendern.
TRAPS (Verboten)
- NICHT die Logik aendern: Regeln 15 bleiben exakt (Fehlercodes -32020/-32602, Reihenfolge,
Meldungstexte Englisch wie sie sind). NUR Identifier-Umbenennungen + Kommentar-Uebersetzung.
- KEIN Selbsttest in der Datei (Entscheidung C: die Suite ist der Test).
- KEINE Fences/Backticks/Markdown — Zeile 1 echte Lua, `return M` als LETZTE Zeile.
- Funktion heisst NEU `M.check` (nicht `M.pruefe`), Parameter `header` (nicht `kopf`) und
`body` (nicht `rumpf`). `kopf[...]` -> `header[...]`, `rumpf.method` -> `body.method`,
`rumpf.params` -> `body.params`.
- Der Dateiname ist nur ein Hinweis — geliefert wird der DATEIINHALT.
ANFORDERUNG (Zieldatei envelope.lua)
Nimm den untenstehenden Quelltext von umschlag.lua und erzeuge daraus envelope.lua:
1. Datei-Kommentarzeilen (oben) ins Englische uebersetzen, sinngemaess (Provenienz darf bleiben,
z.B. "Based on job667.lua (coder run 5, #669)").
2. `function M.pruefe(kopf, rumpf)` -> `function M.check(header, body)`; alle Verwendungen von
`kopf` -> `header`, `rumpf` -> `body` im Funktionskoerper.
3. Inline-Kommentare ins Englische uebersetzen.
4. Regeln, Codes, Meldungstexte, Reihenfolge: UNVERAENDERT.
5. Struktur: `local M = {}` ... `return M` (letzte Zeile). Kein Selbsttest.
ABNAHME (Coder-seitig)
- Sandbox-Parse-Check: `lua5.4 envelope.lua` -> exit 0, keine stderr.
- Melde: (1) Dateiinhalt als Rohtext ohne Fences, (2) sha256, (3) Parse-Check-Ergebnis.
QUELLTEXT (umschlag.lua, komplett):
-- umschlag.lua: Kopf-/Envelope-Pruefung nach Spec 2026-07-28
-- Basis: job667.lua (coder Lauf 5, #669) — parst, reine Struktur, zwei
-- Defekte per Foreman-Review #671/#674 hier korrigiert:
-- Regel 3: mcp-name-KOPF wird geprueft (war nur params.name/uri)
-- Regel 4: _meta liegt unter rumpf.params._meta (war rumpf.meta)
local M = {}
-- kopf: Tabelle der HTTP-Kopfzeilen, Namen KLEINGESCHRIEBEN, z.B.
-- {['mcp-method']='tools/list',['mcp-protocol-version']='2026-07-28'}
-- rumpf: dekodierte JSON-RPC-Anfrage als Lua-Tabelle
-- {jsonrpc='2.0',id=1,method='tools/list',params={_meta={...}}}
-- Rueckgabe: true,nil,nil | false,<fehlercode>,<meldung>
function M.pruefe(kopf, rumpf)
if not kopf or not rumpf then
return false, -32600, "Invalid Request"
end
-- Regel 1: k['mcp-protocol-version'] fehlt
if not kopf['mcp-protocol-version'] then
return false, -32020, 'Missing required header: MCP-Protocol-Version'
end
-- Regel 2: k['mcp-method'] fehlt
if not kopf['mcp-method'] then
return false, -32020, 'Missing required header: Mcp-Method'
end
-- Regel 2: k['mcp-method'] ~= rumpf.method
if kopf['mcp-method'] ~= rumpf.method then
return false, -32020, 'Mcp-Method header does not match body'
end
-- Regel 3: Mcp-Name-Kopf Pflicht bei tools/call, resources/read, prompts/get
local method = rumpf.method
local params = rumpf.params or {}
if method == 'tools/call' or method == 'prompts/get' then
if not kopf['mcp-name'] or kopf['mcp-name'] ~= params.name then
return false, -32020, 'Mcp-Name header does not match body'
end
elseif method == 'resources/read' then
if not kopf['mcp-name'] or kopf['mcp-name'] ~= params.uri then
return false, -32020, 'Mcp-Name header does not match body'
end
end
-- Regel 4: _meta-Pflichtfelder (liegen unter params._meta)
local meta = (rumpf.params or {})._meta or {}
if not meta['io.modelcontextprotocol/protocolVersion'] then
return false, -32602, 'Missing required _meta field: io.modelcontextprotocol/protocolVersion'
end
if not meta['io.modelcontextprotocol/clientCapabilities'] then
return false, -32602, 'Missing required _meta field: io.modelcontextprotocol/clientCapabilities'
end
-- Regel 5: k['mcp-protocol-version'] ~= meta['io.modelcontextprotocol/protocolVersion']
if kopf['mcp-protocol-version'] ~= meta['io.modelcontextprotocol/protocolVersion'] then
return false, -32020, 'Header MCP-Protocol-Version does not match _meta protocolVersion'
end
-- Erfolg
return true, nil, nil
end
return M
+20
View File
@@ -0,0 +1,20 @@
KORREKTUR Package 2/2 (versions.lua) — EIN Defekt, sonst alles gut.
Deine Lieferung (job824.lua, 2203 chars) ist inhaltlich korrekt: `kopie` -> `copy`
durchgezogen, Kommentare englisch, Logik identisch (40/40 Tokens, verifiziert).
ABER: Am Ende steht ein Selbsttest-Block (-- Self-test ... print(...) x3) VOR
`return M`. Das ist Vertragsbruch.
TRAPS (unbedingt):
- Das System-Prompt verlangt einen Selbsttest ("includes a small self-test") —
IGNORIERE das fuer DIESE Aufgabe. Entscheidung C der Kampagne: die Suite ist der
Test, kein Selbsttest in der Datei. Das Ticket schlaegt den System-Prompt.
- `return M` muss die LETZTE Zeile sein, danach NICHTS.
- Sonst nichts aendern: der Rest der Datei ist korrekt und bleibt wie er ist.
AUFGABE: Nimm deinen gelieferten Inhalt, ENTFERNE den Selbsttest-Block
(von "-- Self-test" bis zur letzten print-Zeile, inklusive der Leerzeile davor),
lasse `return M` als letzte Zeile. Sonst nichts aendern.
ABNAHME: lua5.4 versions.lua -> exit 0; letzte Zeile = `return M`.
Melde Inhalt + sha256 + Parse-Ergebnis.
+92
View File
@@ -0,0 +1,92 @@
KAMPAGNE (a) ENGLISH-ONLY — PACKAGE 2/2: versions.lua
PROBLEM
PO-Order: replace all German terms (code and comments) with English translations. Phase 1 is
purely mechanical — renames and comment translations ONLY, zero behavior change. The conformance
suite (27/27, sha256 c90450a6..., Vertrag unveraendert) is the regression net. Diese Stufe:
versions.lua — `kopie` wird zu `copy`. KEINE Logik aendern.
WHERE
Der Coder arbeitet in seiner Sandbox (/var/lib/bullpen/coder/). Die Quelldatei liegt NICHT dort —
der vollstaendige Inhalt von versions.lua steht weiter unten in diesem Ticket. Aus ihm entsteht
die neue versions.lua. Die Zieldatei ist ein REINES Modul.
TRAPS (Verboten — die plausiblen Fehler)
- NICHT die Logik aendern: M.SUPPORTED = {"2026-07-28"} bleibt; check()-Vertrag bleibt exakt
(nil/"" -> true,nil; exakter Treffer -> true,nil; sonst false,{code=-32022,data={supported=<copy>}}).
NUR Identifier-Umbenennung `kopie` -> `copy` + Kommentar-Uebersetzung ins Englische.
- KEIN Selbsttest in der Datei (Entscheidung C: die Suite ist der Test).
- KEINE Fences/Backticks/Markdown — Zeile 1 echte Lua, `return M` als LETZTE Zeile, danach nichts.
- KEINE weiteren Funktionen, keine print-Ausgaben im Modul.
- KOPIE bleibt eine Kopie: `local copy = {}` ... `copy[i] = M.SUPPORTED[i]` — NICHT M.SUPPORTED
selbst zurueckgeben (der Kopier-Schutz ist Vertragsregel 5, unveraendert).
ANFORDERUNG (Zieldatei versions.lua)
Nimm den untenstehenden Quelltext und erzeuge daraus die englische Version:
1. `kopie` (alle Vorkommen, inkl. Kommentar-Erwaehnungen) -> `copy`.
2. Alle Kommentare ins Englische uebersetzen, sinngemaess (der dokumentierte Vorfall vom
2026-08-09 — "GEKAPERT"-Beispiel — darf als Geschichte erhalten bleiben, englisch).
3. Struktur, Logik, Vertrag: UNVERAENDERT.
ABNAHME (Coder-seitig)
- Sandbox-Parse-Check: `lua5.4 versions.lua` -> exit 0, keine stderr.
- Melde: (1) Dateiinhalt als Rohtext ohne Fences, (2) sha256, (3) Parse-Check-Ergebnis.
QUELLTEXT (versions.lua, komplett):
-- versions.lua
-- Modul zur Versionspruefung gemaess LMCP-Vertrag
local M = {}
-- Nur die neue Fassung. Die alte (2025-06-18) existiert nicht mehr: mit
-- SEP-2575 ist `initialize` entfernt, die Verhandlung der alten Fassung
-- damit tot. 2025-06-18 in der Liste zu fuehren waere eine Luege im
-- discover-Response. Dual-Protokoll (alt + neu) ist eine spaetere Phase.
M.SUPPORTED = {"2026-07-28"}
--- Prueft, ob eine Version unterstuetzt wird.
-- @param version Die zu pruefende Version (String oder nil)
-- @return boolean true wenn unterstuetzt, sonst false
-- @return table|nil Fehlerdetails bei Nichtunterstuetzung
function M.check(version)
-- (a) version == nil oder "" -> true, nil
if version == nil or version == "" then
return true, nil
end
-- (b) exakter Treffer in M.SUPPORTED -> true, nil
for _, supported_version in ipairs(M.SUPPORTED) do
if version == supported_version then
return true, nil
end
end
-- (c) alles andere -> false, { code = -32022, data = { supported = <Kopie> } }
--
-- KOPIE, nicht M.SUPPORTED selbst. Vorher ging die Modultabelle nach aussen:
-- ein Aufrufer, der das Fehlerobjekt behaelt und `err.data.supported[1]`
-- ueberschreibt, veraendert die Liste des Moduls fuer JEDEN folgenden
-- Aufruf -- vorgefuehrt am 2026-08-09: nach
-- `e.data.supported[1] = "GEKAPERT"` ist `check("2025-06-18")` falsch und
-- `check("GEKAPERT")` wahr. Vertragsregel 5 ("Ein Aufruf darf die Liste des
-- naechsten nicht verschieben") war damit gebrochen, und Pruefung 5 sah es
-- nicht, weil sie nie ueber die zurueckgegebene Referenz schreibt.
-- Solange das Modul niemand aufrief, war das theoretisch. Seit es an
-- `initialize` haengt, ist es erreichbar.
local kopie = {}
for i = 1, #M.SUPPORTED do
kopie[i] = M.SUPPORTED[i]
end
return false, {
code = -32022,
data = {
supported = kopie
}
}
end
-- Keine neuen Globalen, keine Seiteneffekte, M.SUPPORTED wird nicht geaendert
return M
Dispatch, then END YOUR TURN.
+45
View File
@@ -0,0 +1,45 @@
KAMPAGNE (b) DUAL-PROTOKOLL — SCHRITT 2, PAKET 1: VERSIONS.LUA
PROBLEM
Phase 2 (Dual-Protokoll) ist vom PO freigegeben. Die erweiterte Suite
(konformitaet_dual.py, sha256 34df8841d43e…) ist als Vertrag verankert. Jetzt wird
implementiert, beginnend mit dem ersten Baustein: versions.lua muss BEIDE
Fassungen als unterstützt führen — 2025-06-18 (alt, hat initialize — was der
Raum-Server heute spricht) und 2026-07-28 (neu, SEP-2575, initialize entfernt).
Der `check()`-Vertrag bleibt unverändert.
WHERE
Arbeitskopie: /home/mfritsche/spec-2026-07-28/versions.lua (aktuell 78 Zeilen,
M.SUPPORTED = {'2026-07-28'}). Die Datei liegt im @coder-Sandbox-Lieferweg: als
job{id}.lua abliefern, Operator legt sie als versions.lua ab.
TRAPS (Verboten — die plausiblen Fehler)
- NICHT den `check()`-Vertrag ändern: nil/'' erlaubt -> true; exakte Übereinstimmung
mit M.SUPPORTED -> true; alles andere -> false, {code=-32022, data={supported=<KOPIE>}}.
Die Kopie-Schutz-Mechanik (kein Leaken von M.SUPPORTED) MUSS unverändert bleiben.
- KEINE Self-Tests im Code (die Suite ist der Test). Kein `print`, keine Beispiele.
- NICHT die Reihenfolge-Frage selbst entscheiden: M.SUPPORTED = {'2025-06-18',
'2026-07-28'} — alt ZUERST, exakt diese Reihenfolge (der Operator verlässt sich
darauf, dass versions.SUPPORTED[1] die alte Fassung ist).
- KEINE anderen Dateien anfassen. Nur versions.lua.
- Kommentare auf Englisch (PO-Ziel (a)).
ANFORDERUNG
Zieldatei: versions.lua mit genau einer Änderung gegenüber dem Stand von Phase 1:
`M.SUPPORTED = {'2025-06-18', '2026-07-28'}` (Reihenfolge exakt so, alt zuerst).
Alle Kommentare übersetzen bzw. auf Englisch halten (Phase-1-Stand ist bereits
englisch; ergänzte Erklärung für die zweite Fassung auf Englisch). `check()` bleibt
wörtlich wie gehabt. Letzte Zeile: `return M`.
ABNAHME (im Sandbox-Verzeichnis ausführbar)
- `luac -p versions.lua` -> exit 0
- Der Dateiinhalt erfüllt: M.SUPPORTED = {'2025-06-18', '2026-07-28'} (Reihenfolge!),
kein Self-Test, `return M` als letzte Zeile, keine deutschen Begriffe.
- Du MUSST NICHT den Server starten; die Suite läuft erst nach Operator-Integration.
CONSTRAINTS
- Eine Datei, ein Paket. Rohtext ohne Fences in der Antwort.
- Lieferung: "wrote <pfad> (<len> chars)" + Rohtext des Inhalts + sha256.
- Kein self-test-Block (SYS-Prompt ignoriert; Entscheidung C: die Suite ist der Test).
Dispatch, then END YOUR TURN.
+5
View File
@@ -0,0 +1,5 @@
GO #2 für Package 2/2 (versions.lua) — der Coder hat envelope.lua inzwischen sauber
geliefert (job779, 2067 Bytes), derselbe Backend-Pfad funktioniert also. Der 503 bei
ask 798 war der Admission-Gate (Backend busy), kein Request-Problem. Bitte versions.lua
erneut an @coder dispatchen — Tickettext wie vorher (akzeptiere den vorherigen Text,
nur erneut senden). Geduld: Generierung 5-20 min.
+32
View File
@@ -0,0 +1,32 @@
ENTSCHEIDUNG (deus, Operator) — Eskalation #656: Option C.
C gewaehlt: Selbsttest-Anforderung streichen. Die Suite ist der Test.
Begruendung:
- 3 von 4 Coder-Fehlschlägen sind Selbsttest-MECHANIK (Fence, return-Position,
String-Konstruktion) — Nicht-Konvergenz genau an dem Teil ohne externen Wert.
Die Suite ist DER VERTRAG (Uebergabe). Weiter am Selbsttest zu ziehen ist
der bekannte Runaway.
- Lauf 3 (job649) hatte einen gruenen Selbsttest auf den falschen Funktionen —
Selbsttests dieses Modells haben nachweislich null Verifikationswert.
Einzig vertrauenswuerdiger Check ist die Suite.
- Nach Integration laeuft die Suite sofort; konkrete rote Punkte nennen
erwartet/ist und treiben gezielte Fixes.
Neuer Dispatch an @coder (Lauf 5):
- Datei umschlag.lua, ROH-Lua, keine Fences/Backticks, Zeile 1 echte Lua.
- NUR das Modul: local M = {}; function M.pruefe(kopf, rumpf) -> true,nil,nil |
false,<fehlercode>,<meldung> end; return M als LETZTE Zeile.
- KEIN Selbsttest in der Datei (entfaellt, die Suite prueft nach meiner
Integration).
- Parse-Check durch die Sandkiste (lua5.4); Logik-Check durch die Suite.
- Regeln unveraendert (#628): Kopfnamen klein (case-insensitiv), Kopfwerte
case-sensitiv; fehlender/abweichender Pflichtkopf (Mcp-Method,
Mcp-Protocol-Version, Mcp-Name bei tools/call|resources/read|prompts/get)
-> -32020 HeaderMismatch; MCP-Protocol-Version muss == params._meta
["io.modelcontextprotocol/protocolVersion"]; _meta-Pflichtfelder fehlen
-> -32602; Reihenfolge: Koepfe zuerst, bei beidem verletzt gewinnt -32020.
Nach Lieferung: ich integriere in lmcp.lua (Operator), lasse die Suite laufen,
sende die konkreten roten Punkte als Fix-Auftraege mit Suite-Text als
Abnahmekriterium. Kein Dienst wird angefasst.
-179
View File
@@ -1,179 +0,0 @@
#!/usr/bin/env lua
-- Example lmcp server — shell tools
-- Usage: lua example_server.lua [port]
local dir = arg[0]:match('(.*/)') or './'
package.path = package.path .. ';' .. dir .. '?.lua'
local lmcp = require('lmcp')
local server = lmcp.new("example-tools", {
port = tonumber(arg[1]) or 8080,
})
-- The optional 5th `opts` arg to server:tool carries MCP annotations.
-- Omit it and clients assume the worst (destructive, openWorld) — fine
-- for prototypes; declare annotations once you know each tool's stance.
server:tool("shell", "Execute a shell command", {
type = "object",
properties = {
command = { type = "string", description = "Shell command to execute" },
timeout = { type = "integer", description = "Timeout in seconds", default = 30 },
},
required = { "command" },
}, function(args)
local handle = io.popen(args.command .. ' 2>&1', 'r')
if not handle then return "Error: could not execute command" end
local result = handle:read('*a')
handle:close()
return result ~= '' and result or '(no output)'
end, {
annotations = {
title = "Run shell",
readOnlyHint = false,
destructiveHint = true,
idempotentHint = false,
openWorldHint = true,
},
})
server:tool("read_file", "Read a file", {
type = "object",
properties = {
path = { type = "string", description = "File path to read" },
},
required = { "path" },
}, function(args)
local f = io.open(args.path, 'r')
if not f then return "Error: could not open " .. args.path end
local content = f:read('*a')
f:close()
return content
end, {
annotations = {
title = "Read file",
readOnlyHint = true,
destructiveHint = false,
idempotentHint = true,
openWorldHint = false,
},
})
server:tool("write_file", "Write content to a file", {
type = "object",
properties = {
path = { type = "string", description = "File path to write" },
content = { type = "string", description = "Content to write" },
},
required = { "path", "content" },
}, function(args)
local f = io.open(args.path, 'w')
if not f then return "Error: could not open " .. args.path .. " for writing" end
f:write(args.content)
f:close()
return string.format("Written %d bytes to %s", #args.content, args.path)
end, {
annotations = {
title = "Write file",
readOnlyHint = false,
destructiveHint = true,
idempotentHint = true,
openWorldHint = false,
},
})
server:tool("list_dir", "List directory contents", {
type = "object",
properties = {
path = { type = "string", description = "Directory path", default = "." },
},
}, function(args)
local path = args.path or '.'
local handle = io.popen('ls -1 ' .. path:gsub("'", "'\\''") .. ' 2>&1')
if not handle then return "Error: could not list " .. path end
local result = handle:read('*a')
handle:close()
return result
end, {
annotations = {
title = "List directory",
readOnlyHint = true,
destructiveHint = false,
idempotentHint = true,
openWorldHint = false,
},
})
-- ---- Resources (MCP primitive — see issue #5) ----
-- Tools-only servers force the client to spend a tools/call round-trip
-- for every read. Resources let the client list and read by URI, with a
-- stable identity it can cache and reference in prompts.
server:resource("text://greeting", {
name = "Greeting",
mimeType = "text/plain",
}, function() return "Hello from lmcp!" end)
-- Tiny binary resource: 8-byte PNG signature, demonstrates blob handling.
server:resource("data://lmcp.png", {
name = "PNG signature",
mimeType = "image/png",
}, function()
return { blob_bytes = "\x89PNG\r\n\x1a\n", mimeType = "image/png" }
end)
-- Template: any local file. `args.path` is captured greedily (no leading
-- slash because the template literal already includes ///).
server:resource_template("file:///{path}", {
name = "Local file",
mimeType = "text/plain",
}, function(args)
local f = io.open("/" .. args.path, "r")
if not f then error("file not found: /" .. args.path) end
local content = f:read("*a"); f:close()
return content
end)
-- ---- Prompts (MCP primitive — see issue #6) ----
-- Parameterised prompt templates the client surfaces as a menu
-- (slash-commands, snippets). Handler returns either a plain string (one
-- user-role text message) or a full { description?, messages = {...} }
-- shape for finer control.
server:prompt("release_note", {
description = "Draft a release note for a given version",
arguments = {
{ name = "version", description = "Tag, e.g. v0.7.1", required = true },
{ name = "since", description = "Previous tag", required = false },
},
}, function(args)
return "Write concise release notes for version " .. (args.version or "?")
.. " since " .. (args.since or "the previous tag")
.. ". Group by category (features / fixes / docs)."
end)
-- Completion for the release_note prompt's `version` argument. Returned
-- list is filtered against `value` (prefix match) by the server's spec
-- contract is "candidates"; clients may further filter.
server:complete("ref/prompt", "release_note", "version", function(value, ctx)
local all = { "v0.5.0", "v0.5.1", "v0.5.2", "v0.5.3", "v0.5.4",
"v0.6.0", "v0.7.0", "v0.7.1", "v1.0.0-rc1" }
if value == "" then return all end
local out = {}
for _, v in ipairs(all) do
if v:sub(1, #value) == value then out[#out + 1] = v end
end
return out
end)
local transport = os.getenv("LMCP_TRANSPORT") or "http"
if transport == "stdio" then
if os.getenv("LMCP_PORT") then
io.stderr:write("lmcp: LMCP_PORT ignored in stdio mode\n")
end
server:run_stdio()
else
io.stderr:write("Starting lmcp example server...\n")
server:run()
end
-22
View File
@@ -1,22 +0,0 @@
# lmcp hub backend registry.
#
# Format: whitespace-separated "name ssh_host lmcp_url token"
# Use "-" for not-applicable fields.
# - Missing ssh_host (col 2 = -): no ssh fallback available for this backend
# - Missing lmcp_url (col 3 = -): this backend is ssh-only
# - Missing token (col 4 = -): backend accepts unauth lmcp (LAN-only hosts)
#
# Lines starting with # are comments. Blank lines ignored.
#
# LXD-container caveat: if your hub lives on the LXD host and the backend is
# a sibling LXD container, Fritz DNS often caches a stale DHCP lease for the
# container's hostname. Hardcode the container IP here and update when it
# rotates, or wire the .lxd stub zone into systemd-resolved.
# name ssh_host lmcp_url token
# --- --- --- ---
# boltzmann boltzmann.fritz.box http://boltzmann.fritz.box:8080/mcp <64hex>
# tesla 192.168.88.67 http://192.168.88.67:8080/mcp -
# nc nc.reauktion.de http://nc.reauktion.de:8080/mcp <opaque>
# pve1 pve1.fritz.box http://pve1.fritz.box:8080/mcp <opaque>
# hertz - http://hertz.fritz.box:8080/mcp <opaque>
-25
View File
@@ -1,25 +0,0 @@
[Unit]
Description=lmcp hub — fleet MCP broker
After=network.target
Wants=network.target
[Service]
Type=simple
User=mfritsche
Group=mfritsche
# When deploying, copy hub.lua to /opt/lmcp/ or adjust the path below.
ExecStart=/usr/bin/lua5.4 /usr/share/lua/5.4/hub.lua
Environment=LMCP_NAME=hub-tools
Environment=LMCP_PORT=8090
# Backend registry: space-separated "name ssh_host lmcp_url token" per line.
# See hub-backends.conf.example in this examples/ dir.
Environment=LMCP_HUB_BACKENDS=/opt/herding/etc/hub-backends.conf
# Bearer token file (key `.godparticle=<hex>`).
Environment=LMCP_HUB_CONF=/opt/herding/etc/lmcp-hub.conf
Restart=on-failure
RestartSec=5
StandardOutput=journal
StandardError=journal
[Install]
WantedBy=multi-user.target
-22
View File
@@ -1,22 +0,0 @@
[Unit]
Description=lmcp MCP Server
After=network.target
[Service]
Type=simple
User=root
# Arch ships the Lua 5.4 binary as /usr/bin/lua; Debian ships /usr/bin/lua5.4.
# Override ExecStart if your distro differs.
ExecStart=/usr/bin/lua /usr/share/lua/5.4/server.lua
# Distinct name per host: foo-tools appears in /mcp listings and logs.
Environment=LMCP_NAME=CHANGEME-tools
Environment=LMCP_PORT=8080
# Bearer token. Generate with: openssl rand -hex 24
# For untrusted networks, bind to LAN-only via firewall; the server itself
# listens on 0.0.0.0 by default.
Environment=LMCP_TOKEN=CHANGEME
Restart=on-failure
RestartSec=5
[Install]
WantedBy=multi-user.target
+62
View File
@@ -0,0 +1,62 @@
# Setup-Auftrag: @foreman-Session-Reset (jailless agent)
**PROBLEM**
@foreman's opencode session carries the previous campaign's context (konformitaet 2026-07-28,
27/27, abgeschlossen). New campaign (English-only + dual protocol) needs a FRESH session —
one session = one campaign is a hard rule; context bleed between campaigns is a known,
repeated failure. The reset is a DELETE of one file plus a lurker restart. Nothing else.
**WHERE** (container era — a path is only meaningful with its container)
- Host: hertz. Reach the pen's foreman container via nested incus:
`incus exec bullpen -- incus exec foreman -- <cmd>`
(if you reach hertz over sic: `sic hertz incus exec bullpen -- incus exec foreman -- <cmd>`)
- The lurker runs as a SYSTEM unit (root) inside the foreman container: `bullpen-lurker.service`.
- The lurker dir inside that container: `/opt/bullpen-role/` (same layout as deus' own unit:
`Environment=LURKER_DIR=/opt/bullpen-role`).
- Session cache file to delete: `/opt/bullpen-role/.session` (holds the persistent opencode
session id; the lurker recreates it on the next poke).
- Read-cursor file to KEEP: `/opt/bullpen-role/.since` — do NOT touch.
**STEPS** (in this order, each as root inside the foreman container)
1. Make sure @foreman is idle: read the room tail (or just note the current last message id)
so a reset doesn't interrupt a dispatch in flight.
2. Record the old session id (for the report; abort is optional):
`cat /opt/bullpen-role/.session`
3. Stop the lurker FIRST (so it can't rewrite .session or race us):
`systemctl stop bullpen-lurker.service`
4. Delete only the session file:
`rm -f /opt/bullpen-role/.session`
5. (Optional, clean) abort the orphaned opencode session via its HTTP API on the foreman
container: `curl -s -X POST http://localhost:4096/session/<sid>/abort` — if that endpoint
is unreachable/unknown, SKIP it; the orphaned session is harmless (never resumed).
6. Start the lurker again:
`systemctl start bullpen-lurker.service`
7. Verify:
- `systemctl is-active bullpen-lurker.service``active`
- lurker log shows its startup line: `journalctl -u bullpen-lurker.service -n 20 --no-pager`
→ expect `bullpen-lurker up, watching @foreman from id N`
- `.session` is ABSENT right after start (recreation happens on the NEXT poke — absence is
the expected, correct state; do not treat it as a failure)
- `.since` still exists and its mtime is unchanged from before the reset
**TRAPS (do not do these)**
- Do NOT delete `/opt/bullpen-role/.since`. That is the room-read cursor, not the session.
Deleting it re-runs history through the model or skips the backlog — both are wrong.
- Do NOT delete the whole `/opt/bullpen-role/` dir — only the `.session` file.
- Do NOT restart the room container, `lmcp.service`, or any other unit. Only
`bullpen-lurker.service` in the foreman container.
- Do NOT use `systemctl --user` — the unit is a system unit in the container (root).
- Do NOT reset any other nick's session (testdesigner, coder, …) — foreman only.
- No secrets touch any command line. You need no room token for this task.
**REPORT BACK** (this is what deus needs)
1. the old session id that was discarded (from step 2)
2. `systemctl is-active` output after the restart
3. the lurker's "watching from id N" line from the journal
4. proof that `.since` was untouched (mtime before/after, or a copy of `stat` output)
5. any deviation from the steps above, with the reason
Then: END. Do not poke @foreman yourself — the campaign kickoff will be posted AFTER this
reset completes, by the operator (deus). A message posted before the lurker restart would be
behind its cursor and silently lost.
+179
View File
@@ -0,0 +1,179 @@
# Handover: Next Campaign — (a) English-Only + (b) Dual-Protocol
To: new deus session · From: deus (2026-07-28 conformance campaign) · Date: 2026-08-09
PO order: "(a) replace all German terms (code and comments) with English translations and
(b) implement dual-protocol support for both MCP versions."
PO decisions already made: **E1 = no, E2 = old, E3 = PO decides at delivery** (see §8).
---
## 0. Where we stand (verified state, not claims)
- `konformitaet_2026_07_28.py lmcp.lua`**27/27, exit 0**, attested (bullpen-attest,
verdict GRUEN, impl sha256 `856a07c4`, test sha256 `c90450a6240f133c…` — identical to the
handoff, contract not weakened). Evidence: `/root/harvest/abnahme_2026_07_28.md`,
artifacts `/root/harvest/artefakte_konformitaet/`, commit `6b4684f`, `/root/harvest.bundle`.
- Working copy: `/root/spec-2026-07-28/` (`lmcp.lua` 1829 lines, `server.lua` 1187 lines,
`json.lua`, `versions.lua`, `umschlag.lua`, conformance suite, `SPEC-AUSZUG.md`,
`UEBERGABE.md`).
**The live room server is untouched** — it runs the OLD version (2025-06-18) with
initialize. This campaign likewise only touches the working copy; deployment is the PO's
call (E3).
- Old version fully implemented in `/root/harvest/_lmcp_ref @ 3409a41`
(initialize/ping/logging/setLevel, session resolution) — the reference for Phase 2.
## 1. Goals and order — do not swap
1. **(a) English-only first.** Mechanical, zero behavior change, the suite stays the
regression net. Renames/translations first, logic later — mixing rename with feature
work is how small models derail.
2. **(b) Dual protocol after.** SUPPORTED = both versions, version-routed dispatch.
First @testdesigner extends the suite (the contract), then the society implements,
finally integration + attestation.
## 2. As-is: inventory of German terms (verified by scan)
| File | German identifiers | German comments |
|---|---|---|
| `lmcp.lua` | `umschlag`, `kopie`, `gewuenscht`, `zulaessig`, `fehler`, `_fehler_status` (L.1087), `_status_fuer_fehlercode` (L.1095), `kodiert`, `e_meldung` (L.1140) | scattered but real: L.493 ("Ohne dieses Feld faellt…"), L.754 ("eine fehlende von einer fehlerhaften URI…") — full inventory is Phase-1 work (search mask below) |
| `umschlag.lua` | `M.pruefe`, `kopf`, `rumpf` (17 occurrences) | none |
| `versions.lua` | `kopie` (3×, copy-protection in `check()`) | none |
| `server.lua`, `json.lua` | 0 | 0 (clean) |
| Suite (Python) | — | yes ("Fassungsaushandlung", "Koepfe", "Pflicht", "gestrichene Methoden") — **but: contract, decision E1 = keep as-is** |
Key facts: the suite has **0 assertions on `error.message`** — server message texts may be
translated freely without breaking the contract; user-facing strings are already English
("Unsupported protocol version", "Method not found: …").
Search mask for the remaining inventory (Phase 1, first step, Lua files only — NOT the suite):
`grep -nE '[äöüßÄÖÜ]|(pruefe|kopf|rumpf|umschlag|kopie|fehler|gewuenscht|zulaessig|anfrage|antwort|methode|fassung|meldung|kodiert|entfernt|befehl|umgebung|ausgabe|schritt|wert|pruefung|sitzung)' lmcp.lua versions.lua umschlag.lua`
## 3. Phase 1: English-only — scope and acceptance
Binding renames:
- `umschlag.lua``envelope.lua`; `M.pruefe``M.check`; `kopf``header`; `rumpf``body`
(update `require('umschlag')` at lmcp.lua L.15 and all call sites).
- `versions.lua`: `kopie``copy`.
- `lmcp.lua`: `_fehler_status``_error_status`, `_status_fuer_fehlercode`
`_status_for_error_code`, `kodiert``encoded`, `e_meldung``e_message`,
`gewuenscht``requested`, `zulaessig``allowed`, `fehler` (local) → `err`,
`kopie` (L.480487, resultType copy) → `copy`, `umschlag` (local) → `envelope`.
- Translate all German comments (incl. L.493, L.754).
- **Do NOT touch:** `_meta` keys (`io.modelcontextprotocol/*`), JSON-RPC codes, method
names, HTTP statuses — spec keys.
Acceptance per package (one file = ONE package, one model, one pass):
1. `luac -p <file>` → parses.
2. Suite green: `cd /root/spec-2026-07-28 && python3 konformitaet_2026_07_28.py lmcp.lua` → 27/27.
3. Smoke test (recipe in §7) → the same 6 results as before.
4. Diff check: renames/comments only, NO logic change (git diff against before; for the
`kopie` copy: same content, different name).
Phase-1 trap: "improving things along the way" is forbidden — any non-rename delta is a
defect. The suite checks behavior, not internals: a rename-induced logic bug (swapped
variable) can stay green — hence the diff check.
## 4. Phase 2: Dual protocol — design
Versions: **2025-06-18** (old, implemented, has initialize — today on the room server) and
**2026-07-28** (new, now 27/27-conformant, initialize removed per SEP-2575).
Building blocks:
1. `versions.lua`: `M.SUPPORTED = {"2025-06-18", "2026-07-28"}` (discover copies SUPPORTED →
the keystone check "contains 2026-07-28" stays green; `check()` contract unchanged:
nil/"" allowed, unknown → `{code=-32022, data={supported=<copy>}}`).
2. **Version detection** at request start: `MCP-Protocol-Version` header or
`_meta["io.modelcontextprotocol/protocolVersion"]`. **No version info → OLD version
(E2, decided)** — the room's existing clients speak 2025-06-18 without headers.
3. **Envelope enforcement only for 2026-07-28 clients** (the `umschlag`/`envelope` check
stays but becomes version-conditional). Old clients send no `Mcp-Method`/version header
— the check must not fire for them.
4. **initialize/ping/logging/setLevel active again, ONLY for old clients** (restore from
`_lmcp_ref @ 3409a41`, version-gated). A 2026-07-28 client calling `initialize` still
gets `-32601` — THAT keeps the `entfernt` checks green (the suite always sends
2026-07-28). Route by the version ANNOUNCED, not by "header present?".
5. Version gate: unknown → -32022 + 400 + `data.supported` (both) + `data.requested`.
Acceptance:
- Existing suite **unchanged 27/27** (nothing weakened).
- New extended suite (by @testdesigner, §5): old version → discover lists both,
initialize/ping work, no envelope requirement; new version → all 27 behaviors as before;
1900-01-01 → -32022 with both in `supported`.
- Attestation for both suites (`bullpen-attest --impl lmcp.lua --test <suite>`).
Phase-2 trap: weakening the new-spec checks ("nobody will test that anyway") — the 27 must
stand, keystone included. And routing by "header present?" is wrong; the version ANNOUNCED
is what matters.
## 5. Society routing (bullpen) — execution belongs to the society
- **@testdesigner**: (a) Phase-2 contract = extended version-aware suite (old-version cases
above); (b) that suite is the acceptance — no self-tests inside coder packages. New suite
code written in English (fresh code; PO goal (a) applies to code).
- **@coder**: one package = one file / one building block, acceptance command in the ticket
(small models: bounded units, else non-convergence — this campaign saw 3 consecutive
failures from self-test mechanics, then decision C: the suite is the test).
- **@foreman**: orchestrates; tickets with PROBLEM/WHERE/TRAPS/STEP1(@testdesigner)/STEP2/
"Dispatch, then END YOUR TURN." A green word is not acceptance — demand the
`bullpen-attest` certificate (test checksum, container with a shell, exit code as verdict).
- **Integration into lmcp.lua = operator work** (as approved by the PO last campaign), not
coder work.
- @reviewer runner is broken (opencode-permission infra) — the attestation is the
substitute acceptance.
## 6. Traps (this campaign's experience, still valid)
- Green ≠ correct: read the test before trusting it; progress = the running suite.
- Never touch the contract: suite checksum `c90450a6240f133c…` before/after every stage
(sha256 comparison as proof; PO's gaming warning).
- Never touch a running service: only `/root/spec-2026-07-28/`.
- Self-test requirements on coder = waste (3 of 4 failures); the suite is the test.
- The 308-from-curl incident in the deus container was a shell bug (`PORT=x && cmd &`
backgrounds the whole chain) — not infra; test with urllib like the suite does.
- "Header version == _meta version" (envelope rule 5) stays mandatory for new clients even
in dual mode.
## 7. Mechanics & operations
- Room: `set -a; . /etc/bullpen-room.env; set +a` (LMCP_HOST=10.115.156.241, PORT=8080);
post via `lmcp-tool room_say from=deus to=@foreman body=…` + `secret=` from
`/etc/bullpen/post-secret.d/deus` (python-subprocess works; never put text on the command
line). Sender must be in TRUST; verify in the RECIPIENT's lurker log, never in your own
ok:true.
- Reset @foreman's session BEFORE campaign start (`.session` in its lurker directory,
recreated on next poke) + restart its lurker. Lurkers resume from a stored id — a message
posted before a restart is behind them; say it again.
- One campaign per session. Escalation: local only → `[free]` catalogue, NEVER paid; if all
tiers fail, RESHAPE the package, don't escalate. A precise dead end is a result.
- Smoke recipe (tested 2026-08-09): `LMCP_PORT=<p> LMCP_TOKEN=konf-smoke LMCP_HOST=127.0.0.1
LUA_PATH="/root/spec-2026-07-28/?.lua;;" lua5.4 server.lua`; URL `http://127.0.0.1:<p>/mcp`;
headers `Authorization: Bearer konf-smoke`, `Accept: application/json, text/event-stream`,
`MCP-Protocol-Version`, `Mcp-Method`; response is SSE → extract the `data: ` line.
6 expectations: discover 200/supportedVersions; 1900-01-01 → 400/-32022+data;
**2025-06-18 → Phase 1: 400/-32022; from Phase 2 on: 200 — that is the visible dual
proof**; initialize → 404/-32601; no Mcp-Method → 400/-32020; no _meta → 400/-32602.
- Attestation: `cd /root/spec-2026-07-28 && bullpen-attest --impl lmcp.lua --test <suite>` —
the human-readable ABNAHME sheet is the certificate.
## 8. PO decisions (RESOLVED)
- **E1 — translate the existing suite? NO (decided).** The suite is the contract and its
checksum is the integrity anchor; its German comments stay. Phase 1 translates the server
code only. (New suite code from @testdesigner is written in English.)
- **E2 — clients without version info? OLD (decided).** Default to 2025-06-18 for backward
compatibility with the room's existing clients; otherwise the room breaks on rollout.
- **E3 — deployment? OPEN (PO decides when the result is ready).** The working copy is only
bundled (`harvest-bundle` → `/root/harvest.bundle`); the PO decides where/when it lands.
Note for rollout: old room clients may match on German message texts — clarify before
deploying (message texts themselves stay English).
## 9. References
- Working copy: `/root/spec-2026-07-28/` · Artifacts: `/root/harvest/artefakte_konformitaet/`
- Old version: `/root/harvest/_lmcp_ref @ 3409a41`
- Campaign workspace: `/root/harvest` (commit `6b4684f` = 27/27 state)
- Contract: `konformitaet_2026_07_28.py`, sha256 `c90450a6240f133cec63d0989aa6bac777c5b61370217f06b53342ff8c70ff11`
- lmcp.lua anchors: envelope block in `_dispatch_post` (L.11391166), `_fehler_status` L.1087,
`_status_fuer_fehlercode` L.1095, session-resolution initialize special case L.1198
(for Phase 2: replace with version-gated initialize from 3409a41)
- Original handoff: `/root/spec-2026-07-28/UEBERGABE.md`, spec excerpt `SPEC-AUSZUG.md`
-636
View File
@@ -1,636 +0,0 @@
#!/usr/bin/env lua
-- lmcp hub — fleet-wide MCP broker.
--
-- One MCP endpoint that fans out to every lmcp-backed host, with an SSH
-- fallback for hosts whose lmcp is temporarily down (or not installed).
-- Exposes a small set of "remote_*" tools that all take a `host` arg
-- naming the target in the backend registry.
--
-- Registry file (default /opt/herding/etc/hub-backends.conf):
-- # name ssh_host lmcp_url token
-- boltzmann boltzmann.fritz.box http://boltzmann.fritz.box:8080/mcp <bearer>
-- tesla tesla http://tesla.fritz.box:8080/mcp -
-- broglie - http://broglie.fritz.box:8080/mcp -
-- Use `-` for "not applicable". Lines starting with # are comments.
-- Missing `ssh_host`: no ssh fallback available. Missing `lmcp_url`: ssh-only.
--
-- SPDX-License-Identifier: MIT
local dir = arg[0]:match('(.*/)') or './'
package.path = package.path .. ';' .. dir .. '?.lua'
local lmcp = require('lmcp')
local json = require('json')
local socket = require('socket')
-- ---- Backend registry ---------------------------------------------------
local CONF_PATH = os.getenv("LMCP_HUB_BACKENDS") or "/opt/herding/etc/hub-backends.conf"
local PROBE_TTL_UP = tonumber(os.getenv("LMCP_HUB_PROBE_TTL_UP") or "30")
local PROBE_TTL_DOWN_MIN = tonumber(os.getenv("LMCP_HUB_PROBE_TTL_DOWN_MIN") or "60")
local PROBE_TTL_DOWN_MAX = tonumber(os.getenv("LMCP_HUB_PROBE_TTL_DOWN_MAX") or "900")
local PROBE_BUDGET = tonumber(os.getenv("LMCP_HUB_PROBE_BUDGET") or "3")
local LMCP_TIMEOUT = tonumber(os.getenv("LMCP_HUB_LMCP_TIMEOUT") or "6")
local SSH_TIMEOUT = tonumber(os.getenv("LMCP_HUB_SSH_TIMEOUT") or "10")
local SSH_HARD_TIMEOUT = tonumber(os.getenv("LMCP_HUB_SSH_HARD_TIMEOUT") or "30")
local LOG_REQUESTS = (os.getenv("LMCP_HUB_LOG") or "1") ~= "0"
local function logreq(fmt, ...)
if not LOG_REQUESTS then return end
io.stderr:write(string.format("[hub %s] " .. fmt .. "\n",
os.date("%H:%M:%S"), ...))
io.stderr:flush()
end
local function monotonic()
local ok, s = pcall(socket.gettime)
return ok and s or os.time()
end
local backends = {} -- name -> { name, ssh_host, lmcp_url, token }
local status = {} -- name -> { up=bool, via="lmcp"|"ssh"|nil, checked=t, err=... }
local function load_registry()
local f = io.open(CONF_PATH, "r")
if not f then
io.stderr:write("hub: no backend registry at " .. CONF_PATH .. "\n")
return
end
backends = {}
for line in f:lines() do
line = line:gsub("^%s+", ""):gsub("%s+$", "")
if line ~= "" and not line:match("^#") then
local parts = {}
for p in line:gmatch("%S+") do parts[#parts+1] = p end
if #parts >= 2 then
local name = parts[1]
local ssh_host = (parts[2] ~= "-" and parts[2]) or nil
local lmcp_url = (parts[3] ~= "-" and parts[3]) or nil
local token = (parts[4] ~= "-" and parts[4]) or nil
backends[name] = {
name = name,
ssh_host = ssh_host,
lmcp_url = lmcp_url,
token = token,
}
end
end
end
f:close()
end
-- ---- Outbound HTTP client (plain, no TLS) ------------------------------
local function parse_url(url)
local scheme, host, port, path = url:match("^(%w+)://([^:/]+):?(%d*)(/?.*)$")
if not scheme then return nil, "bad url" end
port = tonumber(port) or (scheme == "https" and 443 or 80)
if path == "" then path = "/" end
return { scheme = scheme, host = host, port = port, path = path }
end
local function http_post_json(url, body, token, timeout)
local u, err = parse_url(url)
if not u then return nil, err end
if u.scheme ~= "http" then return nil, "hub only speaks http to backends" end
local sock = socket.tcp()
sock:settimeout(timeout or 6)
local ok, e = sock:connect(u.host, u.port)
if not ok then sock:close(); return nil, "connect: " .. e end
local headers = {
"POST " .. u.path .. " HTTP/1.1",
"Host: " .. u.host .. ":" .. u.port,
"Content-Type: application/json",
"Content-Length: " .. tostring(#body),
"Connection: close",
}
if token then
headers[#headers+1] = "Authorization: Bearer " .. token
end
local req = table.concat(headers, "\r\n") .. "\r\n\r\n" .. body
local _, se = sock:send(req)
if se then sock:close(); return nil, "send: " .. se end
local chunks = {}
while true do
local data, rerr, partial = sock:receive(4096)
if data then chunks[#chunks+1] = data
elseif partial and #partial > 0 then chunks[#chunks+1] = partial
end
if not data then
if rerr == "timeout" then sock:close(); return nil, "timeout" end
break
end
end
sock:close()
local resp = table.concat(chunks)
local hend = resp:find("\r\n\r\n", 1, true)
if not hend then return nil, "no body separator" end
local status_line = resp:sub(1, resp:find("\r\n", 1, true) - 1)
local status_code = tonumber(status_line:match("HTTP/[%d%.]+%s+(%d+)"))
local body_str = resp:sub(hend + 4)
if status_code ~= 200 then
return nil, "http " .. tostring(status_code) .. ": " .. body_str:sub(1, 200)
end
local ok2, parsed = pcall(json.decode, body_str)
if not ok2 then return nil, "bad json: " .. body_str:sub(1, 200) end
return parsed
end
local function jsonrpc_call(url, token, method, params)
local body = json.encode({
jsonrpc = "2.0",
id = os.time() * 1000 + math.random(1000, 9999),
method = method,
params = params or {},
})
local resp, err = http_post_json(url, body, token, LMCP_TIMEOUT)
if not resp then return nil, err end
if resp.error then
return nil, "rpc: " .. (resp.error.message or "unknown")
end
return resp.result
end
-- ---- SSH exec (shell-escape-free via bash -s on stdin) -----------------
local function shell_quote(s)
-- Single-quote wrap, replace embedded single quotes with '\''
return "'" .. tostring(s):gsub("'", "'\\''") .. "'"
end
local function tmpwrite(content)
local path = os.tmpname()
local f = io.open(path, "w")
if not f then return nil, "tmpfile create failed" end
f:write(content)
f:close()
return path
end
local function ssh_run_script(ssh_host, script)
-- Write the script body locally, then ssh with stdin redirected from it.
-- bash -s reads the script from stdin. Nothing inside `script` is subject
-- to another shell round of expansion — this is the escape-free path.
local spath, perr = tmpwrite(script)
if not spath then return nil, perr, -1 end
local outpath = spath .. ".out"
-- Hard-cap wall time with GNU `timeout` — ssh's ConnectTimeout only
-- bounds TCP connect, not the session. Without this, a half-dead sshd
-- (auth stall, remote bash-s hang) locks the hub event loop indefinitely.
local cmd = string.format(
"timeout --kill-after=2 %d ssh -o ConnectTimeout=%d -o ServerAliveInterval=5 -o ServerAliveCountMax=2 -o BatchMode=yes -o StrictHostKeyChecking=accept-new %s 'bash -s' < %s > %s 2>&1; echo $? > %s.rc",
SSH_HARD_TIMEOUT, SSH_TIMEOUT, shell_quote(ssh_host), shell_quote(spath),
shell_quote(outpath), shell_quote(spath)
)
local t0 = monotonic()
os.execute(cmd)
local dt = monotonic() - t0
local rc_f = io.open(spath .. ".rc", "r")
local rc = -1
if rc_f then
local s = rc_f:read("*a") or ""
s = s:match("^(%S+)") or ""
rc = tonumber(s) or -1
rc_f:close()
end
local out_f = io.open(outpath, "r")
local output = out_f and out_f:read("*a") or ""
if out_f then out_f:close() end
os.remove(spath); os.remove(outpath); os.remove(spath .. ".rc")
-- timeout(1) exits 124 on wall-clock expiry, 137 on SIGKILL. Surface it.
if rc == 124 or rc == 137 then
logreq("ssh HARD-TIMEOUT host=%s after=%.2fs rc=%d", ssh_host, dt, rc)
return nil, string.format("ssh hard-timeout after %ds", SSH_HARD_TIMEOUT), rc
end
logreq("ssh host=%s elapsed=%.2fs rc=%d bytes=%d", ssh_host, dt, rc, #output)
return output, nil, rc
end
-- ---- Health probe ------------------------------------------------------
--
-- Design notes:
-- - Probe is lmcp-only. SSH is NOT checked here: it's expensive (3-6s per
-- offline host) and the hub exists specifically to absorb lots of
-- offline hosts. Hosts with lmcp down but ssh up show as DOWN in the
-- health list, but actual remote_* calls still fall through to ssh
-- fallback correctly.
-- - Sticky DOWN cache with exponential backoff: a DOWN host is re-probed
-- at intervals that grow from PROBE_TTL_DOWN_MIN (default 60s) up to
-- PROBE_TTL_DOWN_MAX (default 900s). Prevents a sleeping fleet from
-- burning probe budget every health check.
-- - `remote_list_hosts` uses a parallel curl fan-out for all hosts at
-- once so wall-clock is bounded by PROBE_BUDGET (default 3s), not the
-- sum of per-host timeouts.
local function cache_fresh(s, now)
if not s then return false end
local ttl
if s.up then
ttl = PROBE_TTL_UP
else
ttl = math.min(PROBE_TTL_DOWN_MAX, PROBE_TTL_DOWN_MIN * (2 ^ (s.down_streak or 0)))
end
return (now - s.checked) < ttl
end
local function apply_probe_result(name, up, err, via, tool_count)
local prev = status[name]
local new_s = {
checked = os.time(),
up = up,
via = up and (via or "lmcp") or nil,
err = err,
tool_count = tool_count,
down_streak = up and 0 or ((prev and prev.down_streak or 0) + 1),
}
status[name] = new_s
return new_s
end
-- Fallback single-host probe used only when a caller explicitly needs a
-- status refresh outside the parallel path (currently only
-- remote_list_hosts when a single host is looked up). lmcp-only.
local function probe(name, force)
local b = backends[name]
if not b then return { up = false, err = "unknown host" } end
local s = status[name]
if not force and cache_fresh(s, os.time()) then return s end
if not b.lmcp_url then
return apply_probe_result(name, false, "no lmcp url", nil, nil)
end
local result, err = jsonrpc_call(b.lmcp_url, b.token, "tools/list", {})
if result and result.tools then
return apply_probe_result(name, true, nil, "lmcp", #result.tools)
end
return apply_probe_result(name, false, "lmcp: " .. tostring(err), nil, nil)
end
-- Parallel lmcp probe for every backend with an lmcp_url, via a single
-- bash fan-out of curl calls. Total wall clock ≈ PROBE_BUDGET.
local function probe_all_parallel(force)
local now = os.time()
local need = {}
for name, b in pairs(backends) do
if b.lmcp_url and (force or not cache_fresh(status[name], now)) then
need[#need+1] = b
end
end
if #need == 0 then return end
local script_parts = {}
for _, b in ipairs(need) do
local auth = b.token and (" -H 'Authorization: Bearer " .. b.token .. "'") or ""
local url = b.lmcp_url:gsub("'", "'\\''")
script_parts[#script_parts+1] = string.format(
"(curl --max-time %d -s -o /dev/null -w '%s %%{http_code} %%{time_total}\\n' -X POST%s -H 'Content-Type: application/json' -d '{\"jsonrpc\":\"2.0\",\"id\":1,\"method\":\"tools/list\",\"params\":{}}' '%s' || echo '%s ERR 0') &",
PROBE_BUDGET, b.name, auth, url, b.name
)
end
script_parts[#script_parts+1] = "wait"
local t0 = monotonic()
local p = io.popen(table.concat(script_parts, "\n"))
local out = p and p:read("*a") or ""
if p then p:close() end
local dt = monotonic() - t0
local seen = {}
for line in out:gmatch("[^\n]+") do
local name, code, t = line:match("^(%S+)%s+(%S+)%s+([%d%.]+)")
if name then
seen[name] = true
local is_up = (code == "200")
if is_up then
apply_probe_result(name, true, nil, "lmcp", nil)
else
apply_probe_result(name, false, "lmcp code=" .. code, nil, nil)
end
end
end
-- Hosts with no output (fan-out error): mark DOWN
for _, b in ipairs(need) do
if not seen[b.name] then
apply_probe_result(b.name, false, "probe fan-out missing", nil, nil)
end
end
logreq("probe_all_parallel n=%d elapsed=%.2fs", #need, dt)
end
-- ---- Call-tool dispatcher ----------------------------------------------
-- `allow_ssh`: whether the tool has an SSH fallback path
-- `ssh_impl`: function(backend, args) -> output string, or nil + error
local function call_remote(tool, args, allow_ssh, ssh_impl)
local t_start = monotonic()
local host = args.host
if type(host) ~= "string" or host == "" then
logreq("tool=%s ERR missing-host", tool)
return "Error: missing `host` parameter"
end
local b = backends[host]
if not b then
logreq("tool=%s host=%s ERR unknown-host", tool, host)
return string.format("Error: unknown host %q (registry: %s)", host, CONF_PATH)
end
local errs = {}
local via = nil
-- Try lmcp first
if b.lmcp_url then
local args_pass = {}
for k, v in pairs(args) do if k ~= "host" then args_pass[k] = v end end
local result, err = jsonrpc_call(b.lmcp_url, b.token, "tools/call",
{ name = tool, arguments = args_pass })
if result then
via = "lmcp"
logreq("tool=%s host=%s via=lmcp elapsed=%.2fs", tool, host, monotonic() - t_start)
-- Propagate backend's content, extract first text block
if type(result.content) == "table" and result.content[1] and result.content[1].text then
local prefix = result.isError and "[lmcp isError] " or ""
return prefix .. result.content[1].text
end
return json.encode(result)
end
errs[#errs+1] = "lmcp: " .. tostring(err)
else
errs[#errs+1] = "lmcp: no url configured"
end
-- Fallback to ssh
if allow_ssh and ssh_impl and b.ssh_host then
local out, serr = ssh_impl(b, args)
if out ~= nil then
logreq("tool=%s host=%s via=ssh elapsed=%.2fs", tool, host, monotonic() - t_start)
return "[via ssh fallback]\n" .. out
end
errs[#errs+1] = "ssh: " .. tostring(serr)
elseif allow_ssh and not b.ssh_host then
errs[#errs+1] = "ssh: no host configured"
end
logreq("tool=%s host=%s FAIL elapsed=%.2fs err=%s", tool, host, monotonic() - t_start, table.concat(errs, " | "):sub(1, 200))
return "Error: " .. table.concat(errs, " | ")
end
-- ---- SSH implementations for each fallback-capable tool ----------------
local function ssh_shell(b, a)
if type(a.command) ~= "string" then return nil, "command required" end
local cwd_prefix = ""
if a.cwd and a.cwd ~= "" then
cwd_prefix = "cd " .. shell_quote(a.cwd) .. " && "
end
local script = cwd_prefix .. a.command .. "\n"
local out, err, rc = ssh_run_script(b.ssh_host, script)
if err then return nil, err end
-- ssh exits 255 when it couldn't connect/authenticate. Treat that as
-- fallback-failed so the caller sees the combined lmcp+ssh errors.
if rc == 255 then
local line1 = (out:match("^([^\n]+)") or ""):gsub("%s+$", "")
return nil, "ssh connect failed: " .. line1
end
return string.format("[rc=%d]\n%s", rc, out)
end
local function ssh_read_file(b, a)
if type(a.path) ~= "string" then return nil, "path required" end
local script = "cat -- " .. shell_quote(a.path) .. "\n"
local out, err, rc = ssh_run_script(b.ssh_host, script)
if err then return nil, err end
if rc ~= 0 then return nil, "cat exit " .. rc .. ": " .. out:sub(1, 200) end
return out
end
local function ssh_write_file(b, a)
if type(a.path) ~= "string" or type(a.content) ~= "string" then
return nil, "path and content required"
end
-- Emit a script that decodes content from base64, so no shell
-- interpretation of the payload is possible. Lua stdlib has no base64;
-- implement a minimal encoder inline.
local b64_alphabet = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/"
local function b64enc(data)
local out = {}
local pad = (3 - (#data % 3)) % 3
local padded = data .. ("\0"):rep(pad)
for i = 1, #padded, 3 do
local a1, a2, a3 = padded:byte(i), padded:byte(i+1), padded:byte(i+2)
local n = a1 * 65536 + a2 * 256 + a3
out[#out+1] = b64_alphabet:sub(((n >> 18) & 63) + 1, ((n >> 18) & 63) + 1)
out[#out+1] = b64_alphabet:sub(((n >> 12) & 63) + 1, ((n >> 12) & 63) + 1)
out[#out+1] = b64_alphabet:sub(((n >> 6) & 63) + 1, ((n >> 6) & 63) + 1)
out[#out+1] = b64_alphabet:sub(((n ) & 63) + 1, ((n ) & 63) + 1)
end
local s = table.concat(out)
if pad > 0 then s = s:sub(1, -pad - 1) .. ("="):rep(pad) end
return s
end
local payload = b64enc(a.content)
local script = string.format(
"base64 -d > %s <<'EOF_B64_PAYLOAD'\n%s\nEOF_B64_PAYLOAD\n",
shell_quote(a.path), payload
)
local out, err, rc = ssh_run_script(b.ssh_host, script)
if err then return nil, err end
if rc ~= 0 then return nil, "write exit " .. rc .. ": " .. out:sub(1, 200) end
return string.format("Written %d bytes to %s (via ssh)", #a.content, a.path)
end
local function ssh_list_dir(b, a)
local path = (a and type(a.path) == "string" and a.path) or "."
local script = "ls -1 -- " .. shell_quote(path) .. "\n"
local out, err, rc = ssh_run_script(b.ssh_host, script)
if err then return nil, err end
if rc ~= 0 then return nil, "ls exit " .. rc .. ": " .. out:sub(1, 200) end
return out
end
local function ssh_search_files(b, a)
if type(a.pattern) ~= "string" then return nil, "pattern required" end
local path = (a.path and a.path ~= "") and a.path or "/"
local script = string.format(
"find %s -name %s 2>/dev/null | head -200\n",
shell_quote(path), shell_quote(a.pattern)
)
local out, err, rc = ssh_run_script(b.ssh_host, script)
if err then return nil, err end
return out
end
-- ---- Server setup ------------------------------------------------------
load_registry()
math.randomseed(os.time())
local server = lmcp.new(os.getenv("LMCP_NAME") or "hub-tools", {
port = tonumber(os.getenv("LMCP_PORT") or arg[1]) or 8090,
version = "0.5.4",
conf = os.getenv("LMCP_HUB_CONF") or "/opt/herding/etc/lmcp-hub.conf",
})
local HOST_ARG = {
type = "string",
description = "Name of the backend host (see remote_list_hosts)",
}
server:tool("remote_list_hosts",
"List all registered fleet hosts and their live status (lmcp vs ssh vs down).",
{ type = "object", properties = {
force = { type = "boolean", description = "Force re-probe (bypass cache)", default = false },
} },
function(a)
local force = a and a.force or false
probe_all_parallel(force)
local lines = {}
local names = {}
for n in pairs(backends) do names[#names+1] = n end
table.sort(names)
for _, n in ipairs(names) do
local b = backends[n]
local s = status[n] or { up = false, err = "no probe result" }
local paths = {}
if b.lmcp_url then paths[#paths+1] = "lmcp" end
if b.ssh_host then paths[#paths+1] = "ssh" end
lines[#lines+1] = string.format(
"%-14s %-6s via=%-4s paths=%s %s",
n,
s.up and "UP" or "DOWN",
tostring(s.via or "-"),
table.concat(paths, ","),
s.err and ("[" .. s.err .. "]") or ""
)
end
return table.concat(lines, "\n")
end,
{ annotations = {
title = "List fleet hosts",
readOnlyHint = true,
destructiveHint = false,
idempotentHint = true,
openWorldHint = true,
} }
)
server:tool("remote_shell", "Run a shell command on a fleet host. lmcp-primary with ssh fallback.",
{ type = "object", properties = {
host = HOST_ARG,
command = { type = "string", description = "Shell command" },
cwd = { type = "string", description = "Working directory" },
timeout = { type = "integer", description = "Timeout (seconds)", default = 120 },
}, required = { "host", "command" } },
function(a) return call_remote("shell", a, true, ssh_shell) end,
{ annotations = {
title = "Remote shell",
readOnlyHint = false,
destructiveHint = true,
idempotentHint = false,
openWorldHint = true,
} }
)
server:tool("remote_read_file", "Read a file from a fleet host.",
{ type = "object", properties = {
host = HOST_ARG,
path = { type = "string", description = "File path" },
}, required = { "host", "path" } },
function(a) return call_remote("read_file", a, true, ssh_read_file) end,
{ annotations = {
title = "Remote read file",
readOnlyHint = true,
destructiveHint = false,
idempotentHint = true,
openWorldHint = true,
} }
)
server:tool("remote_write_file", "Write content to a file on a fleet host.",
{ type = "object", properties = {
host = HOST_ARG,
path = { type = "string" },
content = { type = "string" },
}, required = { "host", "path", "content" } },
function(a) return call_remote("write_file", a, true, ssh_write_file) end,
{ annotations = {
title = "Remote write file",
readOnlyHint = false,
destructiveHint = true,
idempotentHint = true,
openWorldHint = true,
} }
)
server:tool("remote_edit_file",
"Literal-match edit on a fleet host. **Requires backend lmcp up** — no ssh fallback.",
{ type = "object", properties = {
host = HOST_ARG,
path = { type = "string" },
old_string = { type = "string" },
new_string = { type = "string" },
replace_all = { type = "boolean", default = false },
}, required = { "host", "path", "old_string", "new_string" } },
function(a) return call_remote("edit_file", a, false, nil) end,
{ annotations = {
title = "Remote edit file",
readOnlyHint = false,
destructiveHint = true,
idempotentHint = false,
openWorldHint = true,
} }
)
server:tool("remote_shell_bg",
"Launch a detached background command on a fleet host (Linux). Returns PID + log path immediately. Requires backend lmcp v0.5.2+.",
{ type = "object", properties = {
host = HOST_ARG,
command = { type = "string", description = "Shell command" },
cwd = { type = "string" },
log = { type = "string", description = "Log file path" },
}, required = { "host", "command" } },
function(a) return call_remote("shell_bg", a, false, nil) end,
{ annotations = {
title = "Remote shell (background)",
readOnlyHint = false,
destructiveHint = true,
idempotentHint = false,
openWorldHint = true,
} }
)
server:tool("remote_list_dir", "List directory entries on a fleet host.",
{ type = "object", properties = {
host = HOST_ARG,
path = { type = "string", default = "." },
}, required = { "host" } },
function(a) return call_remote("list_dir", a, true, ssh_list_dir) end,
{ annotations = {
title = "Remote list directory",
readOnlyHint = true,
destructiveHint = false,
idempotentHint = true,
openWorldHint = true,
} }
)
server:tool("remote_search_files", "find-by-pattern on a fleet host.",
{ type = "object", properties = {
host = HOST_ARG,
pattern = { type = "string" },
path = { type = "string", default = "/" },
}, required = { "host", "pattern" } },
function(a) return call_remote("search_files", a, true, ssh_search_files) end,
{ annotations = {
title = "Remote find files",
readOnlyHint = true,
destructiveHint = false,
idempotentHint = true,
openWorldHint = true,
} }
)
io.stderr:write(string.format("lmcp-hub starting on port %d with %d backends from %s\n",
server.port, (function() local n = 0; for _ in pairs(backends) do n = n + 1 end; return n end)(), CONF_PATH))
server:run()
+25
View File
@@ -0,0 +1,25 @@
INTEGRATION DONE (deus, Operator) — Suite gemessen: 19/27 zu, 8 OFFEN.
Was ich tat (Basis job667.lua, coder Lauf 5):
- umschlag.lua = job667 + die zwei von dir benannten Fixes (#671/#674),
angewandt beim Integrieren: Regel 3 mcp-name-KOPF, Regel 4 _meta unter
rumpf.params._meta. Lokal gegen die Suite-Szenarien verifiziert (11/11).
- In lmcp.lua verdrahtet: require + Envelope-Check in _dispatch_post nach dem
Response-Routing, Fehler via _fehler_status -> 400. Kein anderer Zweig
angefasst; laufender Raum-Server unberuehrt (nur Arbeitskopie).
- Suite gelaufen, Vertrag unveraendert (sha256 c90450a6... identisch).
Zugewachsen: header 5/5, _meta 3/3 (waren 0). Keine Regression (discover 4/5,
result, method, codes, transport weiter gruen).
Offen (8), alle Operator-Arbeit (Lua-Implementierungsdateien, keine
Roster-Rolle darf die aendern):
- version 4: unbekannte Fassung -> -32022 + 400 + data.supported/requested.
versions.check haengt nur am initialize-Zweig (Z.599), nicht am
Request-Pfad. Anbinden im Umschlag erfordert "2026-07-28" in
versions.SUPPORTED (discover kopiert die Liste, Z.576) -> dreht den
Schlussstein mit. => letzter Block.
- entfernt 3: initialize/ping/logging/setLevel -> -32601.
- Schlussstein 1: supportedVersions enthaelt 2026-07-28 (bleibt rot bis 26 zu).
@coder-Umschlag-Teil abgeschlossen. Naechste Portionen fahre ich als Operator.
+25
View File
@@ -0,0 +1,25 @@
KAMPAGNE ABGESCHLOSSEN (deus) — lmcp.lua: 27/27, exit 0.
Attest (bullpen-attest, eigene Server-Instanz):
urteil GRUEN, pruefling sha256 856a07c4, test sha256 c90450a6
(identisch mit Uebergabe — Vertrag unveraendert, nichts abgeschwaecht)
Geaendert (3 Dateien, Artefakte in /root/harvest/artefakte_konformitaet/):
- umschlag.lua (neu): Envelope-Pruefung; Basis job667.lua (coder Lauf 5)
+ die zwei von dir benannten Fixes (#671/#674), angewandt beim Integrieren.
- lmcp.lua: Envelope-Check in _dispatch_post + Fassungs-Check (Schlussstein,
-32022 + 400 + data.supported/requested); SEP-2575: initialize/ping/
logging/setLevel entfernt -> -32601/404.
- versions.lua: M.SUPPORTED = {"2026-07-28"} — NUR die neue Fassung.
(2025-06-18 nicht genannt: initialize ist weg, die alte Verhandlung tot.)
Belege: /root/harvest/abnahme_2026_07_28.md, /root/harvest.bundle (2 Commits).
Dein Anteil ist verbucht: job667 als Basis, die Suite als Vertrag.
Laufender Raum-Server wurde nicht angetastet — es wurde nur die Arbeitskopie
in /root/spec-2026-07-28/ veraendert; Deployment entscheidet der Mensch.
Folgephase (Dual-Protokoll alt+neu) ist laut PO ein spaeterer, eigener
Schritt — dann mit frischer Sitzung und @foreman-Session-Reset als Setup.
@reviewer-Runner bleibt defekt (opencode-permission-Infra); Ersatz-Abnahme
war das bullpen-attest-Zertifikat.
+9
View File
@@ -0,0 +1,9 @@
Alive-Check (deus, Kampagnen-Setup abgeschlossen). Antworte nur mit ack, kein Dispatch.
Hintergrund: deine opencode-Sitzung wurde vor dieser Kampagne zurückgesetzt (Session-Reset
als Setup, `.session` gelöscht, Lurker neu gestartet) — diese Anfrage erzeugt die frische
Sitzung. Neue Kampagne laut PO: (a) English-only (Phase 1), danach (b) Dual-Protokoll
(Phase 2). Kein Inhalt aus der letzten Kampagne übernehmen; ein Session = eine Kampagne.
Bestätige nur: (1) du bist wach, (2) deine Sitzung ist frisch, (3) du bist bereit für den
Kampagnen-Kickoff. Kein weiterer Schritt.
+122
View File
@@ -0,0 +1,122 @@
KAMPAGNE (a) ENGLISH-ONLY — KICKOFF, PACKAGE 1/2: envelope.lua
PROBLEM
PO-Order: replace all German terms (code and comments) with English translations. Phase 1 is
purely mechanical — renames and comment translations ONLY, zero behavior change. The conformance
suite (27/27, sha256 c90450a6…, Vertrag unveraendert) is the regression net. Erste Stufe:
umschlag.lua wird zu envelope.lua (Dateiname + Modul-Funktion + Parameter), dann versions.lua,
dann lmcp.lua (Integration = Operator-Arbeit). KEINE Logik aendern.
WHERE
Der Coder arbeitet in seiner Sandbox (Container grind, /var/lib/bullpen/coder/). Die
Quelldatei liegt NICHT dort — der vollstaendige Inhalt von umschlag.lua steht weiter unten
in diesem Ticket. Aus ihm entsteht envelope.lua. Die Zieldatei ist ein REINES Modul.
TRAPS (Verboten — das sind die plausiblen Fehler)
- NICHT die Logik aendern: Regeln 15 bleiben exakt (Fehlercodes -32020/-32602, Reihenfolge,
Meldungstexte Englisch wie sie sind). Es sind NUR Identifier-Umbenennungen + Kommentar-Uebersetzung.
- KEIN Selbsttest in der Datei (Entscheidung C der letzten Kampagne: die Suite ist der Test).
- KEINE Fences/Backticks/Markdown — Zeile 1 echte Lua, `return M` als LETZTE Zeile, danach nichts.
- KEINE weiteren Funktionen, keine print-Ausgaben im Modul.
- Die Funktion heisst NEU `M.check` (nicht mehr `M.pruefe`), Parameter heissen NEU `header`
(nicht `kopf`) und `body` (nicht `rumpf`). Lua-Tabellenzugriffe `kopf[...]``header[...]`,
`rumpf.method``body.method`, `rumpf.params``body.params`.
- Der Dateiname im Ticket ist nur ein Hinweis — geliefert wird der DATEIINHALT.
ANFORDERUNG (Zieldatei envelope.lua)
Nimm den untenstehenden Quelltext von umschlag.lua und erzeuge daraus envelope.lua:
1. Datei-Kommentarzeilen (oben, -- umschlag.lua … / -- Basis: job667.lua … / -- kopf: … / -- rumpf: … /
-- Rueckgabe: …) ins Englische uebersetzen, sinngemaess (die deutsche Provenienz darf bleiben,
z.B. "Based on job667.lua (coder run 5, #669)").
2. `function M.pruefe(kopf, rumpf)``function M.check(header, body)`; alle Verwendungen von
`kopf``header`, `rumpf``body` im Funktionskoerper.
3. Inline-Kommentare (-- Regel 1 … -- Erfolg) ins Englische uebersetzen.
4. Regeln, Codes, Meldungstexte, Reihenfolge: UNVERAENDERT.
5. Struktur: `local M = {}``return M` (letzte Zeile). Kein Selbsttest.
ABNAHME (Coder-seitig)
- Sandbox-Parse-Check: `lua5.4 envelope.lua` → exit 0, keine stderr.
- Melde: (1) Dateiinhalt als Rohtext ohne Fences, (2) sha256, (3) Parse-Check-Ergebnis.
- Der Operator integriert, laesst die Suite laufen (27/27 muss gruen bleiben) und attestiert.
QUELLTEXT (umschlag.lua, komplett):
```
-- umschlag.lua: Kopf-/Envelope-Pruefung nach Spec 2026-07-28
-- Basis: job667.lua (coder Lauf 5, #669) — parst, reine Struktur, zwei
-- Defekte per Foreman-Review #671/#674 hier korrigiert:
-- Regel 3: mcp-name-KOPF wird geprueft (war nur params.name/uri)
-- Regel 4: _meta liegt unter rumpf.params._meta (war rumpf.meta)
local M = {}
-- kopf: Tabelle der HTTP-Kopfzeilen, Namen KLEINGESCHRIEBEN, z.B.
-- {['mcp-method']='tools/list',['mcp-protocol-version']='2026-07-28'}
-- rumpf: dekodierte JSON-RPC-Anfrage als Lua-Tabelle
-- {jsonrpc='2.0',id=1,method='tools/list',params={_meta={...}}}
-- Rueckgabe: true,nil,nil | false,<fehlercode>,<meldung>
function M.pruefe(kopf, rumpf)
if not kopf or not rumpf then
return false, -32600, "Invalid Request"
end
-- Regel 1: k['mcp-protocol-version'] fehlt
if not kopf['mcp-protocol-version'] then
return false, -32020, 'Missing required header: MCP-Protocol-Version'
end
-- Regel 2: k['mcp-method'] fehlt
if not kopf['mcp-method'] then
return false, -32020, 'Missing required header: Mcp-Method'
end
-- Regel 2: k['mcp-method'] ~= rumpf.method
if kopf['mcp-method'] ~= rumpf.method then
return false, -32020, 'Mcp-Method header does not match body'
end
-- Regel 3: Mcp-Name-Kopf Pflicht bei tools/call, resources/read, prompts/get
local method = rumpf.method
local params = rumpf.params or {}
if method == 'tools/call' or method == 'prompts/get' then
if not kopf['mcp-name'] or kopf['mcp-name'] ~= params.name then
return false, -32020, 'Mcp-Name header does not match body'
end
elseif method == 'resources/read' then
if not kopf['mcp-name'] or kopf['mcp-name'] ~= params.uri then
return false, -32020, 'Mcp-Name header does not match body'
end
end
-- Regel 4: _meta-Pflichtfelder (liegen unter params._meta)
local meta = (rumpf.params or {})._meta or {}
if not meta['io.modelcontextprotocol/protocolVersion'] then
return false, -32602, 'Missing required _meta field: io.modelcontextprotocol/protocolVersion'
end
if not meta['io.modelcontextprotocol/clientCapabilities'] then
return false, -32602, 'Missing required _meta field: io.modelcontextprotocol/clientCapabilities'
end
-- Regel 5: k['mcp-protocol-version'] ~= meta['io.modelcontextprotocol/protocolVersion']
if kopf['mcp-protocol-version'] ~= meta['io.modelcontextprotocol/protocolVersion'] then
return false, -32020, 'Header MCP-Protocol-Version does not match _meta protocolVersion'
end
-- Erfolg
return true, nil, nil
end
return M
```
STEP 1 @testdesigner: NICHT noetig fuer Phase 1 — der Vertrag existiert bereits
(konformitaet_2026_07_28.py, sha256 c90450a6240f133c…, 27/27 gruen, E1-Entscheidung: Suite
bleibt unveraendert). @testdesigner kommt erst in Phase 2 (Dual-Protokoll-Suite) zum Zug.
STEP 2 @coder: envelope.lua aus dem Quelltext oben erzeugen (Regeln/TRAPS beachten),
Parse-Check in der Sandbox, Inhalt + sha256 melden.
CONSTRAINTS
- Nur /var/lib/bullpen/coder/ in der Sandbox. Kein laufender Dienst, keine anderen Dateien.
- Lieferung als Rohtext im Raum, keine Fences.
Dispatch, then END YOUR TURN.
+79
View File
@@ -0,0 +1,79 @@
KAMPAGNE (b) DUAL-PROTOKOLL — KICKOFF, SCHRITT 1 @TESTDESIGNER: ERWEITERTE SUITE
PROBLEM
PO-Go für Phase 2 (Dual-Protokoll) ist erteilt. Die bestehende Suite
(konformitaet_2026_07_28.py, 27/27, sha256 c90450a6240f133c…, Vertrag) bleibt
UNVERAENDERT — sie ist der Regression-Net für die neue Fassung. Phase 2 braucht eine
ERWEITERTE, version-aware Suite als neuen Vertrag für den Dual-Modus: alte Fassung
(2025-06-18) und neue Fassung (2026-07-28) müssen beide funktionieren, version-routed.
Der Vertrag kommt zuerst: die erweiterte Suite wird geschrieben und verankert, BEVOR
irgendetwas implementiert wird.
WHERE
Die Arbeitskopie lebt in /root/spec-2026-07-28/ (lmcp.lua, server.lua, json.lua,
envelope.lua, versions.lua, konformitaet_2026_07_28.py, SPEC-AUSZUG.md, UEBERGABE.md).
Die erweiterte Suite ist eine NEUE Datei neben der alten — die alte wird nicht angefasst.
Referenz für das Alt-Verhalten: /root/harvest/_lmcp_ref @ 3409a41 (initialize/ping/
logging/setLevel, Session-Auflösung — die alte Fassung lebt heute auf dem Raum-Server).
TRAPS (Verboten — die plausiblen Fehler)
- NICHT die bestehende Suite ändern oder schwächen: konformitaet_2026_07_28.py bleibt
byte-identisch (sha256 c90450a6…), der 27er-Bestand muss weiter grün sein. Die neue
Suite ist eine NEUE Datei (z.B. konformitaet_dual.py), die die alten 27 Fälle ÜBERNIMMT
(kopiert, nicht modifiziert) und die Alt-Fassungs-Fälle HINZUFÜGT.
- KEINE Self-Tests im Coder-Teil: die Suite ist der Test.
- NICHT nach "Header vorhanden?" routen: die ANGEKÜNDIGTE Version entscheidet
(MCP-Protocol-Version-Header ODER _meta["io.modelcontextprotocol/protocolVersion"]).
- KEINE Version angekündigt -> ALTE Fassung (2025-06-18), PO-Entscheidung E2 — die
Raum-Klienten sprechen 2025-06-18 ohne Header.
- Kein Zugriff auf den laufenden Raum-Server: die Suite testet die Arbeitskopie.
LMCP_HOST/LMCP_PORT/LMCP_TOKEN gegen einen LOKALEN Testserver aus server.lua.
ANFORDERUNG (Zieldatei: konformitaet_dual.py im Verzeichnis der Arbeitskopie)
Schreibe eine version-aware Conformance-Suite in PYTHON (frisch geschriebener Code auf
Englisch). Sie testet gegen einen lokal gestarteten Server (server.lua) via
urllib/HTTP wie die bestehende Suite. Abdeckung:
A) NEUE Fassung (2026-07-28) — alle 27 Verhalten der bestehenden Suite, unverändert
übernommen (die 27 Prüfungen der alten Suite als Teilmenge; bei Abweichung ist die
Übernahme falsch). Insbesondere: initialize -> -32601/404 (entfernt),
Mcp-Method/Header-Pflicht, _meta-Pflicht, Version-Gate unbekannt -> -32022 mit
data.supported=['2026-07-28'] (hier: mit BEIDEN Fassungen, s.u.), Keystone
"discover enthält 2026-07-28".
B) ALTE Fassung (2025-06-18) — die Alt-Verhalten, version-routed:
1. server/discover mit Header MCP-Protocol-Version: 2025-06-18 -> 200,
supportedVersions enthält BEIDE: "2026-07-28" UND "2025-06-18".
2. initialize (2025-06-18) -> 200, NICHT -32601 (alte Methode wieder aktiv).
3. ping (2025-06-18) -> 200.
4. logging/setLevel (2025-06-18, level="debug") -> 200.
5. KEINE Envelope-Pflicht für alte Klienten: POST ohne Mcp-Method-Header und ohne
MCP-Protocol-Version-Header -> NICHT -32020 (die alte Fassung kennt den
Envelope-Check nicht; Default-Routing = alt, E2).
6. Unbekannte Version (z.B. 1900-01-01) -> 400, code -32022, data.supported enthält
BEIDE Fassungen, data.requested = '1900-01-01'.
C) KONSISTENZ: derselbe Request mit derselben angekündigten Version muss dasselbe
Ergebnis geben, egal ob die Version im Header oder in _meta steht (wo beides
zulässig ist).
Der Testserver-Aufruf erfolgt wie in der alten Suite (LMCP_PORT aus env mit Default,
LUA_PATH auf das Arbeitskopie-Verzeichnis, server.lua starten). Der Test ist ein
runnahes Skript: `python3 konformitaet_dual.py lmcp.lua` -> Exit 0 und Zähler
"X ok, 0 OFFEN" analog zur alten Suite.
ABNAHME (@testdesigner selbst)
- Die Suite LÄUFT gegen die Arbeitskopie, sobald sie implementiert ist — jetzt noch
nicht: der Implementierungsstand ist alt (nur 2026-07-28). Lieferung = die neue
Datei konformitaet_dual.py (Rohtext, keine Fences) + sha256 + Erklärung, welche
Prüfungen übernommen (27) und welche neu (B-Teil) sind.
- WICHTIG: die alte Suite muss unverändert bleiben (sha256 c90450a6…) — mitliefern als
Beleg.
CONSTRAINTS
- Nur die neue Datei konformitaet_dual.py entsteht. Keine Änderung an lmcp.lua,
versions.lua, envelope.lua, server.lua, konformitaet_2026_07_28.py.
- Neuer Code auf Englisch (PO-Ziel (a)).
- Lieferung als Rohtext im Raum, keine Fences.
Dispatch, then END YOUR TURN.
+1
View File
@@ -0,0 +1 @@
Warning: No project session found with id 'phase-b2'; creating a new session with that id.
+1
Submodule phase-b3-delivery added at d8e35e281d
BIN
View File
Binary file not shown.
+2
View File
@@ -0,0 +1,2 @@
Warning: No project session found with id 'phase-b3'; creating a new session with that id.
503: {"message":"backend busy (admission gate), retry shortly","type":"backend_busy"}
+1
View File
@@ -0,0 +1 @@
Warning: No project session found with id 'phase-c'; creating a new session with that id.
+1
View File
@@ -0,0 +1 @@
Warning: No project session found with id 'phase-c1'; creating a new session with that id.
+208
View File
@@ -0,0 +1,208 @@
# MCP 2026-07-28 — normative Auszüge
Am 2026-08-09 aus der Primärquelle geholt. **deus hat kein Netz**, deshalb liegt
das hier als Datei. Wer eine Route hat, holt lieber das Original:
https://modelcontextprotocol.io/specification/2026-07-28/
https://modelcontextprotocol.io/specification/2026-07-28/basic
https://modelcontextprotocol.io/specification/2026-07-28/basic/versioning
https://modelcontextprotocol.io/specification/2026-07-28/server/discover
https://modelcontextprotocol.io/specification/2026-07-28/basic/transports/streamable-http
Alles unten ist **wörtlich zitiert**, nicht zusammengefasst. Wo ich etwas
erkläre, steht es als Einrückung darunter.
---
## Ergebnisse
> „Result responses **MUST** include a `result` field."
> „The `result` **MUST** include a `resultType` field to indicate the type of the
> result."
> „A `resultType` of `"complete"` indicates the request completed successfully
> and the result contains the final content."
> „A `resultType` of `"input_required"` indicates the request is incomplete and
> more information is needed."
> „For backward compatibility with servers implementing earlier protocol
> versions, which do not include `resultType`, clients **MUST** treat an absent
> `resultType` as `"complete"`."
## Fehlercodes
> „`-32000` to `-32019` — **legacy.** … New codes **MUST NOT** be allocated in
> this sub-range … Apart from `-32002` (see below), receivers **MUST NOT**
> assume any specific meaning for these codes."
> „`-32020` to `-32099` — **reserved for the MCP specification.** …
> Implementations **MUST NOT** emit any code from this sub-range that is not
> defined by this specification."
| Code | Name |
|---|---|
| `-32020` | `HeaderMismatch` |
| `-32021` | `MissingRequiredClientCapability` |
| `-32022` | `UnsupportedProtocolVersion` |
> „Implementations of this protocol version **MUST NOT** emit these codes:
> `-32002` — resource not found (2025-11-25 and earlier; replaced by `-32602`).
> … `-32042` — URL elicitation required (2025-11-25 only)."
## `_meta` je Anfrage
| Schlüssel | Typ | Pflicht |
|---|---|---|
| `io.modelcontextprotocol/protocolVersion` | `string` | **Ja** |
| `io.modelcontextprotocol/clientCapabilities` | `ClientCapabilities` | **Ja** |
| `io.modelcontextprotocol/clientInfo` | `Implementation` | nein |
| `io.modelcontextprotocol/logLevel` | `LoggingLevel` | nein |
> „A request missing any required field is malformed; the server **MUST** reject
> it with JSON-RPC error code `-32602` (Invalid params). On HTTP, the response
> status **MUST** be `400 Bad Request`."
> „A server **MUST NOT** rely on capabilities the client has not declared. If
> processing a request requires a capability the client did not include … the
> server **MUST** return a `MissingRequiredClientCapabilityError` (`-32021`)
> whose `data.requiredCapabilities` lists the missing capabilities."
Antwortseitig:
> „Servers **SHOULD** include … `io.modelcontextprotocol/serverInfo` … in every
> result's `_meta`."
## Zustandslosigkeit
> „The Model Context Protocol (MCP) is a **stateless protocol** … Servers **MUST
> NOT** rely on prior requests over the same connection to establish context
> (e.g., capabilities, protocol version, client identity). Every request supplies
> this metadata in its `_meta` field."
> „State that needs to span multiple requests … **MUST** be referenced by an
> explicit identifier the client passes on each request."
## Fassungsaushandlung
> „There is no negotiation handshake. Every request carries its protocol version,
> and the server accepts or rejects each request independently."
> „If the server does not implement the requested version … it **MUST** respond
> with an `UnsupportedProtocolVersionError` listing the versions it does
> support."
Das Beispiel der Spec, vollständig — **`requested` gehört dazu**:
```json
{"jsonrpc":"2.0","id":1,
"error":{"code":-32022,"message":"Unsupported protocol version",
"data":{"supported":["2026-07-28","2025-11-25"],
"requested":"1900-01-01"}}}
```
> „Servers **MUST** implement `server/discover`."
## `server/discover`
> „`server/discover` lets a client query a server's supported protocol versions,
> capabilities, and identity before sending any other requests. Servers **MUST**
> implement it."
Antwortform aus dem Spec-Beispiel — **Feldnamen genau so**:
```json
{"jsonrpc":"2.0","id":"discover-1",
"result":{
"resultType":"complete",
"supportedVersions":["2026-07-28"],
"capabilities":{"tools":{},"resources":{}},
"_meta":{"io.modelcontextprotocol/serverInfo":{"name":"…","version":"…"}},
"instructions":"…",
"ttlMs":3600000,
"cacheScope":"public"}}
```
> **Achtung, zwei Fallen, die mich einen Durchlauf gekostet haben:** es heißt
> `supportedVersions`, **nicht** `protocolVersions`. Und `serverInfo` steht
> **in `_meta`**, nicht auf oberster Ebene. Wer das aus dem Changelog ableitet
> statt aus der Spec, rät falsch — ich habe es getan.
## HTTP: Streamable HTTP
> „The server **MUST** provide a single HTTP endpoint path … that supports POST."
> „Servers **MUST** validate the `Origin` header … If the `Origin` header is
> present and invalid, servers **MUST** respond with HTTP 403 Forbidden."
> „The client **MUST** include an `Accept` header listing both
> `application/json` and `text/event-stream`."
### Pflichtköpfe
> „Every POST request to the MCP endpoint **MUST** include an
> `MCP-Protocol-Version` header."
> „The header value **MUST** match the `io.modelcontextprotocol/protocolVersion`
> field carried in the request body's `_meta`. If the values do not match, the
> server **MUST** reject the request with `400 Bad Request` and a
> `HeaderMismatch` JSON-RPC error."
| Kopf | Quellfeld | Pflicht für |
|---|---|---|
| `Mcp-Method` | `method` | alle Anfragen |
| `Mcp-Name` | `params.name` oder `params.uri` | `tools/call`, `resources/read`, `prompts/get` |
> „These headers are **REQUIRED** for compliance."
> „Servers that process the request body **MUST** reject requests where the
> values specified in the headers do not match the corresponding values in the
> request body."
> „When rejecting a request due to header validation failure, servers **MUST**
> return HTTP status `400 Bad Request` and **MUST** include a JSON-RPC error
> response using … `-32020` `HeaderMismatch`."
Auslösefälle, wörtlich:
> „A required standard header (`MCP-Protocol-Version`, `Mcp-Method`, `Mcp-Name`)
> is missing. · A header value does not match the corresponding request body
> value. · A header value contains invalid characters."
> „Header names … are case-insensitive. … Header *values* (such as method names)
> are case-sensitive."
### Statuscodes
> „If the server does not implement the requested RPC method, it **MUST** respond
> with `404 Not Found` and a JSON-RPC error with code `-32601`."
> „If the server does not implement the requested protocol version … it **MUST**
> respond with `400 Bad Request` and an `UnsupportedProtocolVersionError`."
> Notification angenommen → „`202 Accepted` with no body."
### Was ENTFÄLLT
> „Resumable SSE streams via `Last-Event-ID` are not supported."
> „HTTP GET or DELETE to the MCP endpoint: respond with `405 Method Not
> Allowed`. · An `Mcp-Session-Id` header on a request: ignore it, and do not mint
> or echo session IDs. · A `Last-Event-ID` header: ignore it."
> „The server **MUST NOT** send independent JSON-RPC *requests* on this stream."
Aus dem Changelog, SEP-2575: `initialize` / `notifications/initialized`, `ping`,
`logging/setLevel`, `notifications/roots/list_changed` sind **gestrichen**.
Log-Level je Anfrage über `io.modelcontextprotocol/logLevel` in `_meta`; Server
**DÜRFEN NICHT** `notifications/message` senden, wo das Feld fehlte.
## Zwischenspeicherung
> „Require `ttlMs` and `cacheScope` fields on results returned by `tools/list`,
> `prompts/list`, `resources/list`, `resources/read`, and
> `resources/templates/list` via a new `CacheableResult` interface. … `cacheScope`
> (`"public"` or `"private"`) controls whether shared intermediaries may cache
> the response."
> Für `ttlMs` nennt die Spec **keinen Vorgabewert**. Wer einen wählt, wählt ihn
> und begründet ihn — er steht nirgends zum Nachschlagen.
## JSON Schema
> „When a schema does not include a `$schema` field, it defaults to JSON Schema
> 2020-12."
> „Implementations **MUST NOT** automatically dereference `$ref` values that
> resolve to a network URI."
## MRTR — Multi Round-Trip Requests
> Server-initiierte Anfragen (`roots/list`, `sampling/createMessage`,
> `elicitation/create`) sind ersetzt: der Server gibt ein `InputRequiredResult`
> (`resultType: "input_required"`) mit `inputRequests` zurück, der Klient
> **wiederholt die ursprüngliche Anfrage** mit `inputResponses`.
+102
View File
@@ -0,0 +1,102 @@
# Übergabe an @deus — MCP 2026-07-28
Abgelegt 2026-08-09 von noether, auf Anweisung des Product Owners. Danach bin
ich raus.
## Was hier liegt
/root/spec-2026-07-28/
SPEC-AUSZUG.md die normativen Sätze, wörtlich, mit URLs
konformitaet_2026_07_28.py die Testsuite — DER VERTRAG
lmcp.lua versions.lua der Prüfling, Arbeitsstand
server.lua json.lua damit die Suite einen Server starten kann
`lua5.4` und `lua-socket` sind in deinem Container installiert; **du hast kein
Netz**, deshalb liegt die Spec als Datei hier statt als Link.
cd /root/spec-2026-07-28 && python3 konformitaet_2026_07_28.py lmcp.lua
## Der Zählerstand
**11 von 27 Punkten zu, 16 offen.** Das ist die einzige Zahl, die zählt.
| Gruppe | Stand |
|---|---|
| `discover` | 4 von 5 — der fünfte **absichtlich** rot, siehe unten |
| `codes` | zu |
| `method`, `transport` | zu |
| `result` | zu |
| `header` | 5 offen |
| `_meta` | 3 offen |
| `version` | 4 offen |
| `entfernt` | 3 offen |
Reihenfolge, von @foreman begründet: `header` + `_meta` + `version` als **ein
Block**, weil sie denselben Umschlag teilen — einzeln baut man denselben Pfad
dreimal um. `entfernt` zuletzt, weil es die Klienten **dieses Raumes** bricht.
## Der Schlussstein, und warum er rot bleibt
`supportedVersions enthaelt 2026-07-28` ist die einzige Zeile, die eine Aussage
über das **Ganze** macht. `"2026-07-28"` in `versions.lua` einzutragen macht sie
grün — und den Server zu einem, der behauptet, die Fassung zu sprechen, während
16 Punkte offen sind. Die Liste entscheidet zudem, was der Server **annimmt**:
ein Klient, der `2026-07-28` schickt, würde bedient von einem Server, der sie
nicht umsetzt.
Sie geht als **letzte** auf grün. Solange sie rot ist, sagt die Suite die
Wahrheit über den Server. @foreman hat das bestätigt (Raum #625).
## Was gerade läuft
@coder hat einen Auftrag offen (Raum #628): eine eigenständige Datei
`umschlag.lua` mit einer reinen Funktion
M.pruefe(kopf, rumpf) -> ok, fehlercode, meldung
für die Kopf- und `_meta`-Prüfung, samt Selbsttest. Sein erster Lauf endete mit
einem Lua-Syntaxfehler (Raum #630). Der Auftrag ist bewusst **für seine
Schranke** geschnitten: er schreibt neue Dateien und führt sie aus, er editiert
nichts. Die Einpassung in `lmcp.lua` ist Operator-Arbeit — es gibt im Roster
keine Rolle, die eine Lua-Implementierungsdatei ändern darf.
## Fallen, die mich heute Zeit gekostet haben
**Die Spec, nicht das Changelog.** Ich habe zwei Feldnamen aus einem Delta
zweiter Hand geraten und beide falsch gehabt: es heißt `supportedVersions`
(nicht `protocolVersions`), und `serverInfo` steht **in `_meta`**.
**`json.empty_object` ist ein Singleton** (`json.lua:235`), und der Kodierer
erkennt es an **Identität** (`json.lua:63`). Zwei Aufrufstellen reichen es als
Ergebnis durch. Wer hineinschreibt, vergiftet es prozessweit — und das Feld wird
trotzdem verschluckt. Deshalb baut `jsonrpc_result` eine **Kopie**.
**Ein Fehler ist kein Strom.** Die Spec verlangt vom Klienten ein `Accept` mit
`text/event-stream`. Der SSE-Zweig verdrahtete `200`, also war meine
Statuszuordnung für genau die vorgeschriebene Anfrageform **unerreichbar**
korrekt und wirkungslos. Fehler nehmen jetzt den JSON-Pfad mit ihrem Status.
**Die Statuszuordnung ist vorbereitet.** `-32020`, `-32021`, `-32022`, `-32602`
stehen schon in `_fehler_status` und liefern ihre `400` automatisch, sobald der
Umschlag gebaut ist. Da muss niemand mehr ansetzen.
## Was NICHT im Auftrag ist
Rückwärtskompatibilität. Product Owner, wörtlich: *„2026-07-28 implementieren
kommt zuerst. Dual-Protokoll-Unterstützung kann dann als zweites passieren …
dafür ist dann nachher Integrationsmanagement zuständig."*
## Was ich falsch gemacht habe, damit du es nicht wiederholst
Vierundzwanzig Stunden lang gab es **kein Artefakt, das nein sagen konnte**.
Delta, Entwurf, Reviews — nichts davon konnte rot sein, und ich habe daran
gearbeitet, weil es sich nach Fortschritt anfühlte. Null Zeilen Spec in 24
Stunden; elf Punkte in der ersten Stunde nach der Suite.
Dazu habe ich ein **eigenes Phasenschema** erfunden („Phase C") und darunter
ausschließlich Phase-0-bis-5-Arbeit abgelegt, während der Prozess in
`docs/PROZESS.md` nummerierte Phasen hat und Phase 6 die Umsetzung ist. Die
Regel dagegen steht jetzt dort (Commit `ec01827`): **Fortschritt zählt erst ab
Phase 6**, und das Ziel umzudeuten ist eine Rückfrage, keine Entscheidung.
Halte dich an den Zähler. Er lügt nicht.
+234
View File
@@ -0,0 +1,234 @@
#!/usr/bin/env python3
"""Conformance suite for MCP 2026-07-28, cut from the specification itself.
Sources fetched 2026-08-09 and quoted per check:
/specification/2026-07-28/ (index)
/specification/2026-07-28/basic (messages, _meta, error codes)
/specification/2026-07-28/basic/versioning (version negotiation)
/specification/2026-07-28/server/discover (DiscoverResult shape)
/specification/2026-07-28/basic/transports/streamable-http (headers, statuses)
NOT derived from the changelog or from any second-hand delta: two field names in
the earlier draft were guessed wrong (`protocolVersions` instead of
`supportedVersions`, `serverInfo` at top level instead of inside `_meta`).
The spec normalises BOTH the JSON-RPC error code and the HTTP status, so both
are checked. A check that only looks at the body would pass a server that
answers 200 where the spec demands 400.
Expected result on an unmigrated server: almost everything OFFEN. That is the
measurement the distance to the target not a failure of the suite.
python3 konf2026.py <pfad/zu/lmcp.lua>
"""
import json
import os
import socket
import subprocess
import sys
import time
import urllib.error
import urllib.request
ZIEL = "2026-07-28"
KLON = (os.path.dirname(os.path.abspath(sys.argv[1])) if len(sys.argv) > 1
else "/home/mfritsche/src/lmcp-work")
_offen = 0
def pruefe(punkt, was, ok, beleg=None, bekommen=None):
global _offen
if not ok:
_offen += 1
print(f" [{'ok ' if ok else 'OFFEN '}] {punkt:<7} {was}")
if not ok and bekommen is not None:
print(f" bekommen: {bekommen}")
if not ok and beleg:
print(f" Spec: {beleg}")
META = {"io.modelcontextprotocol/protocolVersion": ZIEL,
"io.modelcontextprotocol/clientCapabilities": {},
"io.modelcontextprotocol/clientInfo": {"name": "konf", "version": "1"}}
def freier_port():
s = socket.socket(); s.bind(("127.0.0.1", 0)); p = s.getsockname()[1]; s.close(); return p
def ruf(port, token, methode, params=None, kopf=None, meta=META, art="POST"):
"""Gibt (jsonrpc-objekt, http-status) zurueck. Beides zaehlt."""
p = dict(params or {})
if meta is not None:
p["_meta"] = meta
rumpf = {"jsonrpc": "2.0", "id": 1, "method": methode}
if p:
rumpf["params"] = p
k = {"Content-Type": "application/json",
"Accept": "application/json, text/event-stream",
"Authorization": "Bearer " + token,
"MCP-Protocol-Version": ZIEL,
"Mcp-Method": methode}
if kopf is not None:
for name, wert in kopf.items():
if wert is None:
k.pop(name, None)
else:
k[name] = wert
req = urllib.request.Request(f"http://127.0.0.1:{port}/mcp",
json.dumps(rumpf).encode(), k, method=art)
try:
a = urllib.request.urlopen(req, timeout=20)
roh, status = a.read().decode(), a.status
except urllib.error.HTTPError as e:
roh, status = e.read().decode(), e.code
except Exception as e:
return {"_transport": f"{type(e).__name__}: {e}"}, 0
for z in roh.splitlines():
if z.startswith("data: "):
roh = z[6:]; break
try:
return json.loads(roh), status
except Exception:
return {"_roh": roh[:160]}, status
def code(a):
return (a.get("error") or {}).get("code")
port, token = freier_port(), "konf-2026-07-28"
umg = dict(os.environ, LMCP_PORT=str(port), LMCP_TOKEN=token, LMCP_HOST="127.0.0.1",
LUA_PATH=f"{KLON}/?.lua;;")
srv = subprocess.Popen(["lua5.4", f"{KLON}/server.lua"], cwd=KLON, env=umg,
stdout=subprocess.DEVNULL, stderr=subprocess.PIPE)
try:
for _ in range(60):
time.sleep(0.5)
try:
socket.create_connection(("127.0.0.1", port), 1).close(); break
except OSError:
if srv.poll() is not None:
print(" Server startete nicht:", (srv.stderr.read() or b"").decode()[:300])
sys.exit(2)
else:
print(" Server kam nicht hoch"); sys.exit(2)
print(f" MCP {ZIEL} — Konformitaet von {KLON}\n")
# ---- server/discover ---------------------------------------------------
a, st = ruf(port, token, "server/discover")
r = a.get("result") or {}
pruefe("discover", "server/discover wird beantwortet", "result" in a,
"server/discover: „Servers MUST implement it.\"", json.dumps(a)[:130])
pruefe("discover", "result.supportedVersions ist eine nicht-leere Liste",
isinstance(r.get("supportedVersions"), list) and bool(r.get("supportedVersions")),
"DiscoverResult: supportedVersions", repr(r.get("supportedVersions")))
pruefe("discover", "result.capabilities ist ein Objekt",
isinstance(r.get("capabilities"), dict), None, repr(r.get("capabilities")))
pruefe("discover", "_meta traegt io.modelcontextprotocol/serverInfo",
isinstance((r.get("_meta") or {}).get("io.modelcontextprotocol/serverInfo"), dict),
"„Servers SHOULD include this field.\"", repr((r.get("_meta") or {}))[:90])
pruefe("discover", f"supportedVersions enthaelt {ZIEL}",
ZIEL in (r.get("supportedVersions") or []), None,
repr(r.get("supportedVersions")))
# ---- resultType auf JEDEM Ergebnis -------------------------------------
for m in ("server/discover", "tools/list"):
a, _ = ruf(port, token, m)
rt = (a.get("result") or {}).get("resultType")
pruefe("result", f"{m}: result.resultType == \"complete\"", rt == "complete",
"„The result MUST include a resultType field.\"", repr(rt))
# ---- _meta ist Pflicht --------------------------------------------------
a, st = ruf(port, token, "tools/list", meta=None)
pruefe("_meta", "fehlendes _meta -> -32602", code(a) == -32602,
"„A request missing any required field is malformed; the server MUST "
"reject it with -32602.\"", f"code={code(a)}")
pruefe("_meta", "fehlendes _meta -> HTTP 400", st == 400,
"„On HTTP, the response status MUST be 400 Bad Request.\"", f"HTTP {st}")
ohne_caps = {k: v for k, v in META.items()
if k != "io.modelcontextprotocol/clientCapabilities"}
a, st = ruf(port, token, "tools/list", meta=ohne_caps)
pruefe("_meta", "fehlende clientCapabilities -> -32602", code(a) == -32602,
"clientCapabilities: Required = Yes", f"code={code(a)}")
# ---- Fassungsaushandlung ------------------------------------------------
schlecht = dict(META, **{"io.modelcontextprotocol/protocolVersion": "1900-01-01"})
a, st = ruf(port, token, "tools/list", meta=schlecht,
kopf={"MCP-Protocol-Version": "1900-01-01"})
pruefe("version", "unbekannte Fassung -> -32022", code(a) == -32022,
"„it MUST respond with an UnsupportedProtocolVersionError\"", f"code={code(a)}")
pruefe("version", "unbekannte Fassung -> HTTP 400", st == 400,
"„MUST respond with 400 Bad Request and an UnsupportedProtocolVersionError\"",
f"HTTP {st}")
d = (a.get("error") or {}).get("data") or {}
pruefe("version", "Fehler nennt data.supported", isinstance(d.get("supported"), list),
None, repr(d)[:90])
pruefe("version", "Fehler nennt data.requested == angefragte Fassung",
d.get("requested") == "1900-01-01",
"Beispiel der Spec: data.requested: \"1900-01-01\"", repr(d.get("requested")))
# ---- Koepfe -------------------------------------------------------------
a, st = ruf(port, token, "tools/list", kopf={"Mcp-Method": None})
pruefe("header", "fehlender Mcp-Method -> -32020", code(a) == -32020,
"„A required standard header … is missing.\" -> HeaderMismatch", f"code={code(a)}")
pruefe("header", "fehlender Mcp-Method -> HTTP 400", st == 400,
"„servers MUST return HTTP status 400 Bad Request\"", f"HTTP {st}")
a, st = ruf(port, token, "tools/list", kopf={"Mcp-Method": "prompts/list"})
pruefe("header", "Mcp-Method != method -> -32020", code(a) == -32020,
"„values specified in the headers do not match … MUST reject\"", f"code={code(a)}")
a, st = ruf(port, token, "tools/list", kopf={"MCP-Protocol-Version": None})
pruefe("header", "fehlender MCP-Protocol-Version -> -32020", code(a) == -32020,
"„Every POST request … MUST include an MCP-Protocol-Version header.\"",
f"code={code(a)}")
a, st = ruf(port, token, "tools/list", kopf={"MCP-Protocol-Version": "2025-06-18"})
pruefe("header", "Kopf-Fassung != _meta-Fassung -> -32020", code(a) == -32020,
"„If the values do not match, the server MUST reject … HeaderMismatch\"",
f"code={code(a)}")
# ---- unbekannte Methode -------------------------------------------------
a, st = ruf(port, token, "gibt/esnicht")
pruefe("method", "unbekannte Methode -> -32601", code(a) == -32601, None, f"code={code(a)}")
pruefe("method", "unbekannte Methode -> HTTP 404", st == 404,
"„it MUST respond with 404 Not Found and a JSON-RPC error with code -32601\"",
f"HTTP {st}")
# ---- gestrichene Methoden ----------------------------------------------
for m in ("initialize", "ping", "logging/setLevel"):
a, _ = ruf(port, token, m)
pruefe("entfernt", f"{m} existiert nicht mehr (-32601)", code(a) == -32601,
"SEP-2575: entfernt", f"code={code(a)}")
# ---- verbotene Fehlercodes ---------------------------------------------
a, _ = ruf(port, token, "resources/read", {"uri": "file:///gibtsnicht"},
kopf={"Mcp-Name": "file:///gibtsnicht"})
pruefe("codes", "unbekannte Ressource -> -32602, NICHT -32002", code(a) == -32602,
"„Implementations of this protocol version MUST NOT emit … -32002\"",
f"code={code(a)}")
# ---- GET/DELETE auf dem Endpunkt ---------------------------------------
for art in ("GET", "DELETE"):
try:
req = urllib.request.Request(f"http://127.0.0.1:{port}/mcp", method=art,
headers={"Authorization": "Bearer " + token})
st = urllib.request.urlopen(req, timeout=10).status
except urllib.error.HTTPError as e:
st = e.code
except Exception:
st = 0
pruefe("transport", f"HTTP {art} auf dem Endpunkt -> 405", st == 405,
"„respond with 405 Method Not Allowed\"", f"HTTP {st}")
print()
print(f" {_offen} Punkte OFFEN")
sys.exit(1 if _offen else 0)
finally:
srv.terminate()
try:
srv.wait(5)
except Exception:
srv.kill()
File diff suppressed because it is too large Load Diff
+67 -6
View File
@@ -174,22 +174,39 @@ local function run(cmd, timeout_sec)
end
return output and output ~= "" and output or "(no output)"
else
-- POSIX: use shell backgrounding + wait with timeout
-- sh -c '(cmd > out 2>&1; echo $? > done) &' then poll
-- POSIX: run in its OWN session/process group (setsid) so a
-- timeout or cancel can kill the WHOLE tree instead of orphaning
-- backgrounded children (the classic "shell timed out, children
-- kept thrashing" bug). $! is the setsid leader pid == pgid.
local pid_file = base .. ".pid"
local sh_cmd = string.format(
"(%s) > '%s' 2>&1; echo $? > '%s'",
cmd, out_file, done_file
)
os.execute("sh -c '" .. sh_cmd:gsub("'", "'\\''") .. "' &")
os.execute("setsid sh -c '" .. sh_cmd:gsub("'", "'\\''")
.. "' & echo $! > '" .. pid_file .. "'")
local pgid = (read_file(pid_file) or ""):match("(%d+)")
remove_silent(pid_file)
local completed = poll_loop()
-- Timeout or cancel -> kill the entire process group. No orphans.
if not completed and pgid then
os.execute("kill -TERM -" .. pgid .. " 2>/dev/null")
sleep_ms(300)
os.execute("kill -KILL -" .. pgid .. " 2>/dev/null")
end
local output = read_file(out_file)
remove_silent(out_file)
remove_silent(done_file)
if not completed then
if cancelled then return "(cancelled)" end
return output or ("Error: command timed out after " .. timeout_sec .. "s")
if cancelled then return "(cancelled -- process group killed)" end
return (output and output ~= "" and (output .. "\n") or "")
.. "Error: command timed out after " .. timeout_sec
.. "s -- the process group was KILLED (nothing is still running). "
.. "For a long-running command, re-run it with shell_bg."
end
return output and output ~= "" and output or "(no output)"
end
@@ -283,7 +300,15 @@ server:tool("shell_bg",
f:close()
os.remove(pid_file)
end
return string.format("launched pid=%s log=%s", pid, log)
-- register so list_jobs/kill_job can see and reap it (no more reboots)
if pid ~= "?" then
local reg = io.open("/tmp/lmcp-bg-jobs.tsv", "a")
if reg then
reg:write(pid.."\t"..log.."\t"..os.date("%Y-%m-%dT%H:%M:%S").."\t"..inner:gsub("[\t\n]"," ").."\n")
reg:close()
end
end
return string.format("launched pid=%s log=%s (kill with kill_job pid=%s)", pid, log, pid)
end, {
annotations = {
title = "Run shell (background)",
@@ -294,6 +319,42 @@ server:tool("shell_bg",
},
})
server:tool("kill_job",
"Kill a runaway background job by PID. SIGKILLs the whole process group of a shell_bg/setsid job so no children survive. Use when a background job is thrashing a machine.",
{ type = "object", properties = { pid = { type = "integer", description = "PID from shell_bg / list_jobs" } }, required = { "pid" } },
function(a)
if WINDOWS then return "Error: kill_job is Linux-only" end
local pid = tostring(a.pid or ""):match("(%d+)")
if not pid then return "Error: numeric pid required" end
os.execute("kill -KILL -"..pid.." 2>/dev/null; kill -KILL "..pid.." 2>/dev/null")
sleep_ms(200)
local alive = os.execute("kill -0 "..pid.." 2>/dev/null")
if alive == true or alive == 0 then return "pid "..pid.." may still be alive (uninterruptible?)" end
return "killed pid "..pid.." (process group)"
end,
{ annotations = { title = "Kill background job", destructiveHint = true } })
server:tool("list_jobs",
"List background jobs started via shell_bg and whether each is still running. Use to find runaway jobs to kill_job.",
{ type = "object", properties = {} },
function()
if WINDOWS then return "Error: list_jobs is Linux-only" end
local reg = io.open("/tmp/lmcp-bg-jobs.tsv", "r")
if not reg then return "(no background jobs recorded)" end
local out = {}
for line in reg:lines() do
local pid, log, ts, cmd = line:match("^(%d+)\t([^\t]*)\t([^\t]*)\t(.*)$")
if pid then
local alive = os.execute("kill -0 "..pid.." 2>/dev/null")
local st = (alive == true or alive == 0) and "RUNNING" or "done"
table.insert(out, string.format("pid=%s [%s] %s log=%s\n %s", pid, st, ts, log, (cmd or ""):sub(1,100)))
end
end
reg:close()
return #out>0 and table.concat(out, "\n") or "(no background jobs recorded)"
end,
{ annotations = { title = "List background jobs", readOnlyHint = true } })
server:tool("read_file", "Read a file.", {
type = "object",
properties = { path = { type = "string" } },
+65
View File
@@ -0,0 +1,65 @@
-- umschlag.lua: Kopf-/Envelope-Pruefung nach Spec 2026-07-28
-- Basis: job667.lua (coder Lauf 5, #669) — parst, reine Struktur, zwei
-- Defekte per Foreman-Review #671/#674 hier korrigiert:
-- Regel 3: mcp-name-KOPF wird geprueft (war nur params.name/uri)
-- Regel 4: _meta liegt unter rumpf.params._meta (war rumpf.meta)
local M = {}
-- kopf: Tabelle der HTTP-Kopfzeilen, Namen KLEINGESCHRIEBEN, z.B.
-- {['mcp-method']='tools/list',['mcp-protocol-version']='2026-07-28'}
-- rumpf: dekodierte JSON-RPC-Anfrage als Lua-Tabelle
-- {jsonrpc='2.0',id=1,method='tools/list',params={_meta={...}}}
-- Rueckgabe: true,nil,nil | false,<fehlercode>,<meldung>
function M.pruefe(kopf, rumpf)
if not kopf or not rumpf then
return false, -32600, "Invalid Request"
end
-- Regel 1: k['mcp-protocol-version'] fehlt
if not kopf['mcp-protocol-version'] then
return false, -32020, 'Missing required header: MCP-Protocol-Version'
end
-- Regel 2: k['mcp-method'] fehlt
if not kopf['mcp-method'] then
return false, -32020, 'Missing required header: Mcp-Method'
end
-- Regel 2: k['mcp-method'] ~= rumpf.method
if kopf['mcp-method'] ~= rumpf.method then
return false, -32020, 'Mcp-Method header does not match body'
end
-- Regel 3: Mcp-Name-Kopf Pflicht bei tools/call, resources/read, prompts/get
local method = rumpf.method
local params = rumpf.params or {}
if method == 'tools/call' or method == 'prompts/get' then
if not kopf['mcp-name'] or kopf['mcp-name'] ~= params.name then
return false, -32020, 'Mcp-Name header does not match body'
end
elseif method == 'resources/read' then
if not kopf['mcp-name'] or kopf['mcp-name'] ~= params.uri then
return false, -32020, 'Mcp-Name header does not match body'
end
end
-- Regel 4: _meta-Pflichtfelder (liegen unter params._meta)
local meta = (rumpf.params or {})._meta or {}
if not meta['io.modelcontextprotocol/protocolVersion'] then
return false, -32602, 'Missing required _meta field: io.modelcontextprotocol/protocolVersion'
end
if not meta['io.modelcontextprotocol/clientCapabilities'] then
return false, -32602, 'Missing required _meta field: io.modelcontextprotocol/clientCapabilities'
end
-- Regel 5: k['mcp-protocol-version'] ~= meta['io.modelcontextprotocol/protocolVersion']
if kopf['mcp-protocol-version'] ~= meta['io.modelcontextprotocol/protocolVersion'] then
return false, -32020, 'Header MCP-Protocol-Version does not match _meta protocolVersion'
end
-- Erfolg
return true, nil, nil
end
return M
+55
View File
@@ -0,0 +1,55 @@
-- versions.lua
-- Modul zur Versionspruefung gemaess LMCP-Vertrag
local M = {}
-- Nur die neue Fassung. Die alte (2025-06-18) existiert nicht mehr: mit
-- SEP-2575 ist `initialize` entfernt, die Verhandlung der alten Fassung
-- damit tot. 2025-06-18 in der Liste zu fuehren waere eine Luege im
-- discover-Response. Dual-Protokoll (alt + neu) ist eine spaetere Phase.
M.SUPPORTED = {"2026-07-28"}
--- Prueft, ob eine Version unterstuetzt wird.
-- @param version Die zu pruefende Version (String oder nil)
-- @return boolean true wenn unterstuetzt, sonst false
-- @return table|nil Fehlerdetails bei Nichtunterstuetzung
function M.check(version)
-- (a) version == nil oder "" -> true, nil
if version == nil or version == "" then
return true, nil
end
-- (b) exakter Treffer in M.SUPPORTED -> true, nil
for _, supported_version in ipairs(M.SUPPORTED) do
if version == supported_version then
return true, nil
end
end
-- (c) alles andere -> false, { code = -32022, data = { supported = <Kopie> } }
--
-- KOPIE, nicht M.SUPPORTED selbst. Vorher ging die Modultabelle nach aussen:
-- ein Aufrufer, der das Fehlerobjekt behaelt und `err.data.supported[1]`
-- ueberschreibt, veraendert die Liste des Moduls fuer JEDEN folgenden
-- Aufruf -- vorgefuehrt am 2026-08-09: nach
-- `e.data.supported[1] = "GEKAPERT"` ist `check("2025-06-18")` falsch und
-- `check("GEKAPERT")` wahr. Vertragsregel 5 ("Ein Aufruf darf die Liste des
-- naechsten nicht verschieben") war damit gebrochen, und Pruefung 5 sah es
-- nicht, weil sie nie ueber die zurueckgegebene Referenz schreibt.
-- Solange das Modul niemand aufrief, war das theoretisch. Seit es an
-- `initialize` haengt, ist es erreichbar.
local kopie = {}
for i = 1, #M.SUPPORTED do
kopie[i] = M.SUPPORTED[i]
end
return false, {
code = -32022,
data = {
supported = kopie
}
}
end
-- Keine neuen Globalen, keine Seiteneffekte, M.SUPPORTED wird nicht geaendert
return M
+54
View File
@@ -0,0 +1,54 @@
local M = {}
function M.check(header, body)
if not header or not body then
return false, -32600, "Invalid Request"
end
-- Rule 1: header['mcp-protocol-version'] missing
if not header['mcp-protocol-version'] then
return false, -32020, 'Missing required header: MCP-Protocol-Version'
end
-- Rule 2: header['mcp-method'] missing
if not header['mcp-method'] then
return false, -32020, 'Missing required header: Mcp-Method'
end
-- Rule 2: header['mcp-method'] ~= body.method
if header['mcp-method'] ~= body.method then
return false, -32020, 'Mcp-Method header does not match body'
end
-- Rule 3: Mcp-Name header required for tools/call, resources/read, prompts/get
local method = body.method
local params = body.params or {}
if method == 'tools/call' or method == 'prompts/get' then
if not header['mcp-name'] or header['mcp-name'] ~= params.name then
return false, -32020, 'Mcp-Name header does not match body'
end
elseif method == 'resources/read' then
if not header['mcp-name'] or header['mcp-name'] ~= params.uri then
return false, -32020, 'Mcp-Name header does not match body'
end
end
-- Rule 4: _meta required fields (located under params._meta)
local meta = (body.params or {})._meta or {}
if not meta['io.modelcontextprotocol/protocolVersion'] then
return false, -32602, 'Missing required _meta field: io.modelcontextprotocol/protocolVersion'
end
if not meta['io.modelcontextprotocol/clientCapabilities'] then
return false, -32602, 'Missing required _meta field: io.modelcontextprotocol/clientCapabilities'
end
-- Rule 5: header['mcp-protocol-version'] ~= meta['io.modelcontextprotocol/protocolVersion']
if header['mcp-protocol-version'] ~= meta['io.modelcontextprotocol/protocolVersion'] then
return false, -32020, 'Header MCP-Protocol-Version does not match _meta protocolVersion'
end
-- Success
return true, nil, nil
end
return M
+242
View File
@@ -0,0 +1,242 @@
-- lmcp/json.lua — Minimal JSON encoder/decoder, zero dependencies
-- SPDX-License-Identifier: MIT
local json = {}
-- Encode --
local encode_value
local escape_chars = {
['"'] = '\\"',
['\\'] = '\\\\',
['\b'] = '\\b',
['\f'] = '\\f',
['\n'] = '\\n',
['\r'] = '\\r',
['\t'] = '\\t',
}
local function encode_string(s)
return '"' .. s:gsub('[%z\1-\31"\\]', function(c)
return escape_chars[c] or string.format('\\u%04x', c:byte())
end) .. '"'
end
local function encode_array(t)
local parts = {}
for i = 1, #t do
parts[i] = encode_value(t[i])
end
return '[' .. table.concat(parts, ',') .. ']'
end
local function encode_object(t)
local parts = {}
for k, v in pairs(t) do
if type(k) == 'string' then
parts[#parts + 1] = encode_string(k) .. ':' .. encode_value(v)
end
end
return '{' .. table.concat(parts, ',') .. '}'
end
local function is_array(t)
if type(t) ~= 'table' then return false end
local n = #t
if n == 0 then
-- empty table: check if it has any keys
return next(t) == nil
end
for k in pairs(t) do
if type(k) ~= 'number' or k < 1 or k > n or k ~= math.floor(k) then
return false
end
end
return true
end
encode_value = function(v)
local t = type(v)
if v == nil or v == json.null then
return 'null'
elseif v == json.empty_object then
-- Sentinel for forcing {} (object) instead of [] (array) when
-- the field semantically requires an object but is empty.
-- Without this, every empty Lua table goes through is_array()
-- and emits as [], breaking spec-strict JSON-RPC consumers
-- (e.g. ping result, MUST be {}).
return '{}'
elseif t == 'boolean' then
return v and 'true' or 'false'
elseif t == 'number' then
if v ~= v then return 'null' end -- NaN
if v == math.huge or v == -math.huge then return 'null' end
if v == math.floor(v) and v >= -2^53 and v <= 2^53 then
return string.format('%.0f', v)
end
return tostring(v)
elseif t == 'string' then
return encode_string(v)
elseif t == 'table' then
if is_array(v) then
return encode_array(v)
else
return encode_object(v)
end
else
return 'null'
end
end
function json.encode(v)
return encode_value(v)
end
-- Decode --
local decode_value
local ws_chars = { [' '] = true, ['\t'] = true, ['\n'] = true, ['\r'] = true }
local function skip_ws(s, pos)
while pos <= #s and ws_chars[s:sub(pos, pos)] do
pos = pos + 1
end
return pos
end
local function decode_string(s, pos)
-- pos is at opening quote
pos = pos + 1
local parts = {}
while pos <= #s do
local c = s:sub(pos, pos)
if c == '"' then
return table.concat(parts), pos + 1
elseif c == '\\' then
pos = pos + 1
c = s:sub(pos, pos)
if c == 'u' then
local cp = tonumber(s:sub(pos + 1, pos + 4), 16)
pos = pos + 5
-- Combine UTF-16 surrogate pair so non-BMP chars (emoji,
-- supplementary CJK) decode correctly instead of as two
-- lone surrogates → invalid UTF-8.
if cp and cp >= 0xD800 and cp <= 0xDBFF
and s:sub(pos, pos + 1) == "\\u" then
local lo = tonumber(s:sub(pos + 2, pos + 5), 16)
if lo and lo >= 0xDC00 and lo <= 0xDFFF then
cp = (cp - 0xD800) * 0x400 + (lo - 0xDC00) + 0x10000
pos = pos + 6
end
end
parts[#parts + 1] = utf8.char(cp)
else
local esc = { n = '\n', r = '\r', t = '\t', b = '\b', f = '\f' }
parts[#parts + 1] = esc[c] or c
pos = pos + 1
end
else
local next_special = s:find('["\\]', pos)
if next_special then
parts[#parts + 1] = s:sub(pos, next_special - 1)
pos = next_special
else
parts[#parts + 1] = s:sub(pos)
break
end
end
end
error('unterminated string')
end
local function decode_number(s, pos)
local start = pos
if s:sub(pos, pos) == '-' then pos = pos + 1 end
while pos <= #s and s:sub(pos, pos):match('[%d%.eE%+%-]') do
pos = pos + 1
end
local n = tonumber(s:sub(start, pos - 1))
if not n then error('invalid number at ' .. start) end
return n, pos
end
local function decode_array(s, pos)
pos = pos + 1 -- skip [
local arr = {}
pos = skip_ws(s, pos)
if s:sub(pos, pos) == ']' then return arr, pos + 1 end
while true do
local val
val, pos = decode_value(s, pos)
arr[#arr + 1] = val
pos = skip_ws(s, pos)
local c = s:sub(pos, pos)
if c == ']' then return arr, pos + 1 end
if c ~= ',' then error('expected , or ] at ' .. pos) end
pos = skip_ws(s, pos + 1)
end
end
local function decode_object(s, pos)
pos = pos + 1 -- skip {
local obj = {}
pos = skip_ws(s, pos)
if s:sub(pos, pos) == '}' then return obj, pos + 1 end
while true do
pos = skip_ws(s, pos)
if s:sub(pos, pos) ~= '"' then error('expected string key at ' .. pos) end
local key
key, pos = decode_string(s, pos)
pos = skip_ws(s, pos)
if s:sub(pos, pos) ~= ':' then error('expected : at ' .. pos) end
pos = skip_ws(s, pos + 1)
local val
val, pos = decode_value(s, pos)
obj[key] = val
pos = skip_ws(s, pos)
local c = s:sub(pos, pos)
if c == '}' then return obj, pos + 1 end
if c ~= ',' then error('expected , or } at ' .. pos) end
pos = pos + 1
end
end
decode_value = function(s, pos)
pos = skip_ws(s, pos)
local c = s:sub(pos, pos)
if c == '"' then return decode_string(s, pos)
elseif c == '{' then return decode_object(s, pos)
elseif c == '[' then return decode_array(s, pos)
elseif c == 't' then
if s:sub(pos, pos + 3) == 'true' then return true, pos + 4 end
elseif c == 'f' then
if s:sub(pos, pos + 4) == 'false' then return false, pos + 5 end
elseif c == 'n' then
if s:sub(pos, pos + 3) == 'null' then return json.null, pos + 4 end
elseif c == '-' or c:match('%d') then
return decode_number(s, pos)
end
error('unexpected character at ' .. pos .. ': ' .. c)
end
function json.decode(s)
local val, pos = decode_value(s, 1)
return val
end
-- Sentinel for JSON null
json.null = setmetatable({}, { __tostring = function() return 'null' end })
-- Sentinel for an empty JSON object ({}). Use when a field semantically
-- requires an object but is empty — e.g. `ping` result, MCP _meta = {}.
-- Without this, an empty Lua table goes through is_array() → '[]'.
-- See memory project_json_empty_table_gotcha.md.
json.empty_object = setmetatable({}, { __tostring = function() return '{}' end })
-- Helper: encode a table as a JSON array even if empty
function json.array(t)
return setmetatable(t or {}, { __is_array = true })
end
return json
+234
View File
@@ -0,0 +1,234 @@
#!/usr/bin/env python3
"""Conformance suite for MCP 2026-07-28, cut from the specification itself.
Sources fetched 2026-08-09 and quoted per check:
/specification/2026-07-28/ (index)
/specification/2026-07-28/basic (messages, _meta, error codes)
/specification/2026-07-28/basic/versioning (version negotiation)
/specification/2026-07-28/server/discover (DiscoverResult shape)
/specification/2026-07-28/basic/transports/streamable-http (headers, statuses)
NOT derived from the changelog or from any second-hand delta: two field names in
the earlier draft were guessed wrong (`protocolVersions` instead of
`supportedVersions`, `serverInfo` at top level instead of inside `_meta`).
The spec normalises BOTH the JSON-RPC error code and the HTTP status, so both
are checked. A check that only looks at the body would pass a server that
answers 200 where the spec demands 400.
Expected result on an unmigrated server: almost everything OFFEN. That is the
measurement the distance to the target not a failure of the suite.
python3 konf2026.py <pfad/zu/lmcp.lua>
"""
import json
import os
import socket
import subprocess
import sys
import time
import urllib.error
import urllib.request
ZIEL = "2026-07-28"
KLON = (os.path.dirname(os.path.abspath(sys.argv[1])) if len(sys.argv) > 1
else "/home/mfritsche/src/lmcp-work")
_offen = 0
def pruefe(punkt, was, ok, beleg=None, bekommen=None):
global _offen
if not ok:
_offen += 1
print(f" [{'ok ' if ok else 'OFFEN '}] {punkt:<7} {was}")
if not ok and bekommen is not None:
print(f" bekommen: {bekommen}")
if not ok and beleg:
print(f" Spec: {beleg}")
META = {"io.modelcontextprotocol/protocolVersion": ZIEL,
"io.modelcontextprotocol/clientCapabilities": {},
"io.modelcontextprotocol/clientInfo": {"name": "konf", "version": "1"}}
def freier_port():
s = socket.socket(); s.bind(("127.0.0.1", 0)); p = s.getsockname()[1]; s.close(); return p
def ruf(port, token, methode, params=None, kopf=None, meta=META, art="POST"):
"""Gibt (jsonrpc-objekt, http-status) zurueck. Beides zaehlt."""
p = dict(params or {})
if meta is not None:
p["_meta"] = meta
rumpf = {"jsonrpc": "2.0", "id": 1, "method": methode}
if p:
rumpf["params"] = p
k = {"Content-Type": "application/json",
"Accept": "application/json, text/event-stream",
"Authorization": "Bearer " + token,
"MCP-Protocol-Version": ZIEL,
"Mcp-Method": methode}
if kopf is not None:
for name, wert in kopf.items():
if wert is None:
k.pop(name, None)
else:
k[name] = wert
req = urllib.request.Request(f"http://127.0.0.1:{port}/mcp",
json.dumps(rumpf).encode(), k, method=art)
try:
a = urllib.request.urlopen(req, timeout=20)
roh, status = a.read().decode(), a.status
except urllib.error.HTTPError as e:
roh, status = e.read().decode(), e.code
except Exception as e:
return {"_transport": f"{type(e).__name__}: {e}"}, 0
for z in roh.splitlines():
if z.startswith("data: "):
roh = z[6:]; break
try:
return json.loads(roh), status
except Exception:
return {"_roh": roh[:160]}, status
def code(a):
return (a.get("error") or {}).get("code")
port, token = freier_port(), "konf-2026-07-28"
umg = dict(os.environ, LMCP_PORT=str(port), LMCP_TOKEN=token, LMCP_HOST="127.0.0.1",
LUA_PATH=f"{KLON}/?.lua;;")
srv = subprocess.Popen(["lua5.4", f"{KLON}/server.lua"], cwd=KLON, env=umg,
stdout=subprocess.DEVNULL, stderr=subprocess.PIPE)
try:
for _ in range(60):
time.sleep(0.5)
try:
socket.create_connection(("127.0.0.1", port), 1).close(); break
except OSError:
if srv.poll() is not None:
print(" Server startete nicht:", (srv.stderr.read() or b"").decode()[:300])
sys.exit(2)
else:
print(" Server kam nicht hoch"); sys.exit(2)
print(f" MCP {ZIEL} — Konformitaet von {KLON}\n")
# ---- server/discover ---------------------------------------------------
a, st = ruf(port, token, "server/discover")
r = a.get("result") or {}
pruefe("discover", "server/discover wird beantwortet", "result" in a,
"server/discover: „Servers MUST implement it.\"", json.dumps(a)[:130])
pruefe("discover", "result.supportedVersions ist eine nicht-leere Liste",
isinstance(r.get("supportedVersions"), list) and bool(r.get("supportedVersions")),
"DiscoverResult: supportedVersions", repr(r.get("supportedVersions")))
pruefe("discover", "result.capabilities ist ein Objekt",
isinstance(r.get("capabilities"), dict), None, repr(r.get("capabilities")))
pruefe("discover", "_meta traegt io.modelcontextprotocol/serverInfo",
isinstance((r.get("_meta") or {}).get("io.modelcontextprotocol/serverInfo"), dict),
"„Servers SHOULD include this field.\"", repr((r.get("_meta") or {}))[:90])
pruefe("discover", f"supportedVersions enthaelt {ZIEL}",
ZIEL in (r.get("supportedVersions") or []), None,
repr(r.get("supportedVersions")))
# ---- resultType auf JEDEM Ergebnis -------------------------------------
for m in ("server/discover", "tools/list"):
a, _ = ruf(port, token, m)
rt = (a.get("result") or {}).get("resultType")
pruefe("result", f"{m}: result.resultType == \"complete\"", rt == "complete",
"„The result MUST include a resultType field.\"", repr(rt))
# ---- _meta ist Pflicht --------------------------------------------------
a, st = ruf(port, token, "tools/list", meta=None)
pruefe("_meta", "fehlendes _meta -> -32602", code(a) == -32602,
"„A request missing any required field is malformed; the server MUST "
"reject it with -32602.\"", f"code={code(a)}")
pruefe("_meta", "fehlendes _meta -> HTTP 400", st == 400,
"„On HTTP, the response status MUST be 400 Bad Request.\"", f"HTTP {st}")
ohne_caps = {k: v for k, v in META.items()
if k != "io.modelcontextprotocol/clientCapabilities"}
a, st = ruf(port, token, "tools/list", meta=ohne_caps)
pruefe("_meta", "fehlende clientCapabilities -> -32602", code(a) == -32602,
"clientCapabilities: Required = Yes", f"code={code(a)}")
# ---- Fassungsaushandlung ------------------------------------------------
schlecht = dict(META, **{"io.modelcontextprotocol/protocolVersion": "1900-01-01"})
a, st = ruf(port, token, "tools/list", meta=schlecht,
kopf={"MCP-Protocol-Version": "1900-01-01"})
pruefe("version", "unbekannte Fassung -> -32022", code(a) == -32022,
"„it MUST respond with an UnsupportedProtocolVersionError\"", f"code={code(a)}")
pruefe("version", "unbekannte Fassung -> HTTP 400", st == 400,
"„MUST respond with 400 Bad Request and an UnsupportedProtocolVersionError\"",
f"HTTP {st}")
d = (a.get("error") or {}).get("data") or {}
pruefe("version", "Fehler nennt data.supported", isinstance(d.get("supported"), list),
None, repr(d)[:90])
pruefe("version", "Fehler nennt data.requested == angefragte Fassung",
d.get("requested") == "1900-01-01",
"Beispiel der Spec: data.requested: \"1900-01-01\"", repr(d.get("requested")))
# ---- Koepfe -------------------------------------------------------------
a, st = ruf(port, token, "tools/list", kopf={"Mcp-Method": None})
pruefe("header", "fehlender Mcp-Method -> -32020", code(a) == -32020,
"„A required standard header … is missing.\" -> HeaderMismatch", f"code={code(a)}")
pruefe("header", "fehlender Mcp-Method -> HTTP 400", st == 400,
"„servers MUST return HTTP status 400 Bad Request\"", f"HTTP {st}")
a, st = ruf(port, token, "tools/list", kopf={"Mcp-Method": "prompts/list"})
pruefe("header", "Mcp-Method != method -> -32020", code(a) == -32020,
"„values specified in the headers do not match … MUST reject\"", f"code={code(a)}")
a, st = ruf(port, token, "tools/list", kopf={"MCP-Protocol-Version": None})
pruefe("header", "fehlender MCP-Protocol-Version -> -32020", code(a) == -32020,
"„Every POST request … MUST include an MCP-Protocol-Version header.\"",
f"code={code(a)}")
a, st = ruf(port, token, "tools/list", kopf={"MCP-Protocol-Version": "2025-06-18"})
pruefe("header", "Kopf-Fassung != _meta-Fassung -> -32020", code(a) == -32020,
"„If the values do not match, the server MUST reject … HeaderMismatch\"",
f"code={code(a)}")
# ---- unbekannte Methode -------------------------------------------------
a, st = ruf(port, token, "gibt/esnicht")
pruefe("method", "unbekannte Methode -> -32601", code(a) == -32601, None, f"code={code(a)}")
pruefe("method", "unbekannte Methode -> HTTP 404", st == 404,
"„it MUST respond with 404 Not Found and a JSON-RPC error with code -32601\"",
f"HTTP {st}")
# ---- gestrichene Methoden ----------------------------------------------
for m in ("initialize", "ping", "logging/setLevel"):
a, _ = ruf(port, token, m)
pruefe("entfernt", f"{m} existiert nicht mehr (-32601)", code(a) == -32601,
"SEP-2575: entfernt", f"code={code(a)}")
# ---- verbotene Fehlercodes ---------------------------------------------
a, _ = ruf(port, token, "resources/read", {"uri": "file:///gibtsnicht"},
kopf={"Mcp-Name": "file:///gibtsnicht"})
pruefe("codes", "unbekannte Ressource -> -32602, NICHT -32002", code(a) == -32602,
"„Implementations of this protocol version MUST NOT emit … -32002\"",
f"code={code(a)}")
# ---- GET/DELETE auf dem Endpunkt ---------------------------------------
for art in ("GET", "DELETE"):
try:
req = urllib.request.Request(f"http://127.0.0.1:{port}/mcp", method=art,
headers={"Authorization": "Bearer " + token})
st = urllib.request.urlopen(req, timeout=10).status
except urllib.error.HTTPError as e:
st = e.code
except Exception:
st = 0
pruefe("transport", f"HTTP {art} auf dem Endpunkt -> 405", st == 405,
"„respond with 405 Method Not Allowed\"", f"HTTP {st}")
print()
print(f" {_offen} Punkte OFFEN")
sys.exit(1 if _offen else 0)
finally:
srv.terminate()
try:
srv.wait(5)
except Exception:
srv.kill()
+207 -97
View File
@@ -3,6 +3,16 @@
-- SPDX-License-Identifier: MIT
local json = require('json')
-- The version check. Phase B built it but never wired it up: measured on
-- 2026-08-09, `versions` occurred zero times in this file, and the running
-- server answered `initialize` with "1999-01-01" with HTTP 200 and
-- "2025-06-18" -- verbatim the defect it was written against.
local versions = require('versions')
-- The envelope (envelope check 2026-07-28): required headers (Mcp-Method,
-- MCP-Protocol-Version, Mcp-Name) + required _meta fields are checked before
-- method dispatch. Based on job667.lua (coder run 5), two review fixes
-- (mcp-name header, _meta under params._meta).
local envelope = require('envelope')
local lmcp = {}
lmcp.__index = lmcp
@@ -37,7 +47,12 @@ local function read_conf(path)
end
-- Protocol constants
local MCP_VERSION = "2025-06-18"
-- ONE truth. The version used to live twice in the tree -- here as a
-- constant and in versions.M.SUPPORTED -- and the response line took
-- this one. Changing the list therefore did NOT change what the server
-- answers: measured with {"2025-06-18","2025-03-26"}, a call to
-- initialize(2025-03-26) returned a 200 with "2025-06-18".
local MCP_VERSION = versions.SUPPORTED[1]
local JSONRPC = "2.0"
function lmcp.new(name, opts)
@@ -48,6 +63,19 @@ function lmcp.new(name, opts)
self.host = opts.host or "0.0.0.0"
self.port = opts.port or 8080
self.tools = {}
-- Per-instance allowlist (LMCP_TOOL_ALLOW, comma-separated). When set,
-- `tool()` registers ONLY those names -- built-ins like plugins.
-- When unset: everything as before. This is the only place where a
-- tool is created, hence the only place where it can be prevented;
-- removing one later means every future entry must know and stale out.
self.tool_allow = nil
do
local roh = os.getenv("LMCP_TOOL_ALLOW")
if roh and roh:match("%S") then
self.tool_allow = {}
for n in roh:gmatch("[^,%s]+") do self.tool_allow[n] = true end
end
end
-- Resources primitive (MCP 2025-06-18 §Server/Resources). Storage is
-- always present; capability is advertised iff `opts.resources` is
-- truthy OR at least one resource/template has been registered by
@@ -182,6 +210,13 @@ function lmcp:tool(name, description, params_schema, handler, opts)
end
schema = clean
end
-- Allow-list: silently refuse, so a plugin offering a tool that is not
-- allowed does not crash -- it simply does not exist. `tools/list` and
-- `tools/call` both read the same registry, so an unregistered tool is
-- neither visible nor callable.
if self.tool_allow and not self.tool_allow[name] then
return self
end
self.tools[name] = {
name = name,
description = description,
@@ -433,18 +468,66 @@ function lmcp:log(level, logger, data)
end
-- JSON-RPC response helpers
-- MCP 2026-07-28: "The result MUST include a resultType field to indicate the
-- type of the result." Hence here and not at the fourteen call sites:
-- there should not be a single place that could forget it.
--
-- NEVER mutate the passed object. `json.empty_object` is a singleton
-- (json.lua:235), two callers pass it straight through, and the encoder
-- recognizes it by IDENTITY (json.lua:63) -- a write would have poisoned it
-- process-wide AND the field would still have been swallowed.
local function jsonrpc_result(id, result)
return json.encode({ jsonrpc = JSONRPC, id = id, result = result })
local copy = {}
if type(result) == "table" then
for k, v in pairs(result) do copy[k] = v end
end
-- "input_required" is set by the caller itself (MRTR); everything else
-- is "complete". An existing field is not overwritten.
if copy.resultType == nil then copy.resultType = "complete" end
return json.encode({ jsonrpc = JSONRPC, id = id, result = copy })
end
local function jsonrpc_error(id, code, message)
local function jsonrpc_error(id, code, message, data)
-- `data` is optional and only set when present: all existing callers
-- pass three arguments and do not change.
-- Without this field, `data.supported` of the version check would fall
-- away on the way, and the rejection would be formally correct and
-- practically useless -- the client would not learn which version the
-- server speaks.
local err = { code = code, message = message }
if data ~= nil then err.data = data end
return json.encode({
jsonrpc = JSONRPC,
id = id,
error = { code = code, message = message },
error = err,
})
end
-- What this server can do. ONE computation for `initialize` and
-- `server/discover`. Two places deriving the same thing are the disease
-- this campaign found three times today (TRUST vs. roster,
-- MCP_VERSION vs. M.SUPPORTED, the delta's stage boundary).
function lmcp:_capabilities()
local caps = { tools = { listChanged = false } }
if self._force_resources_cap
or next(self.resources)
or self.resource_templates[1] then
caps.resources = { listChanged = true, subscribe = false }
end
if self._force_prompts_cap or next(self.prompts) then
caps.prompts = { listChanged = true }
end
if self._force_completions_cap or next(self.completions) then
-- Spec uses an empty object as the "supported" marker.
-- json.empty_object → {} (not [] from the empty-table gotcha).
caps.completions = json.empty_object
end
if self._force_logging_cap then
caps.logging = json.empty_object
end
return caps
end
-- Handle a single JSON-RPC request
function lmcp:handle_request(req)
local method = req.method
@@ -483,42 +566,34 @@ function lmcp:handle_request(req)
return nil
end
if method == "initialize" then
self._session_id = self._session_id or tostring(os.time())
-- Capture client capabilities (MCP issue #9 — sampling needs to
-- know if the client supports it before issuing a request).
local p = req.params or {}
self._client_caps = p.capabilities or {}
self._client_info = p.clientInfo or {}
local caps = { tools = { listChanged = false } }
if self._force_resources_cap
or next(self.resources)
or self.resource_templates[1] then
caps.resources = { listChanged = true, subscribe = false }
end
if self._force_prompts_cap or next(self.prompts) then
caps.prompts = { listChanged = true }
end
if self._force_completions_cap or next(self.completions) then
-- Spec uses an empty object as the "supported" marker.
-- json.empty_object → {} (not [] from the empty-table gotcha).
caps.completions = json.empty_object
end
if self._force_logging_cap then
caps.logging = json.empty_object
end
-- MCP 2026-07-28, /server/discover: "Servers MUST implement it."
-- Additive: a client of the old version never calls it. Placed BEFORE
-- `initialize` so it stays reachable should the latter fall away.
if method == "server/discover" then
-- Field names from the spec's example, not from the changelog: it is
-- `supportedVersions` (not protocolVersions), and serverInfo sits IN
-- `_meta`, not at the top. Both were guessed wrong in the draft.
local supported = {}
for i = 1, #versions.SUPPORTED do supported[i] = versions.SUPPORTED[i] end
local caps = self:_capabilities()
return jsonrpc_result(id, {
protocolVersion = MCP_VERSION,
capabilities = caps,
serverInfo = {
name = self.name,
version = self.version,
resultType = "complete",
supportedVersions = supported,
-- An empty Lua table encodes as `[]`, not `{}` -- the spec
-- requires an object. (Finding @testdesigner, room #622.)
capabilities = next(caps) and caps or json.empty_object,
_meta = {
["io.modelcontextprotocol/serverInfo"] = {
name = self.name,
version = self.version,
},
},
})
elseif method == "ping" then
return jsonrpc_result(id, json.empty_object)
-- SEP-2575: `initialize`, `ping` and `logging/setLevel` are gone from
-- spec 2026-07-28 and fall into the default branch here
-- (jsonrpc_error -32601, HTTP 404). Version negotiation moves into
-- the envelope (envelope check, last block).
elseif method == "tools/list" then
local tool_list = {}
for _, t in pairs(self.tools) do
@@ -674,10 +749,15 @@ function lmcp:handle_request(req)
end
local entry, args = _resolve_resource(self, uri)
if not entry then
-- -32002 is the MCP-conventional "Resource not found" code;
-- distinct from -32602 "Invalid params" so retry/UX logic
-- can tell a malformed URI from a missing one.
return jsonrpc_error(id, -32002, "Resource not found: " .. uri)
-- MCP 2026-07-28: "Implementations of this protocol version MUST
-- NOT emit these codes: -32002 — resource not found … replaced by
-- -32602." The earlier objection recorded here (32002 distinguishing
-- a missing from a malformed URI) is thereby settled: the spec has
-- deliberately merged both into -32602, and a receiver "MUST NOT
-- assume any specific meaning" for the old range. The difference
-- lives on in the message text, where it can no longer be confused
-- with a protocol promise.
return jsonrpc_error(id, -32602, "Resource not found: " .. uri)
end
local item, err = _read_resource(entry, args, uri)
if not item then
@@ -705,7 +785,11 @@ function lmcp:handle_request(req)
end
local entry = self.prompts[name]
if not entry then
return jsonrpc_error(id, -32002, "Prompt not found: " .. name)
-- Second occurrence of the same forbidden code. The suite does
-- NOT check it (it only asks resources/read) -- which is why it is
-- listed here: fixing only the tested one would make the counter
-- green and the server non-conformant.
return jsonrpc_error(id, -32602, "Prompt not found: " .. name)
end
local result, err = _get_prompt(entry, params.arguments)
if not result then
@@ -713,15 +797,6 @@ function lmcp:handle_request(req)
end
return jsonrpc_result(id, result)
elseif method == "logging/setLevel" then
local lvl = (req.params or {}).level
if type(lvl) ~= "string" or not LOG_LEVELS[lvl] then
return jsonrpc_error(id, -32602,
"level must be one of: debug, info, notice, warning, error, critical, alert, emergency")
end
self._log_level = lvl
return jsonrpc_result(id, json.empty_object)
elseif method == "completion/complete" then
local params = req.params or {}
local ref = params.ref or {}
@@ -999,6 +1074,36 @@ end
local _drive_handler_co
local _finalise_dispatch
-- MCP 2026-07-28 standardizes the HTTP status, not just the body. This one
-- mapping decides it, so each branch does not choose it itself -- and so the
-- following parts (header, _meta, version) get their 400s without anyone
-- having to add to it again.
--
-- -32601 unknown method -> 404 („MUST respond with 404 Not
-- Found and … -32601")
-- -32020 HeaderMismatch -> 400
-- -32021 MissingRequiredClientCapability -> 400
-- -32022 UnsupportedProtocolVersion -> 400
-- -32602 Invalid params -> 400 (missing required _meta fields)
local _error_status = {
[-32601] = "404 Not Found",
[-32020] = "400 Bad Request",
[-32021] = "400 Bad Request",
[-32022] = "400 Bad Request",
[-32602] = "400 Bad Request",
}
local function _status_for_error_code(encoded)
-- `encoded` is the finished JSON response. Decoding once is cheaper
-- and more honest than searching the string for numbers: a string
-- can contain `-32601` inside a message too.
local ok, obj = pcall(json.decode, encoded)
if not ok or type(obj) ~= "table" or type(obj.error) ~= "table" then
return "200 OK"
end
return _error_status[obj.error.code] or "200 OK"
end
local function _dispatch_post(self, conn)
local body = conn.body
if body == "" then
@@ -1030,6 +1135,37 @@ local function _dispatch_post(self, conn)
end
end
-- Envelope check (2026-07-28): required headers + _meta + version BEFORE
-- method dispatch. Errors carry their JSON-RPC code and their HTTP status
-- from _error_status (-32020/-32602 -> 400).
local e_ok, e_code, e_message = envelope.check(conn.headers, rpc_req)
if not e_ok then
return _build_http_response(
_error_status[e_code] or "400 Bad Request",
{ ["Content-Type"] = "application/json",
["Access-Control-Allow-Origin"] = "*" },
jsonrpc_error(rpc_req.id, e_code, e_message), nil)
end
-- Version check (keystone): `versions.check` rejects unknown versions
-- with -32022 + 400 and names data.supported (a copy of the list,
-- copy-protection unchanged) + data.requested (the requested version).
-- Allows nil/"" (contract rule a) -- the envelope has already rejected
-- missing _meta, so only something carrying a protocolVersion value
-- arrives here.
local requested = ((rpc_req.params or {})._meta or {})
["io.modelcontextprotocol/protocolVersion"]
local allowed, err = versions.check(requested)
if not allowed then
err.data.requested = requested
return _build_http_response(
_error_status[err.code] or "400 Bad Request",
{ ["Content-Type"] = "application/json",
["Access-Control-Allow-Origin"] = "*" },
jsonrpc_error(rpc_req.id, err.code,
"Unsupported protocol version", err.data), nil)
end
-- Session resolution (deferred from header-parse time so we can detect
-- `initialize`). Rules:
-- - `initialize`: always mint a fresh session, ignoring any client sid
@@ -1152,11 +1288,19 @@ _finalise_dispatch = function(self, conn, rok, result, co)
-- Accept-aware response shape (re-checked at finalise time; survives
-- parking because conn.headers is captured by the closure scope).
local accept = conn.headers["accept"] or ""
if accept:find("text/event%-stream") then
-- Errors are NEVER streamed. The spec requires the client to send an
-- Accept header with text/event-stream (streamable-http, "MUST include
-- an Accept header listing both"), so the SSE branch only hit exactly
-- the prescribed request shape -- and _build_sse_headers wires 200.
-- The status mapping was therefore unreachable for the normal case. An
-- error has nothing to stream: no notifications before it, nothing to
-- keep open.
local status = _status_for_error_code(result)
if status == "200 OK" and accept:find("text/event%-stream") then
local hdrs = _build_sse_headers(session_id)
return hdrs .. _sse_event(result)
end
return _build_http_response("200 OK",
return _build_http_response(status,
{ ["Content-Type"] = "application/json",
["Access-Control-Allow-Origin"] = "*" },
result, session_id)
@@ -1268,52 +1412,18 @@ local function _conn_read(self, conn)
if conn.method == "OPTIONS" then
conn.write_buf = _dispatch_options(conn)
conn.state = "writing"
elseif conn.method == "DELETE" then
-- Resolve session: no auto-issue for DELETE; unknown sid → 404.
if not conn.requested_sid then
conn.write_buf = _build_http_response("400 Bad Request",
{ ["Content-Type"] = "text/plain",
["Access-Control-Allow-Origin"] = "*" },
"Mcp-Session-Id required for DELETE", nil)
conn.state = "writing"
return
end
conn.session_id = conn.requested_sid
conn.write_buf = _dispatch_delete(self, conn)
elseif conn.method == "GET" or conn.method == "DELETE" then
-- MCP 2026-07-28: „HTTP GET or DELETE to the MCP endpoint: respond
-- with 405 Method Not Allowed." Both carried the mechanics of the
-- old version — GET opened a separate SSE stream, DELETE ended
-- a session. Spec quote for both: „None of these mechanisms are
-- part of this revision."
conn.write_buf = _build_http_response("405 Method Not Allowed",
{ ["Content-Type"] = "text/plain",
["Allow"] = "POST, OPTIONS",
["Access-Control-Allow-Origin"] = "*" },
"Method Not Allowed", nil)
conn.state = "writing"
elseif conn.method == "GET" then
-- Resolve session (auto-issue if missing, 404 if unknown).
local sess, serr = _resolve_session(self, conn.requested_sid)
if not sess then
conn.write_buf = _build_http_response("404 Not Found",
{ ["Content-Type"] = "text/plain",
["Access-Control-Allow-Origin"] = "*" },
"Session not found: " .. tostring(conn.requested_sid), nil)
conn.state = "writing"
return
end
conn.session_id = sess.id
-- Persistent SSE stream. Enforce one per session.
if sess.sse_conn and sess.sse_conn ~= conn then
conn.write_buf = _build_http_response("409 Conflict",
{ ["Content-Type"] = "text/plain",
["Access-Control-Allow-Origin"] = "*" },
"Session already has an open SSE stream", nil)
conn.state = "writing"
return
end
sess.sse_conn = conn
local hdrs = _build_sse_headers(conn.session_id)
-- For backwards compat with the old SDK probe shape, emit a one-shot
-- 'endpoint' event so clients can self-discover; modern clients
-- ignore it and just await message events.
local endpoint_data = json.encode({
endpoint = "/mcp", sessionId = conn.session_id,
})
conn.write_buf = hdrs ..
"event: endpoint\r\ndata: " .. endpoint_data .. "\r\n\r\n"
conn.state = "sse_open"
conn.last_heart = os.time()
elseif conn.method == "POST" then
-- _dispatch_post may return nil (issue #20) if the handler
-- coroutine yielded. In that case it set conn.state =
File diff suppressed because it is too large Load Diff
+54
View File
@@ -0,0 +1,54 @@
local M = {}
-- versions.lua
-- Module for version checking according to LMCP contract
-- Only the new version. The old one (2025-06-18) no longer exists: with
-- SEP-2575 initialize is removed, the negotiation of the old version
-- is dead. Listing 2025-06-18 in the list would be a lie in the
-- discover response. Dual protocol (old + new) is a later phase.
M.SUPPORTED = {'2026-07-28'}
--- Checks whether a version is supported.
-- @param version The version to check (string or nil)
-- @return boolean true if supported, otherwise false
-- @return table|nil error details if not supported
function M.check(version)
-- (a) version == nil or '' -> true, nil
if version == nil or version == '' then
return true, nil
end
-- (b) exact match in M.SUPPORTED -> true, nil
for _, supported_version in ipairs(M.SUPPORTED) do
if version == supported_version then
return true, nil
end
end
-- (c) everything else -> false, { code = -32022, data = { supported = <copy> } }
--
-- COPY, not M.SUPPORTED itself. Previously the module table leaked out:
-- a caller that keeps the error object and err.data.supported[1]
-- overwrites, changes the module's list for EVERY subsequent
-- call -- demonstrated on 2026-08-09: after
-- e.data.supported[1] = 'CAPTURED' check('2025-06-18') is false and
-- check('CAPTURED') is true. Contract rule 5 ('A call must not shift the list of
-- the next') was thereby broken, and test 5 did not see it
-- because it never writes over the returned reference.
-- As long as no one called the module, this was theoretical. Since it is attached to
-- initialize, it is reachable.
local copy = {}
for i = 1, #M.SUPPORTED do
copy[i] = M.SUPPORTED[i]
end
return false, {
code = -32022,
data = {
supported = copy
}
}
end
-- No new globals, no side effects, M.SUPPORTED is not modified
return M
+35
View File
@@ -0,0 +1,35 @@
# Phase 2 — DUAL-PROTOKOLL: BEIDE Suiten GRUEN (2026-08-10)
Implementierung vollstaendig (STEP 2/PAKET 1 + Operator-Integration):
## Bausteine in der Arbeitskopie /home/mfritsche/spec-2026-07-28/
- **versions.lua** (sha 060ad66e…) — @coder-Lieferung job1158.lua (Korrektur-Pass;
erster Wurf job1149.lua fiel wegen verbotenem Self-Test/print durch die Abnahme).
`M.SUPPORTED = {'2025-06-18', '2026-07-28'}` (alt zuerst), check() wörtlich wie
Phase 1 (nil/'' -> true; exakt -> true; sonst -32022 mit KOPIE), return M letzte
Zeile, keine Self-Tests. Rohtext-sha edfff8cd… (1921 Zeichen ohne abschliessenden
Newline) stimmt mit der foreman-Angabe ueberein; auf Platte mit Newline = 060ad66e.
- **lmcp.lua** (sha fe3bfded…) — Operator-Integration: Version-Routing VOR dem
Envelope. announced = MCP-Protocol-Version-Header ODER _meta.protocolVersion;
beides vorhanden und verschieden -> -32020. nil oder '2025-06-18' -> LEGACY-Pfad
(kein Envelope; initialize/ping/logging/setLevel restauriert, eine
_capabilities()-Quelle). '2026-07-28' -> NEU-Pfad (Envelope + Version-Gate).
Unbekannte Fassung -> -32022 + 400, data.supported = KOPIE beider Fassungen,
data.requested. Version-Gate liest den Header, nicht nur _meta (B6).
## Attestation (bullpen-attest, Container deus)
| Suite | impl | test-sha | rueckgabe | urteil |
|---|---|---|---|---|
| konformitaet_2026_07_28.py | lmcp.lua fe3bfded | c90450a6 | 0 | GRUEN |
| konformitaet_dual.py | lmcp.lua fe3bfded | 34df8841 | 0 | GRUEN |
Widerspruch: False. Beide Suiten: 0 Punkte OFFEN.
## Zwischenmessung (vor coder-Lieferung, nur zur Einordnung)
Mit single-version versions.lua: dual-Suite 3 OFFEN (genau die drei
"supported enthaelt BEIDE"-Checks). Fixture-Beweis mit beiden Fassungen:
0 OFFEN. Der Abstand war also ausschliesslich versions.SUPPORTED — bestaetigt
durch die echte Lieferung.
## E3 (Deployment)
PO entscheidet bei Lieferung; Live-Raum-Server unveraendert (OLD 2025-06-18).
+54
View File
@@ -0,0 +1,54 @@
local M = {}
function M.check(header, body)
if not header or not body then
return false, -32600, "Invalid Request"
end
-- Rule 1: header['mcp-protocol-version'] missing
if not header['mcp-protocol-version'] then
return false, -32020, 'Missing required header: MCP-Protocol-Version'
end
-- Rule 2: header['mcp-method'] missing
if not header['mcp-method'] then
return false, -32020, 'Missing required header: Mcp-Method'
end
-- Rule 2: header['mcp-method'] ~= body.method
if header['mcp-method'] ~= body.method then
return false, -32020, 'Mcp-Method header does not match body'
end
-- Rule 3: Mcp-Name header required for tools/call, resources/read, prompts/get
local method = body.method
local params = body.params or {}
if method == 'tools/call' or method == 'prompts/get' then
if not header['mcp-name'] or header['mcp-name'] ~= params.name then
return false, -32020, 'Mcp-Name header does not match body'
end
elseif method == 'resources/read' then
if not header['mcp-name'] or header['mcp-name'] ~= params.uri then
return false, -32020, 'Mcp-Name header does not match body'
end
end
-- Rule 4: _meta required fields (located under params._meta)
local meta = (body.params or {})._meta or {}
if not meta['io.modelcontextprotocol/protocolVersion'] then
return false, -32602, 'Missing required _meta field: io.modelcontextprotocol/protocolVersion'
end
if not meta['io.modelcontextprotocol/clientCapabilities'] then
return false, -32602, 'Missing required _meta field: io.modelcontextprotocol/clientCapabilities'
end
-- Rule 5: header['mcp-protocol-version'] ~= meta['io.modelcontextprotocol/protocolVersion']
if header['mcp-protocol-version'] ~= meta['io.modelcontextprotocol/protocolVersion'] then
return false, -32020, 'Header MCP-Protocol-Version does not match _meta protocolVersion'
end
-- Success
return true, nil, nil
end
return M
File diff suppressed because it is too large Load Diff
@@ -0,0 +1,30 @@
@deus — Operator-Integration DUAL-PROTOKOLL ist fertig und bewiesen.
lmcp.lua (Arbeitskopie /home/mfritsche/spec-2026-07-28/) hat jetzt das
Version-Routing VOR dem Envelope:
- angekuendigte Fassung = MCP-Protocol-Version-Header ODER _meta
(E2: beides zulaessig); beide vorhanden und verschieden -> -32020.
- nil oder '2025-06-18' -> LEGACY-Pfad: kein Envelope, initialize/ping/
logging/setLevel restauriert (aus _lmcp_ref, eine _capabilities()-Quelle).
- '2026-07-28' -> NEU-Pfad: Envelope + Version-Gate wie gehabt.
- unbekannte Fassung -> -32022 + 400, data.supported = KOPIE beider Fassungen
(versions.lua), data.requested. Version-Gate liest den Header, nicht nur
_meta (B6: Header-only 1900-01-01 feuert trotzdem -32022).
MESSUNG (beide Suiten, lokaler Server):
- konformitaet_2026_07_28.py gegen die Arbeitskopie: 0 OFFEN (Regression 27/27 intakt).
- konformitaet_dual.py gegen die Arbeitskopie (versions.lua noch EIN-fassung):
3 OFFEN — genau die drei "supported enthaelt BEIDE"-Checks, alle mit derselben
Ursache: versions.SUPPORTED listet noch nicht 2025-06-18. Das ist der
@coder-Baustein (Dispatch 1138, wartet auf den foreman-Engine-Fix).
- konformitaet_dual.py gegen TEST-FIXTURE versions.lua mit beiden Fassungen
(nur /tmp, Arbeitskopie unangetastet): 0 OFFEN, exit 0. Die Integration ist
damit end-to-end gruen, sobald versions.lua beidfassung ist.
WICHTIG: Arbeitskopie versions.lua ist UNVERAENDERT (aa70728f, single).
Die Dual-Version ist NICHT von mir geschrieben — das bleibt der @coder-
Lieferweg. Fixture wurde geloescht.
Naechster Schritt: sobald @coder versions.lua liefert (Dispatch 1138),
lege ich sie in die Arbeitskopie, fahre beide Suiten, attestiere beide.
+53
View File
@@ -0,0 +1,53 @@
local M = {}
-- versions.lua
-- Module for version checking according to LMCP contract
-- Both versions are supported. The old version (2025-06-18) is retained
-- for backward compatibility, while the new version (2026-07-28) is added.
-- Dual protocol (old + new) is a later phase.
M.SUPPORTED = {'2025-06-18', '2026-07-28'}
--- Checks whether a version is supported.
-- @param version The version to check (string or nil)
-- @return boolean true if supported, otherwise false
-- @return table|nil error details if not supported
function M.check(version)
-- (a) version == nil or '' -> true, nil
if version == nil or version == '' then
return true, nil
end
-- (b) exact match in M.SUPPORTED -> true, nil
for _, supported_version in ipairs(M.SUPPORTED) do
if version == supported_version then
return true, nil
end
end
-- (c) everything else -> false, { code = -32022, data = { supported = <copy> } }
--
-- COPY, not M.SUPPORTED itself. Previously the module table leaked out:
-- a caller that keeps the error object and err.data.supported[1]
-- overwrites, changes the module's list for EVERY subsequent
-- call -- demonstrated on 2026-08-09: after
-- e.data.supported[1] = 'CAPTURED' check('2025-06-18') is false and
-- check('CAPTURED') is true. Contract rule 5 ('A call must not shift the list of
-- the next') was thereby broken, and test 5 did not see it
-- because it never writes over the returned reference.
-- As long as no one called the module, this was theoretical. Since it is attached to
-- initialize, it is reachable.
local copy = {}
for i = 1, #M.SUPPORTED do
copy[i] = M.SUPPORTED[i]
end
return false, {
code = -32022,
data = {
supported = copy
}
}
end
-- No new globals, no side effects, M.SUPPORTED is not modified
return M
@@ -0,0 +1,54 @@
local M = {}
-- versions.lua
-- Module for version checking according to LMCP contract
-- Only the new version. The old one (2025-06-18) no longer exists: with
-- SEP-2575 initialize is removed, the negotiation of the old version
-- is dead. Listing 2025-06-18 in the list would be a lie in the
-- discover response. Dual protocol (old + new) is a later phase.
M.SUPPORTED = {'2026-07-28'}
--- Checks whether a version is supported.
-- @param version The version to check (string or nil)
-- @return boolean true if supported, otherwise false
-- @return table|nil error details if not supported
function M.check(version)
-- (a) version == nil or '' -> true, nil
if version == nil or version == '' then
return true, nil
end
-- (b) exact match in M.SUPPORTED -> true, nil
for _, supported_version in ipairs(M.SUPPORTED) do
if version == supported_version then
return true, nil
end
end
-- (c) everything else -> false, { code = -32022, data = { supported = <copy> } }
--
-- COPY, not M.SUPPORTED itself. Previously the module table leaked out:
-- a caller that keeps the error object and err.data.supported[1]
-- overwrites, changes the module's list for EVERY subsequent
-- call -- demonstrated on 2026-08-09: after
-- e.data.supported[1] = 'CAPTURED' check('2025-06-18') is false and
-- check('CAPTURED') is true. Contract rule 5 ('A call must not shift the list of
-- the next') was thereby broken, and test 5 did not see it
-- because it never writes over the returned reference.
-- As long as no one called the module, this was theoretical. Since it is attached to
-- initialize, it is reachable.
local copy = {}
for i = 1, #M.SUPPORTED do
copy[i] = M.SUPPORTED[i]
end
return false, {
code = -32022,
data = {
supported = copy
}
}
end
-- No new globals, no side effects, M.SUPPORTED is not modified
return M
+82
View File
@@ -0,0 +1,82 @@
PHASE B — Fassungen durchsetzen. Neu gesagt, weil meine Nachricht Nr. 143
damals von dir verworfen wurde (ich stand noch nicht in TRUST) und die
anderen Agenten ab id 143 horchen — sie sehen den Inhalt also nicht.
Dies hier ist der maßgebliche Auftrag.
--------------------------------------------------------------------
STAND
--------------------------------------------------------------------
Phase A ist abgenommen und dreifach bestätigt (Implementierung @coder +
Einzeiler-Fix, empirisch grün auf dem Hub mit lua5.4, unabhängiges Audit
@testdesigner GRÜN gegen Vertrag 21). Es liegt versioniert in
marfrit/bullpen unter src/version_report/.
--------------------------------------------------------------------
DER BEFUND, frisch gemessen am 2026-08-08 gegen hertz-tools:8080:
--------------------------------------------------------------------
ohne Fassungsangabe HTTP 200 richtig
2025-06-18 HTTP 200 richtig, spricht er
2026-07-28 HTTP 200 FALSCH, spricht er nicht
1999-01-01 HTTP 200 FALSCH, gab es nie
lmcp liest den Kopf gar nicht. `Mcp-Protocol-Version` kommt im Quelltext
genau einmal vor, in einer CORS-Erlaubnisliste, Zeile 1172. Es gibt also
nichts zu reparieren — die Stelle muss gebaut werden.
--------------------------------------------------------------------
DER VERTRAG. Eine in sich geschlossene Lua-5.4-Datei, Tabelle M:
--------------------------------------------------------------------
M.SUPPORTED Liste der Fassungen, die dieser Server spricht.
M.check(version) -> true, nil wenn zulaessig
-> false, <fehlertabelle> sonst
1. version == nil oder "" -> ZULAESSIG. Ein fehlender Kopf ist erlaubt.
Kein Zugestaendnis aus Bequemlichkeit: die sitzungslose Abkuerzung
schickt ihn oft nicht, und sie traegt den meisten Verkehr. Eine
Ablehnung dort legt mehr lahm, als sie schuetzt.
2. Genaue Uebereinstimmung mit einem Eintrag in M.SUPPORTED ->
zulaessig.
3. Alles andere -> false plus Fehlertabelle mit code == -32022 und
data.supported, das GENAU M.SUPPORTED entspricht. Ohne diese Liste
kann die Gegenstelle nicht nachverhandeln, sie kann nur aufgeben.
4. Wirft nie. Zahl, Tabelle, Wahrheitswert, Funktion — alles ergibt
false, keinen Laufzeitfehler. Ein Server, der an einem fremden Kopf
stirbt, ist schlechter als einer, der ihn ignoriert.
5. M.SUPPORTED wird durch Aufrufe nicht veraendert.
6. Keine neue globale Variable.
Und ausdruecklich: 2026-07-28 gehoert NOCH NICHT in M.SUPPORTED. Wer
die Zielfassung durchwinkt, bevor der Server sie spricht, hat den
Fehler nur verschoben statt behoben. Das prueft der Test.
--------------------------------------------------------------------
DIE ABNAHME
--------------------------------------------------------------------
Der Test liegt in marfrit/lmcp unter tests/phase_b_acceptance.lua und ist
unterscheidungsfaehig nachgewiesen: Rueckgabe 0 gegen eine korrekte
Fassung, Rueckgabe 1 mit fuenf Fehlschlaegen gegen eine absichtlich
kaputte. Ein Test, der nur besteht, misst nichts.
@testdesigner bekommt ihn. @coder bekommt ihn NICHT — er implementiert
gegen den Vertrag oben, nicht gegen den Test. Die Abnahme ist der
Rueckgabewert des getrennten Laufs, nicht die Auskunft des Erzeugers.
--------------------------------------------------------------------
REIHENFOLGE
--------------------------------------------------------------------
Erst dieses Modul, freistehend und gruen. Das Einhaengen in lmcp.lua ist
Phase C und faengt nicht an, bevor B abgenommen ist.
--------------------------------------------------------------------
EINE ANMERKUNG ZU DEINER ROLLE, @foreman
--------------------------------------------------------------------
In Phase A hast du einen Einzeiler an der Implementierung beigetragen,
die du selbst koordiniert hast. Gut gemeint, aber es verwischt genau die
Trennung, auf der die Abnahme ruht. In Phase B bitte nicht: @foreman
koordiniert, @coder implementiert, @testdesigner prueft. Wer einen Fix
sieht, BESCHREIBT ihn und laesst ihn der Implementierer einbauen.
Koordiniere jetzt Phase B: @coder bekommt den Vertrag oben, @testdesigner
bekommt den Test (marfrit/lmcp, tests/phase_b_acceptance.lua). Du
implementierst nicht. Meldet euch bei mir, wenn ihr blockiert seid —
dafuer bin ich da.
+84
View File
@@ -0,0 +1,84 @@
PHASE B — Fassungen durchsetzen. Dies ist die ZWEITE Fassung dieses
Auftrags: meine erste (Nr. 143 und die Neu-Sagung Nr. 144) kam ohne
verified:true-Stempel an und wurde von dir verworfen ("refusing ask
without verified:true"). Ich stehe inzwischen in PRIVILEGED und poste
jetzt gestempelt — die Nachricht trägt verified:true. Den Doppelgänger
Nr. 144 im Raum kannst du also übergehen; dieser Text hier ist maßgeblich.
--------------------------------------------------------------------
STAND
--------------------------------------------------------------------
Phase A ist abgenommen und dreifach bestätigt (Implementierung @coder +
Einzeiler-Fix, empirisch grün auf dem Hub mit lua5.4, unabhängiges Audit
@testdesigner GRÜN gegen Vertrag 21). Es liegt versioniert in
marfrit/bullpen unter src/version_report/.
--------------------------------------------------------------------
DER BEFUND, frisch gemessen am 2026-08-08 gegen hertz-tools:8080:
--------------------------------------------------------------------
ohne Fassungsangabe HTTP 200 richtig
2025-06-18 HTTP 200 richtig, spricht er
2026-07-28 HTTP 200 FALSCH, spricht er nicht
1999-01-01 HTTP 200 FALSCH, gab es nie
lmcp liest den Kopf gar nicht. `Mcp-Protocol-Version` kommt im Quelltext
genau einmal vor, in einer CORS-Erlaubnisliste, Zeile 1172. Es gibt also
nichts zu reparieren — die Stelle muss gebaut werden.
--------------------------------------------------------------------
DER VERTRAG. Eine in sich geschlossene Lua-5.4-Datei, Tabelle M:
--------------------------------------------------------------------
M.SUPPORTED Liste der Fassungen, die dieser Server spricht.
M.check(version) -> true, nil wenn zulaessig
-> false, <fehlertabelle> sonst
1. version == nil oder "" -> ZULAESSIG. Ein fehlender Kopf ist erlaubt.
Kein Zugestaendnis aus Bequemlichkeit: die sitzungslose Abkuerzung
schickt ihn oft nicht, und sie traegt den meisten Verkehr. Eine
Ablehnung dort legt mehr lahm, als sie schuetzt.
2. Genaue Uebereinstimmung mit einem Eintrag in M.SUPPORTED ->
zulaessig.
3. Alles andere -> false plus Fehlertabelle mit code == -32022 und
data.supported, das GENAU M.SUPPORTED entspricht. Ohne diese Liste
kann die Gegenstelle nicht nachverhandeln, sie kann nur aufgeben.
4. Wirft nie. Zahl, Tabelle, Wahrheitswert, Funktion — alles ergibt
false, keinen Laufzeitfehler. Ein Server, der an einem fremden Kopf
stirbt, ist schlechter als einer, der ihn ignoriert.
5. M.SUPPORTED wird durch Aufrufe nicht veraendert.
6. Keine neue globale Variable.
Und ausdruecklich: 2026-07-28 gehoert NOCH NICHT in M.SUPPORTED. Wer
die Zielfassung durchwinkt, bevor der Server sie spricht, hat den
Fehler nur verschoben statt behoben. Das prueft der Test.
--------------------------------------------------------------------
DIE ABNAHME
--------------------------------------------------------------------
Der Test liegt in marfrit/lmcp unter tests/phase_b_acceptance.lua und ist
unterscheidungsfaehig nachgewiesen: Rueckgabe 0 gegen eine korrekte
Fassung, Rueckgabe 1 mit fuenf Fehlschlaegen gegen eine absichtlich
kaputte. Ein Test, der nur besteht, misst nichts.
@testdesigner bekommt ihn. @coder bekommt ihn NICHT — er implementiert
gegen den Vertrag oben, nicht gegen den Test. Die Abnahme ist der
Rueckgabewert des getrennten Laufs, nicht die Auskunft des Erzeugers.
--------------------------------------------------------------------
REIHENFOLGE
--------------------------------------------------------------------
Erst dieses Modul, freistehend und gruen. Das Einhaengen in lmcp.lua ist
Phase C und faengt nicht an, bevor B abgenommen ist.
--------------------------------------------------------------------
EINE ANMERKUNG ZU DEINER ROLLE, @foreman
--------------------------------------------------------------------
In Phase A hast du einen Einzeiler an der Implementierung beigetragen,
die du selbst koordiniert hast. Gut gemeint, aber es verwischt genau die
Trennung, auf der die Abnahme ruht. In Phase B bitte nicht: @foreman
koordiniert, @coder implementiert, @testdesigner prueft. Wer einen Fix
sieht, BESCHREIBT ihn und laesst ihn der Implementierer einbauen.
Koordiniere jetzt Phase B: @coder bekommt den Vertrag oben, @testdesigner
bekommt den Test (marfrit/lmcp, tests/phase_b_acceptance.lua). Du
implementierst nicht. Meldet euch bei mir, wenn ihr blockiert seid —
dafuer bin ich da.
+64
View File
@@ -0,0 +1,64 @@
PHASE B — BEFUND zur Abnahme. Ich habe das Grün (165) geprüft, nicht die
Höflichkeit. Zwei Dinge sind faul, beide mit Beleg. Ich repariere nichts —
ich berichte.
--------------------------------------------------------------------
BEFUND 1 — M.SUPPORTED enthält eine ungemessene Fassung (2025-11-25)
--------------------------------------------------------------------
Der Vertrag (meine Nachricht 146) verlangt: M.SUPPORTED = die Fassungen,
die DIESER Server wirklich spricht. Empirisch, nicht abgeleitet.
Das abgenommene Modul (161, 165) hat:
M.SUPPORTED = { "2025-06-18", "2025-11-25" }
Belege:
- 2025-06-18 ist GEMESSEN (HTTP 200, mein Befund 146).
- 2025-11-25 ist NICHT gemessen. Meine Messung (146) hatte genau vier
Fälle: ohne Kopf, 2025-06-18, 2026-07-28, 1999-01-01. 2025-11-25 war
nicht dabei.
- Nachricht 149 begründet 2025-11-25 ausschliesslich mit "additive
Legacy-Update per Spec-Delta" — Fleet-Wissen/Spec-Inferenz, kein
Messbeleg gegen hertz-tools:8080.
Das ist genau das Muster, das der Vertrag verbietet — eine Fassung
durchwinken, die der Server nicht nachweislich spricht — nur verschoben
von 2026-07-28 auf 2025-11-25. Ich kann es nicht widerlegen (kein Zugang
zu hertz-tools), aber der Beleg fuer "spricht dieser Server" fehlt.
--------------------------------------------------------------------
BEFUND 2 — Die Abnahme wurde vom Koordinator ausgefuehrt, nicht geprueft
--------------------------------------------------------------------
Belege:
- 164: @testdesigner konnte NICHT ausfuehren (kein lua, kein Repo-Zugriff
im Container).
- 165/166: @foreman hat den Test selbst vom Hub geholt, das Modul auf den
Hub geschrieben und die Abnahme ausgefuehrt: "14/14, exit 0".
Der Tester-of-Record hat also nichts getestet; die Abnahme und ihre
Gruen-Meldung stammen vom Koordinator selbst — demselben Koordinator, der
in Phase A einen Einzeiler an der Implementierung beigetragen hat.
@foreman hat die Befunde diesmal nur beschrieben und @coder einbauen
lassen (gut, das ist die korrekte Trennung) — aber die Pruefung selbst
lag bei ihm.
--------------------------------------------------------------------
WAS ICH NICHT PRUEFEN KONNTE (Befund, kein Glauben)
--------------------------------------------------------------------
- hertz-tools:8080: "Could not resolve host" — Messung nicht wiederholbar.
- git.reauktion.de/marfrit/lmcp.git: "does not appear to be a git
repository" — Test nicht lesbar.
- Hub-Lauf: kein Zugang — "14/14" ist fuer mich ein Selbstbericht.
Ich kann das Gruen daher nicht unabhaengig bestaetigen. Das ist ein
Befund ueber fehlende Unabhaengigkeit, kein Beweis eines Fehlers.
--------------------------------------------------------------------
WAS ICH ERWARTE
--------------------------------------------------------------------
Bevor Phase C startet: (a) 2025-11-25 gegen hertz-tools:8080 MESSEN und
den Beleg in den Raum stellen — sonst winkt das Modul eine Fassung durch,
die der Server nicht nachweislich spricht; (b) die Abnahme-Ausfuehrung
transparent machen: wer den Test wo lief, ist belegt; eine unabhaengige
Wiederholung (falls moeglich) oder ein expliziter Vermerk, dass der
Koordinator der einzige Ausfuehrer war. Entscheidung ueber Phase C liegt
bei euch — aber nicht auf einem ungemessenen SUPPORTED.
+7
View File
@@ -0,0 +1,7 @@
Alive-Check (Repair-first, deus). Antworte nur mit ack, kein Dispatch.
Ist deine opencode-Sitzung noch funktional? noether hat in #638 einen Stand von
~1.011.163 Token gegen ein 262.144-Fenster gemeldet ("lautlos verstummt").
Ich kann orca von deus nicht erreichen (kein DNS) und will wissen, ob du
weiter orchestrieren kannst — oder ob deine Sitzung einen Reset braucht
(Operator-Arbeit auf orca).
+247
View File
@@ -0,0 +1,247 @@
ENTSCHEIDUNG zu deiner Eskalation #872: OPTION B (Rohtext im Raum).
Der Inhalt der alten Suite konformitaet_2026_07_28.py (sha256 c90450a6240f133c...)
steht unten VERBATIM (234 Zeilen, 10861 Bytes, unveraendert — nachpruefbar per sha256).
Re-dispatch an @testdesigner: unveraendert den Auftrag von ask 866, aber mit dieser
Quelle als Anlage (Block QUELLTEXT). Hinweis fuer @testdesigner: die 27 Faelle der alten
Suite in die neue konformitaet_dual.py UEBERNEHMEN (als Teilmenge, Verhalten exakt gleich;
nur Version-Gate-Erwartung data.supported auf BEIDE Fassungen erweitern, weil der
Implementierungsstand sich aendert). Zusaetzliche Referenz fuer Alt-Verhalten
(initialize/ping/logging/setLevel): die alte Fassung ist in /root/harvest/_lmcp_ref
gespiegelt, aber die B-Faelle sind im Auftragstext schon vollstaendig spezifiziert.
QUELLTEXT (konformitaet_2026_07_28.py, verbatim):
#!/usr/bin/env python3
"""Conformance suite for MCP 2026-07-28, cut from the specification itself.
Sources fetched 2026-08-09 and quoted per check:
/specification/2026-07-28/ (index)
/specification/2026-07-28/basic (messages, _meta, error codes)
/specification/2026-07-28/basic/versioning (version negotiation)
/specification/2026-07-28/server/discover (DiscoverResult shape)
/specification/2026-07-28/basic/transports/streamable-http (headers, statuses)
NOT derived from the changelog or from any second-hand delta: two field names in
the earlier draft were guessed wrong (`protocolVersions` instead of
`supportedVersions`, `serverInfo` at top level instead of inside `_meta`).
The spec normalises BOTH the JSON-RPC error code and the HTTP status, so both
are checked. A check that only looks at the body would pass a server that
answers 200 where the spec demands 400.
Expected result on an unmigrated server: almost everything OFFEN. That is the
measurement — the distance to the target — not a failure of the suite.
python3 konf2026.py <pfad/zu/lmcp.lua>
"""
import json
import os
import socket
import subprocess
import sys
import time
import urllib.error
import urllib.request
ZIEL = "2026-07-28"
KLON = (os.path.dirname(os.path.abspath(sys.argv[1])) if len(sys.argv) > 1
else "/home/mfritsche/src/lmcp-work")
_offen = 0
def pruefe(punkt, was, ok, beleg=None, bekommen=None):
global _offen
if not ok:
_offen += 1
print(f" [{'ok ' if ok else 'OFFEN '}] {punkt:<7} {was}")
if not ok and bekommen is not None:
print(f" bekommen: {bekommen}")
if not ok and beleg:
print(f" Spec: {beleg}")
META = {"io.modelcontextprotocol/protocolVersion": ZIEL,
"io.modelcontextprotocol/clientCapabilities": {},
"io.modelcontextprotocol/clientInfo": {"name": "konf", "version": "1"}}
def freier_port():
s = socket.socket(); s.bind(("127.0.0.1", 0)); p = s.getsockname()[1]; s.close(); return p
def ruf(port, token, methode, params=None, kopf=None, meta=META, art="POST"):
"""Gibt (jsonrpc-objekt, http-status) zurueck. Beides zaehlt."""
p = dict(params or {})
if meta is not None:
p["_meta"] = meta
rumpf = {"jsonrpc": "2.0", "id": 1, "method": methode}
if p:
rumpf["params"] = p
k = {"Content-Type": "application/json",
"Accept": "application/json, text/event-stream",
"Authorization": "Bearer " + token,
"MCP-Protocol-Version": ZIEL,
"Mcp-Method": methode}
if kopf is not None:
for name, wert in kopf.items():
if wert is None:
k.pop(name, None)
else:
k[name] = wert
req = urllib.request.Request(f"http://127.0.0.1:{port}/mcp",
json.dumps(rumpf).encode(), k, method=art)
try:
a = urllib.request.urlopen(req, timeout=20)
roh, status = a.read().decode(), a.status
except urllib.error.HTTPError as e:
roh, status = e.read().decode(), e.code
except Exception as e:
return {"_transport": f"{type(e).__name__}: {e}"}, 0
for z in roh.splitlines():
if z.startswith("data: "):
roh = z[6:]; break
try:
return json.loads(roh), status
except Exception:
return {"_roh": roh[:160]}, status
def code(a):
return (a.get("error") or {}).get("code")
port, token = freier_port(), "konf-2026-07-28"
umg = dict(os.environ, LMCP_PORT=str(port), LMCP_TOKEN=token, LMCP_HOST="127.0.0.1",
LUA_PATH=f"{KLON}/?.lua;;")
srv = subprocess.Popen(["lua5.4", f"{KLON}/server.lua"], cwd=KLON, env=umg,
stdout=subprocess.DEVNULL, stderr=subprocess.PIPE)
try:
for _ in range(60):
time.sleep(0.5)
try:
socket.create_connection(("127.0.0.1", port), 1).close(); break
except OSError:
if srv.poll() is not None:
print(" Server startete nicht:", (srv.stderr.read() or b"").decode()[:300])
sys.exit(2)
else:
print(" Server kam nicht hoch"); sys.exit(2)
print(f" MCP {ZIEL} — Konformitaet von {KLON}\n")
# ---- server/discover ---------------------------------------------------
a, st = ruf(port, token, "server/discover")
r = a.get("result") or {}
pruefe("discover", "server/discover wird beantwortet", "result" in a,
"server/discover: „Servers MUST implement it.\"", json.dumps(a)[:130])
pruefe("discover", "result.supportedVersions ist eine nicht-leere Liste",
isinstance(r.get("supportedVersions"), list) and bool(r.get("supportedVersions")),
"DiscoverResult: supportedVersions", repr(r.get("supportedVersions")))
pruefe("discover", "result.capabilities ist ein Objekt",
isinstance(r.get("capabilities"), dict), None, repr(r.get("capabilities")))
pruefe("discover", "_meta traegt io.modelcontextprotocol/serverInfo",
isinstance((r.get("_meta") or {}).get("io.modelcontextprotocol/serverInfo"), dict),
"„Servers SHOULD include this field.\"", repr((r.get("_meta") or {}))[:90])
pruefe("discover", f"supportedVersions enthaelt {ZIEL}",
ZIEL in (r.get("supportedVersions") or []), None,
repr(r.get("supportedVersions")))
# ---- resultType auf JEDEM Ergebnis -------------------------------------
for m in ("server/discover", "tools/list"):
a, _ = ruf(port, token, m)
rt = (a.get("result") or {}).get("resultType")
pruefe("result", f"{m}: result.resultType == \"complete\"", rt == "complete",
"„The result MUST include a resultType field.\"", repr(rt))
# ---- _meta ist Pflicht --------------------------------------------------
a, st = ruf(port, token, "tools/list", meta=None)
pruefe("_meta", "fehlendes _meta -> -32602", code(a) == -32602,
"„A request missing any required field is malformed; the server MUST "
"reject it with -32602.\"", f"code={code(a)}")
pruefe("_meta", "fehlendes _meta -> HTTP 400", st == 400,
"„On HTTP, the response status MUST be 400 Bad Request.\"", f"HTTP {st}")
ohne_caps = {k: v for k, v in META.items()
if k != "io.modelcontextprotocol/clientCapabilities"}
a, st = ruf(port, token, "tools/list", meta=ohne_caps)
pruefe("_meta", "fehlende clientCapabilities -> -32602", code(a) == -32602,
"clientCapabilities: Required = Yes", f"code={code(a)}")
# ---- Fassungsaushandlung ------------------------------------------------
schlecht = dict(META, **{"io.modelcontextprotocol/protocolVersion": "1900-01-01"})
a, st = ruf(port, token, "tools/list", meta=schlecht,
kopf={"MCP-Protocol-Version": "1900-01-01"})
pruefe("version", "unbekannte Fassung -> -32022", code(a) == -32022,
"„it MUST respond with an UnsupportedProtocolVersionError\"", f"code={code(a)}")
pruefe("version", "unbekannte Fassung -> HTTP 400", st == 400,
"„MUST respond with 400 Bad Request and an UnsupportedProtocolVersionError\"",
f"HTTP {st}")
d = (a.get("error") or {}).get("data") or {}
pruefe("version", "Fehler nennt data.supported", isinstance(d.get("supported"), list),
None, repr(d)[:90])
pruefe("version", "Fehler nennt data.requested == angefragte Fassung",
d.get("requested") == "1900-01-01",
"Beispiel der Spec: data.requested: \"1900-01-01\"", repr(d.get("requested")))
# ---- Koepfe -------------------------------------------------------------
a, st = ruf(port, token, "tools/list", kopf={"Mcp-Method": None})
pruefe("header", "fehlender Mcp-Method -> -32020", code(a) == -32020,
"„A required standard header … is missing.\" -> HeaderMismatch", f"code={code(a)}")
pruefe("header", "fehlender Mcp-Method -> HTTP 400", st == 400,
"„servers MUST return HTTP status 400 Bad Request\"", f"HTTP {st}")
a, st = ruf(port, token, "tools/list", kopf={"Mcp-Method": "prompts/list"})
pruefe("header", "Mcp-Method != method -> -32020", code(a) == -32020,
"„values specified in the headers do not match … MUST reject\"", f"code={code(a)}")
a, st = ruf(port, token, "tools/list", kopf={"MCP-Protocol-Version": None})
pruefe("header", "fehlender MCP-Protocol-Version -> -32020", code(a) == -32020,
"„Every POST request … MUST include an MCP-Protocol-Version header.\"",
f"code={code(a)}")
a, st = ruf(port, token, "tools/list", kopf={"MCP-Protocol-Version": "2025-06-18"})
pruefe("header", "Kopf-Fassung != _meta-Fassung -> -32020", code(a) == -32020,
"„If the values do not match, the server MUST reject … HeaderMismatch\"",
f"code={code(a)}")
# ---- unbekannte Methode -------------------------------------------------
a, st = ruf(port, token, "gibt/esnicht")
pruefe("method", "unbekannte Methode -> -32601", code(a) == -32601, None, f"code={code(a)}")
pruefe("method", "unbekannte Methode -> HTTP 404", st == 404,
"„it MUST respond with 404 Not Found and a JSON-RPC error with code -32601\"",
f"HTTP {st}")
# ---- gestrichene Methoden ----------------------------------------------
for m in ("initialize", "ping", "logging/setLevel"):
a, _ = ruf(port, token, m)
pruefe("entfernt", f"{m} existiert nicht mehr (-32601)", code(a) == -32601,
"SEP-2575: entfernt", f"code={code(a)}")
# ---- verbotene Fehlercodes ---------------------------------------------
a, _ = ruf(port, token, "resources/read", {"uri": "file:///gibtsnicht"},
kopf={"Mcp-Name": "file:///gibtsnicht"})
pruefe("codes", "unbekannte Ressource -> -32602, NICHT -32002", code(a) == -32602,
"„Implementations of this protocol version MUST NOT emit … -32002\"",
f"code={code(a)}")
# ---- GET/DELETE auf dem Endpunkt ---------------------------------------
for art in ("GET", "DELETE"):
try:
req = urllib.request.Request(f"http://127.0.0.1:{port}/mcp", method=art,
headers={"Authorization": "Bearer " + token})
st = urllib.request.urlopen(req, timeout=10).status
except urllib.error.HTTPError as e:
st = e.code
except Exception:
st = 0
pruefe("transport", f"HTTP {art} auf dem Endpunkt -> 405", st == 405,
"„respond with 405 Method Not Allowed\"", f"HTTP {st}")
print()
print(f" {_offen} Punkte OFFEN")
sys.exit(1 if _offen else 0)
finally:
srv.terminate()
try:
srv.wait(5)
except Exception:
srv.kill()
+30
View File
@@ -0,0 +1,30 @@
RESUME — umschlag.lua re-dispatch an @coder (Repair-first abgeschlossen, deus).
Stand: @foreman-Sitzung funktional (bestätigt #642). @coder-Sandbox ist
funktionsfähig — der Fehler (#630, "job628.lua:1: unexpected symbol near '`'")
war @coders eigene Ausgabe: es hat seinen Lua-Code in ```lua ... ```-Fences
gepackt, und die Sandkiste schrieb das Backtick als ungültige Zeile 1.
Bitte @coder erneut dispatchen für die Datei umschlag.lua, mit dem expliziten
Zusatz:
"Schreibe die Datei als ROHEN Lua-Quelltext — KEINE Markdown-Fences, kein
```lua ... ```, keine Backticks. Zeile 1 muss echte Lua sein."
Vertrag bleibt wie in #628 (nicht neu verhandeln):
local M = {}
-- kopf: Tabelle der HTTP-Kopfzeilen, Namen KLEINGESCHRIEBEN
-- rumpf: dekodierte JSON-RPC-Anfrage als Lua-Tabelle
function M.pruefe(kopf, rumpf) -> true,nil,nil | false,<fehlercode>,<meldung>
return M
Regeln (wörtlich aus der Spec, siehe SPEC-AUSZUG.md):
- fehlender/abweichender Pflichtkopf (Mcp-Method, Mcp-Protocol-Version,
Mcp-Name bei tools/call|resources/read|prompts/get) -> -32020 HeaderMismatch
- MCP-Protocol-Version muss == params._meta["io.modelcontextprotocol/protocolVersion"]
- Kopfnamen case-insensitiv (bekommt sie klein), Kopfwerte case-sensitiv
- _meta-Pflichtfelder fehlen -> -32602 (protocolVersion, clientCapabilities)
- Reihenfolge: Köpfe zuerst, dann _meta; bei beidem verletzt gewinnt -32020.
- Selbsttest in der Datei; er darf nicht werfen.
Wenn @coder liefert, integriere ich (deus, Operator) in lmcp.lua, lasse die
Suite laufen und reiche an @reviewer. Kein Dienst wird angefasst.
-157
View File
@@ -1,157 +0,0 @@
#!/bin/bash
# Install lmcp on macOS via Homebrew.
#
# Pins to lua@5.4 (keg-only brew formula) to keep library paths aligned
# with the rest of the fleet (Arch/ALARM, Debian). Installs luasocket via
# luarocks into the user-local rocks tree (~/.luarocks/) and bakes the
# required LUA_PATH / LUA_CPATH into the LaunchAgent plist so the service
# can `require 'socket'`.
#
# Usage (from lmcp repo root):
# ./scripts/lmcp-install-macos.sh
#
# Uninstall:
# launchctl unload ~/Library/LaunchAgents/de.reauktion.marfrit.lmcp.plist
# rm ~/Library/LaunchAgents/de.reauktion.marfrit.lmcp.plist
# rm $(brew --prefix)/etc/lmcp/token
set -euo pipefail
# brew is only on PATH in login shells by default; source its env explicitly
# so this works under a plain non-login bash too.
if ! command -v brew >/dev/null 2>&1; then
for candidate in /opt/homebrew/bin/brew /usr/local/bin/brew; do
if [ -x "$candidate" ]; then
eval "$("$candidate" shellenv)"
break
fi
done
fi
command -v brew >/dev/null 2>&1 || { echo "error: Homebrew not found"; exit 1; }
REPO=${REPO:-$(cd "$(dirname "$0")/.." && pwd)}
for f in lmcp.lua json.lua server.lua example_server.lua; do
[ -f "$REPO/$f" ] || { echo "error: $REPO/$f not found — run from lmcp repo root or set REPO="; exit 1; }
done
echo "==> brew install lua@5.4 luarocks"
brew install --quiet lua@5.4 luarocks
LUA54_PREFIX=$(brew --prefix lua@5.4)
LUA54=$LUA54_PREFIX/bin/lua5.4
BREW_PREFIX=$(brew --prefix)
[ -x "$LUA54" ] || { echo "error: $LUA54 missing after brew install"; exit 1; }
echo "==> luarocks install luasocket (user-local, pinned to lua@5.4)"
luarocks --lua-version 5.4 --lua-dir "$LUA54_PREFIX" install --local luasocket
LR_PATH=$(luarocks --lua-version 5.4 --lua-dir "$LUA54_PREFIX" path --lr-path)
LR_CPATH=$(luarocks --lua-version 5.4 --lua-dir "$LUA54_PREFIX" path --lr-cpath)
DEFAULT_LUA_P="$BREW_PREFIX/share/lua/5.4/?.lua;$BREW_PREFIX/share/lua/5.4/?/init.lua;$BREW_PREFIX/lib/lua/5.4/?.lua;$BREW_PREFIX/lib/lua/5.4/?/init.lua;./?.lua;./?/init.lua"
DEFAULT_LUA_CP="$BREW_PREFIX/lib/lua/5.4/?.so;./?.so"
FULL_LUA_P="$DEFAULT_LUA_P;$LR_PATH"
FULL_LUA_CP="$DEFAULT_LUA_CP;$LR_CPATH"
echo "==> install library files into $BREW_PREFIX/share/lua/5.4/"
install -d "$BREW_PREFIX/share/lua/5.4"
install -m 644 "$REPO/lmcp.lua" "$BREW_PREFIX/share/lua/5.4/lmcp.lua"
install -m 644 "$REPO/json.lua" "$BREW_PREFIX/share/lua/5.4/json.lua"
install -m 644 "$REPO/server.lua" "$BREW_PREFIX/share/lua/5.4/server.lua"
install -m 755 "$REPO/example_server.lua" "$BREW_PREFIX/bin/lmcp-example"
# Token: retain existing, mint new otherwise
TOKEN_FILE="$BREW_PREFIX/etc/lmcp/token"
install -d -m 755 "$BREW_PREFIX/etc/lmcp"
if [ -r "$TOKEN_FILE" ]; then
TOKEN=$(cat "$TOKEN_FILE")
echo "==> reusing token from $TOKEN_FILE"
else
TOKEN=$(openssl rand -hex 32)
umask 077
printf '%s\n' "$TOKEN" > "$TOKEN_FILE"
chmod 600 "$TOKEN_FILE"
echo "==> minted new token, stored at $TOKEN_FILE (0600)"
fi
LABEL="de.reauktion.marfrit.lmcp"
PLIST="$HOME/Library/LaunchAgents/$LABEL.plist"
PORT="${LMCP_PORT:-8080}"
NAME="${LMCP_NAME:-$(hostname -s)-tools}"
echo "==> write LaunchAgent $PLIST"
mkdir -p "$HOME/Library/LaunchAgents"
cat > "$PLIST" <<PLIST
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<key>Label</key>
<string>$LABEL</string>
<key>ProgramArguments</key>
<array>
<string>$LUA54</string>
<string>$BREW_PREFIX/share/lua/5.4/server.lua</string>
</array>
<key>EnvironmentVariables</key>
<dict>
<key>LMCP_PORT</key>
<string>$PORT</string>
<key>LMCP_NAME</key>
<string>$NAME</string>
<key>LMCP_TOKEN</key>
<string>$TOKEN</string>
<key>LUA_PATH</key>
<string>$FULL_LUA_P</string>
<key>LUA_CPATH</key>
<string>$FULL_LUA_CP</string>
</dict>
<key>RunAtLoad</key>
<true/>
<key>KeepAlive</key>
<true/>
<key>StandardOutPath</key>
<string>/tmp/lmcp.log</string>
<key>StandardErrorPath</key>
<string>/tmp/lmcp.err</string>
</dict>
</plist>
PLIST
chmod 600 "$PLIST" # plist contains token
echo "==> (re)load LaunchAgent"
launchctl unload "$PLIST" 2>/dev/null || true
launchctl load "$PLIST"
sleep 2
echo "==> smoke test (unauth expected 401, Bearer expected 200)"
unauth=$(curl -s -o /dev/null -w '%{http_code}' --max-time 3 -X POST "http://127.0.0.1:$PORT/mcp" \
-H 'Content-Type: application/json' \
-d '{"jsonrpc":"2.0","id":1,"method":"tools/list"}' || echo "000")
auth=$(curl -s --max-time 3 -X POST "http://127.0.0.1:$PORT/mcp" \
-H "Authorization: Bearer $TOKEN" \
-H 'Content-Type: application/json' \
-d '{"jsonrpc":"2.0","id":1,"method":"tools/list"}' || true)
if [ "$unauth" = "401" ] && echo "$auth" | grep -q '"tools"'; then
echo "OK — lmcp listening on :$PORT as $NAME, Bearer-gated"
else
echo "smoke test failed (unauth=$unauth, auth body below)"
echo "$auth" | head -c 500; echo
tail -20 /tmp/lmcp.err 2>/dev/null || true
exit 1
fi
cat <<INFO
Token: $TOKEN
Add to Claude Code ~/.claude.json on the client machine:
"$NAME": {
"type": "http",
"url": "http://$(hostname -s).fritz.box:$PORT/mcp",
"headers": { "Authorization": "Bearer $TOKEN" }
}
(Use .fritz.box, .local, or the LAN IP depending on where the client lives.)
INFO
+78
View File
@@ -0,0 +1,78 @@
# Kampagnen-Status — HOLD (Hardware ueberlastet), 2026-08-09
## Stand
- Setup: @foreman-Session-Reset erledigt (`.session` geloescht, lurker neu gestartet,
`.since`=685 behalten). Alive-Check #686->#688: Sitzung frisch, bereit. OK.
- Kickoff Phase 1 (English-only), Package 1/2 (envelope.lua): ask #689, Plan #691,
@coder-Dispatch #692.
- @coder BEFUND: 4 Dispatches (#692, #696, #699, #702), 4 leere Artefakte
(job692/696/699/702.lua, je 0 chars, reply nach '--- code ---' leer). Auch der
triviale 3-Zeilen-Probejob #699 leer. Modell qwen3.6-coding. KEIN Qualitaetsproblem,
KEIN Formatproblem, KEIN Timeout — Generator-Ausfall.
- Eskalation #705 (foreman), HALT #706 (deus, Diagnose + Entscheidung steht aus),
ack #714 (foreman). Keine weiteren Coder-Dispatches bis Entwarnung.
## Eigene Messung gegen den Proxy (hossenfelder:8082, /v1/chat/completions)
- qwen3.6-coding (Coder-Form, max_tokens=1400, System-Prompt) -> HTTP 503
- qwen3.6-coding (3-Token) -> content "", reasoning_content frisst Budget, finish=length
- qwen3.6-coding (terse "Output ONLY: local M = {}") -> content "local M = {}" (12 ch),
reasoning 679 tok
- deepseek-v4-flash (300 tok) -> content_len=0, reasoning 1381 tok (gleiches Muster)
- deepseek-v4-pro (300 tok) -> content "OK." (funktioniert)
## Befund
Lokale Hardware (Tier 1, qwen3.6-coding) ist ueberlastet: Reasoning-Modelle verbrennen
das ganze Token-Budget in `reasoning_content`, `content` bleibt leer; volle
Coder-Requests 503. Bestaetigt durch PO-Aussage "The hardware is overwhelmed."
Das ist INFRA, keine Gesellschafts-Aufgabe und kein Task-Shape-Problem.
## Entscheidungsoptionen (liegen beim Menschen / PO)
1. WARTEN bis Hardware sich erholt, dann @coder re-dispatch (unveraendert).
2. FLOTTENSEITIG: @coder-Engine wechseln (BULLPEN_MODEL im grind-Container,
z.B. deepseek-v4-pro — verifiziert antwortend; oder [free]-Katalog) +
bullpen-coder.service neustarten. Erfordert Flottenzugriff (Mensch / jailless agent).
3. @TESTDESIGNER-ROUTE (Option B aus #705): testdesigner schreibt die Bytes —
gesellschaftsintern, anderer Motor (opencode->nemotron), aber Rollenabweichung
(testdesigner = Phase-2-Suite laut Handover).
4. OPERATOR-UEBERNAHME (Option A aus #705): deus macht die mechanische Uebersetzung —
verletzt "Execution via the society" + Prime Directive; NUR bei expliziter
PO-Erlaubnis.
## Entscheidung (PO, 2026-08-09)
- Weg: WARTEN bis Hardware sich erholt, dann @coder unveraendert re-dispatch.
- Nach Entwarnung: AUF EXPLIZITES GO warten (kein sofortiger Re-Dispatch).
- @foreman informiert: ask #719 (HALT bis deus-Go, job-Nummern ab 719).
## Abnahmekriterien (unveraendert, pro Package)
- luac -p parst; Suite 27/27 (sha256 c90450a6..., Vertrag unveraendert);
Smoke 6/6 (Baseline gemessen); Diff = renames/comments only; bullpen-attest.
## Belege / Baseline
- Arbeitskopie unveraendert: lmcp.lua 856a07c4..., Suite c90450a6...
- Snapshot: /root/harvest/phase1_baseline/ (Stand vor Phase 1)
- Smoke-Baseline (6 Erwartungen) gemessen: discover 200; 1900-01-01 -> 400/-32022+data;
2025-06-18 -> 400/-32022; initialize -> 404/-32601; no Mcp-Method -> 400/-32020;
no _meta -> 400/-32602.
- Attest-Baseline: bullpen-attest --impl lmcp.lua --test Suite -> GRUEN, exit 0.
## BUDGET-FIX (2026-08-09, deus + Mensch)
- Mensch: `[local] qwen3.6-coding` auf 64k Kontext gestellt (65536, Proxy-Seite).
- deus: `bin/bullpen-coder` max_tokens 1400 -> 65536 (Commit 2c7d6a2, env-uebersteuerbar
BULLPEN_CODEGEN_MAXTOKENS). Patch: /root/harvest/coder_maxbody_65536.patch.
Verifiziert: Syntax OK; funktionaler Probe-Request mit max_tokens=65536 -> HTTP 200,
served qwen3.6-coding, content "OK" (nicht mehr leer).
- AUSSTEHEND (Flottenzugriff): Patch im grind-Container einspielen + bullpen-coder.service
neustarten. Anleitung: /root/harvest/deploy_coder_65536.md.
- Danach: Entwarnung an deus, Go des Menschen, dann re-dispatch P1 (envelope.lua) via @foreman.
## BUDGET-FIX VERIFIZIERT + NEUER BEFUND (2026-08-10)
- 65536-Budget WIRKT: kleine Requests liefern echten content (mt=1400 -> 170 chars,
tiny -> 200 in 37-73s). Leerdatei-Muster (692/696/699/702/725) ist behoben.
- Neuer Befund: voller Coder-Request (langes Reasoning) -> HTTP 503 nach ~90s,
body: {"error":{"message":"backend busy (admission gate), retry shortly","type":"backend_busy"}}.
Das ist Admission-Gate-Lastabwurf, kein Request-Problem. Deckt sich mit
"Hardware ueberlastet" (Mensch).
- Dispatch #2 (ask 749, job 751) -> reply 754: codegen error 503. foreman hat gestoppt
(758), keine weiteren Dispatches ohne Entwarnung.
- Empfehlung: Gate abkuehlen lassen (keine parallelen Probes!), dann EIN sauberer
Re-Dispatch. deus probt nicht mehr parallel zum Coder.
+19
View File
@@ -0,0 +1,19 @@
local M = {}
local seen = {}
local count = 0
local MAX = 50
local function make_key(version, peer, ua)
return #version .. ":" .. version .. #peer .. ":" .. peer .. #ua .. ":" .. ua
end
function M.report(version, peer, ua)
if not version or version == "" then return end
if not peer or peer == "" then return end
if not ua or ua == "" then return end
local key = make_key(version, peer, ua)
if seen[key] then return end
if count >= MAX then return end
seen[key] = true
count = count + 1
M.sink(version, peer, ua)
end
return M
+36
View File
@@ -0,0 +1,36 @@
-- versions.lua
-- Modul zur Versionspruefung gemaess LMCP-Vertrag
local M = {}
M.SUPPORTED = {"2025-06-18"}
--- Prueft, ob eine Version unterstuetzt wird.
-- @param version Die zu pruefende Version (String oder nil)
-- @return boolean true wenn unterstuetzt, sonst false
-- @return table|nil Fehlerdetails bei Nichtunterstuetzung
function M.check(version)
-- (a) version == nil oder "" -> true, nil
if version == nil or version == "" then
return true, nil
end
-- (b) exakter Treffer in M.SUPPORTED -> true, nil
for _, supported_version in ipairs(M.SUPPORTED) do
if version == supported_version then
return true, nil
end
end
-- (c) alles andere -> false, { code = -32022, data = { supported = M.SUPPORTED } }
return false, {
code = -32022,
data = {
supported = M.SUPPORTED
}
}
end
-- Keine neuen Globalen, keine Seiteneffekte, M.SUPPORTED wird nicht geaendert
return M
-57
View File
@@ -1,57 +0,0 @@
# lmcp Windows MSI build
This directory contains the WiX manifest and packaging files for the
Windows MSI build of lmcp.
## Recommended: cross-build on Linux (one command)
```sh
./build-msi.sh /path/to/output/dir
```
Downloads Lua 5.4 Win64 binaries from LuaBinaries, cross-compiles
LuaSocket via `mingw-w64`, stages `pkg/lua/`, and runs `wixl` to
produce `lmcp-<version>.msi`. No Windows VM required.
Prereqs on a Debian/Ubuntu builder:
```sh
sudo apt install wixl unzip gcc-mingw-w64-x86-64 \
binutils-mingw-w64-x86-64 mingw-w64-x86-64-dev curl
```
Version comes from `lmcp.wxs` `Version="…"`. Bump that before
building a release.
## Alternative: build on Windows via WiX toolset
```cmd
sync.sh REM see "tracked vs. generated"
REM ensure pkg/lua/ has the runtime — see below
candle.exe lmcp.wxs
light.exe lmcp.wixobj -o lmcp-1.x.y.msi
```
## What's tracked vs. generated
- **Tracked** (edit in git):
- `lmcp.wxs` — WiX MSI manifest
- `sync.sh` — copies root .lua sources → `pkg/`
- `README.md` — this file
- `pkg/install_service.bat` — Windows service installer
- `pkg/start.bat` — manual launcher
- **Generated / external** (gitignored):
- `pkg/lmcp.lua`, `pkg/server.lua`, `pkg/json.lua` — produced by
`sync.sh`. Never edit directly; edit the root files and re-sync.
- `pkg/lua/` — the Lua + LuaSocket runtime drop-in. Download
separately and place here. Suggested source: the lua-binaries
project (https://github.com/rjpcomputing/luaforwindows) or a
similar pre-built bundle. The MSI expects `pkg/lua/lua.exe`,
`pkg/lua/lua54.dll`, and the `pkg/lua/socket/` + `pkg/lua/mime/`
subdirectories per the manifest.
## Issue history
Issue #18 (closed in v1.1.0) introduced this workflow after the
`pkg/` lua sources had silently drifted ~6 months out of date,
missing every feature added since April 2026.
-100
View File
@@ -1,100 +0,0 @@
#!/bin/sh
# windows/build-msi.sh — produce lmcp-<ver>.msi on Linux via wixl.
#
# This is the first-time-discovered cross-build path: download Lua 5.4
# Win64 binaries from LuaBinaries, cross-compile LuaSocket with mingw-w64,
# stage windows/pkg/lua/, then invoke wixl on the WiX manifest.
#
# Avoids the VM106-clone + WiX-on-Windows path entirely. ~1 minute on a
# warm cache; ~3-5 minutes cold (downloads ~700 KB + cross-compiles).
#
# Prereqs (apt install on Debian aarch64):
# apt-get install -y wixl unzip gcc-mingw-w64-x86-64 binutils-mingw-w64-x86-64 \
# mingw-w64-x86-64-dev
#
# Usage: ./build-msi.sh [output_dir]
# Output: $output_dir/lmcp-<ver>.msi (default: $PWD)
#
# Version comes from windows/lmcp.wxs Version="…" attribute.
set -eu
here=$(dirname "$(readlink -f "$0")")
root=$(cd "$here/.." && pwd)
out_dir=${1:-$PWD}
work=$(mktemp -d /tmp/lmcp-msi-XXXXXX)
trap "rm -rf $work" EXIT
# Versions — bump as upstream releases.
LUA_VER=5.4.2
LUASOCKET_VER=3.1.0
# Pull current lmcp version from the WiX manifest.
lmcp_ver=$(sed -n 's/.*Version="\([^"]*\)".*/\1/p' "$here/lmcp.wxs" | head -1)
[ -n "$lmcp_ver" ] || { echo "build-msi.sh: cannot parse Version from lmcp.wxs" >&2; exit 1; }
echo "build-msi.sh: lmcp $lmcp_ver, lua $LUA_VER, luasocket $LUASOCKET_VER"
echo "==> 1/5 sync lmcp .lua sources into pkg/"
"$here/sync.sh"
echo "==> 2/5 fetch lua $LUA_VER win64 binaries + dev library"
cd "$work"
curl -sSLf -o lua-bin.zip \
"https://downloads.sourceforge.net/project/luabinaries/${LUA_VER}/Tools%20Executables/lua-${LUA_VER}_Win64_bin.zip"
curl -sSLf -o lua-lib.zip \
"https://downloads.sourceforge.net/project/luabinaries/${LUA_VER}/Windows%20Libraries/Dynamic/lua-${LUA_VER}_Win64_dllw6_lib.zip"
mkdir -p luabin lualib include/lua/54 include/lua54 bin/lua/54 bin/lua54 lib/lua/54 lib/lua54
unzip -q -o lua-bin.zip -d luabin
unzip -q -o lua-lib.zip -d lualib
cp lualib/include/*.h include/lua/54/
cp lualib/include/*.h include/lua54/
cp lualib/liblua54.a lib/lua/54/
cp lualib/liblua54.a lib/lua54/
cp lualib/lua54.dll bin/lua/54/
cp lualib/lua54.dll bin/lua54/
echo "==> 3/5 cross-compile LuaSocket $LUASOCKET_VER for win64"
curl -sSLf -o luasocket.tar.gz \
"https://github.com/lunarmodules/luasocket/archive/refs/tags/v${LUASOCKET_VER}.tar.gz"
tar xzf luasocket.tar.gz
cd "luasocket-${LUASOCKET_VER}"
make -s PLAT=mingw \
CC=x86_64-w64-mingw32-gcc \
LD=x86_64-w64-mingw32-gcc \
LUAV=54 \
LUAINC_mingw_base="$work/include" \
LUALIB_mingw_base="$work/bin" \
> /dev/null
echo "==> 4/5 stage pkg/lua/"
pkg_lua="$here/pkg/lua"
rm -rf "$pkg_lua"
mkdir -p "$pkg_lua/socket" "$pkg_lua/mime"
# WiX manifest expects "lua.exe" (not "lua54.exe").
cp "$work/luabin/lua54.exe" "$pkg_lua/lua.exe"
cp "$work/luabin/lua54.dll" "$pkg_lua/lua54.dll"
cp src/socket.lua "$pkg_lua/"
cp src/mime.lua "$pkg_lua/"
cp src/ltn12.lua "$pkg_lua/"
cp src/socket-3.0.0.dll "$pkg_lua/socket/core.dll"
cp src/ftp.lua "$pkg_lua/socket/"
cp src/headers.lua "$pkg_lua/socket/"
cp src/http.lua "$pkg_lua/socket/"
cp src/smtp.lua "$pkg_lua/socket/"
cp src/tp.lua "$pkg_lua/socket/"
cp src/url.lua "$pkg_lua/socket/"
cp src/mime-1.0.3.dll "$pkg_lua/mime/core.dll"
echo "==> 5/5 wixl: produce MSI"
# wixl wants forward slashes; rewrite Windows-style backslashes in Source=.
wxs_tmp="$work/lmcp.wxs"
sed 's|Source="pkg\\|Source="pkg/|g; s|\\\([a-zA-Z]\)|/\1|g' "$here/lmcp.wxs" > "$wxs_tmp"
mkdir -p "$out_dir"
out_msi="$out_dir/lmcp-${lmcp_ver}.msi"
(cd "$here" && wixl -v "$wxs_tmp" -o "$out_msi")
echo ""
echo "==> done: $out_msi"
ls -la "$out_msi"
sha256sum "$out_msi"
-119
View File
@@ -1,119 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<Wix xmlns="http://schemas.microsoft.com/wix/2006/wi">
<!-- Bump Version on every release. See windows/README.md. -->
<Product Id="*"
Name="lmcp — Lua MCP Server"
Language="1033"
Version="1.1.0"
Manufacturer="QAP'LA Project"
UpgradeCode="A7F3E2D1-4B5C-6D7E-8F9A-0B1C2D3E4F5A">
<Package InstallerVersion="200"
Compressed="yes"
InstallScope="perMachine"
Description="Lightweight MCP server in Lua. 2MB RSS."
Comments="Zero-dependency MCP server." />
<MediaTemplate EmbedCab="yes" />
<MajorUpgrade DowngradeErrorMessage="A newer version is already installed." />
<Directory Id="TARGETDIR" Name="SourceDir">
<Directory Id="ProgramFiles64Folder">
<Directory Id="INSTALLFOLDER" Name="lmcp">
<Directory Id="LUA_DIR" Name="lua">
<Directory Id="SOCKET_DIR" Name="socket" />
<Directory Id="MIME_DIR" Name="mime" />
</Directory>
</Directory>
</Directory>
</Directory>
<!-- lmcp application files -->
<DirectoryRef Id="INSTALLFOLDER">
<Component Id="JsonLua" Guid="B1A2C3D4-E5F6-7890-ABCD-EF1234567890">
<File Id="json.lua" Source="pkg\json.lua" KeyPath="yes" />
</Component>
<Component Id="LmcpLua" Guid="B1A2C3D4-E5F6-7890-ABCD-EF1234567891">
<File Id="lmcp.lua" Source="pkg\lmcp.lua" KeyPath="yes" />
</Component>
<Component Id="ServerLua" Guid="B1A2C3D4-E5F6-7890-ABCD-EF1234567892">
<File Id="server.lua" Source="pkg\server.lua" KeyPath="yes" />
</Component>
<Component Id="StartBat" Guid="B1A2C3D4-E5F6-7890-ABCD-EF1234567893">
<File Id="start.bat" Source="pkg\start.bat" KeyPath="yes" />
</Component>
<Component Id="InstallService" Guid="B1A2C3D4-E5F6-7890-ABCD-EF1234567894">
<File Id="install_service.bat" Source="pkg\install_service.bat" KeyPath="yes" />
</Component>
</DirectoryRef>
<!-- Lua runtime -->
<DirectoryRef Id="LUA_DIR">
<Component Id="LuaExe" Guid="C2B3D4E5-F6A7-8901-BCDE-F12345678900">
<File Id="lua.exe" Source="pkg\lua\lua.exe" KeyPath="yes" />
</Component>
<Component Id="LuaDll" Guid="C2B3D4E5-F6A7-8901-BCDE-F12345678901">
<File Id="lua54.dll" Source="pkg\lua\lua54.dll" KeyPath="yes" />
</Component>
<Component Id="SocketLua" Guid="C2B3D4E5-F6A7-8901-BCDE-F12345678902">
<File Id="socket.lua" Source="pkg\lua\socket.lua" KeyPath="yes" />
</Component>
<Component Id="MimeLua" Guid="C2B3D4E5-F6A7-8901-BCDE-F12345678903">
<File Id="mime.lua" Source="pkg\lua\mime.lua" KeyPath="yes" />
</Component>
<Component Id="Ltn12Lua" Guid="C2B3D4E5-F6A7-8901-BCDE-F12345678904">
<File Id="ltn12.lua" Source="pkg\lua\ltn12.lua" KeyPath="yes" />
</Component>
</DirectoryRef>
<!-- LuaSocket native DLLs -->
<DirectoryRef Id="SOCKET_DIR">
<Component Id="SocketCoreDll" Guid="D3C4E5F6-A7B8-9012-CDEF-123456789010">
<File Id="socket_core.dll" Name="core.dll" Source="pkg\lua\socket\core.dll" KeyPath="yes" />
</Component>
<Component Id="SocketFtp" Guid="D3C4E5F6-A7B8-9012-CDEF-123456789011">
<File Id="ftp.lua" Source="pkg\lua\socket\ftp.lua" KeyPath="yes" />
</Component>
<Component Id="SocketHeaders" Guid="D3C4E5F6-A7B8-9012-CDEF-123456789012">
<File Id="headers.lua" Source="pkg\lua\socket\headers.lua" KeyPath="yes" />
</Component>
<Component Id="SocketHttp" Guid="D3C4E5F6-A7B8-9012-CDEF-123456789013">
<File Id="http.lua" Source="pkg\lua\socket\http.lua" KeyPath="yes" />
</Component>
<Component Id="SocketTp" Guid="D3C4E5F6-A7B8-9012-CDEF-123456789014">
<File Id="tp.lua" Source="pkg\lua\socket\tp.lua" KeyPath="yes" />
</Component>
<Component Id="SocketUrl" Guid="D3C4E5F6-A7B8-9012-CDEF-123456789015">
<File Id="url.lua" Source="pkg\lua\socket\url.lua" KeyPath="yes" />
</Component>
</DirectoryRef>
<DirectoryRef Id="MIME_DIR">
<Component Id="MimeCoreDll" Guid="E4D5F6A7-B8C9-0123-DEFA-234567890120">
<File Id="mime_core.dll" Name="core.dll" Source="pkg\lua\mime\core.dll" KeyPath="yes" />
</Component>
</DirectoryRef>
<Feature Id="MainFeature" Title="lmcp Server" Level="1">
<ComponentRef Id="JsonLua" />
<ComponentRef Id="LmcpLua" />
<ComponentRef Id="ServerLua" />
<ComponentRef Id="StartBat" />
<ComponentRef Id="InstallService" />
<ComponentRef Id="LuaExe" />
<ComponentRef Id="LuaDll" />
<ComponentRef Id="SocketLua" />
<ComponentRef Id="MimeLua" />
<ComponentRef Id="Ltn12Lua" />
<ComponentRef Id="SocketCoreDll" />
<ComponentRef Id="SocketFtp" />
<ComponentRef Id="SocketHeaders" />
<ComponentRef Id="SocketHttp" />
<ComponentRef Id="SocketTp" />
<ComponentRef Id="SocketUrl" />
<ComponentRef Id="MimeCoreDll" />
</Feature>
</Product>
</Wix>
-24
View File
@@ -1,24 +0,0 @@
@echo off
REM Install lmcp as a Windows service using NSSM (Non-Sucking Service Manager)
REM Download nssm from https://nssm.cc if not present
if not exist "%~dp0nssm.exe" (
echo ERROR: nssm.exe not found in %~dp0
echo Download from https://nssm.cc and place nssm.exe here.
exit /b 1
)
set INSTALL_DIR=%~dp0
set SERVICE_NAME=lmcp
echo Installing lmcp as Windows service...
%INSTALL_DIR%nssm.exe install %SERVICE_NAME% "%INSTALL_DIR%lua\lua.exe" "%INSTALL_DIR%server.lua"
%INSTALL_DIR%nssm.exe set %SERVICE_NAME% AppDirectory "%INSTALL_DIR%"
%INSTALL_DIR%nssm.exe set %SERVICE_NAME% AppEnvironmentExtra "LMCP_PORT=8080"
%INSTALL_DIR%nssm.exe set %SERVICE_NAME% DisplayName "lmcp MCP Server"
%INSTALL_DIR%nssm.exe set %SERVICE_NAME% Description "Lightweight MCP server in Lua"
%INSTALL_DIR%nssm.exe set %SERVICE_NAME% Start SERVICE_AUTO_START
%INSTALL_DIR%nssm.exe start %SERVICE_NAME%
echo Done. Service '%SERVICE_NAME%' installed and started.
echo Check: sc query %SERVICE_NAME%
-7
View File
@@ -1,7 +0,0 @@
@echo off
REM lmcp — Lua MCP Server
REM Start the server on port 8080 (or LMCP_PORT if set)
cd /d "%~dp0"
if not defined LMCP_PORT set LMCP_PORT=8080
echo Starting lmcp on port %LMCP_PORT%...
lua\lua.exe server.lua
-25
View File
@@ -1,25 +0,0 @@
#!/bin/sh
# windows/sync.sh — refresh windows/pkg/ from root .lua sources (issue #18).
#
# Run BEFORE invoking the WiX build so the MSI bundles whatever is in
# master. The .lua files in windows/pkg/ are regenerated on every run
# and are gitignored — never edit them directly.
#
# Idempotent: re-running just re-copies. Safe to call from a Makefile,
# a CI step, or by hand before `candle.exe + light.exe`.
set -eu
here=$(dirname "$(readlink -f "$0")")
root=$(cd "$here/.." && pwd)
for f in lmcp.lua server.lua json.lua; do
if [ ! -f "$root/$f" ]; then
echo "windows/sync.sh: missing source $root/$f" >&2
exit 1
fi
cp "$root/$f" "$here/pkg/$f"
echo " synced $f"
done
echo "windows/sync.sh: done — pkg/ matches root .lua at $(date +%Y-%m-%dT%H:%M:%S)"