Phase 1 (English-only) ABGESCHLOSSEN: 27/27, Smoke 6/6, attest GRUEN

This commit is contained in:
deus
2026-08-10 09:04:57 +00:00
parent b2185eeaa0
commit 89fe21c424
12 changed files with 4001 additions and 0 deletions
+48
View File
@@ -0,0 +1,48 @@
# 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
+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.
+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.
+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()
File diff suppressed because it is too large Load Diff
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