Phase 1 (English-only) ABGESCHLOSSEN: 27/27, Smoke 6/6, attest GRUEN
This commit is contained in:
@@ -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
|
||||
@@ -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 1–5 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.
|
||||
@@ -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 1–5 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
|
||||
@@ -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.
|
||||
@@ -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.
|
||||
@@ -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.
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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
@@ -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
|
||||
Reference in New Issue
Block a user