9.1 KiB
9.1 KiB
Task 6 Report: CORS-Tür + MCP-Endpoint
Status: DONE
Datum: 2026-10-18
Basis: 24ceff3 (Tasks 1–5 verifiziert) → HEAD 30bd535
Commits (feat/v1, kein Push, .scratch/ unberührt):
cef6e6bfeat(config): CORS-Erlaubnisliste server.allowed_origins (TALK_ALLOWED_ORIGINS)156c20dfeat(api): CORS-Tür für POST /speak — OPTIONS-Preflight und ACAO aus Configa577597feat(api): MCP-Endpoint POST /mcp — schlankes eigenes JSON-RPC-Subset mit speak-Tool150c374test: Prozessgrenzen-Tests für CORS-Tür und MCP-Handshake30bd535docs(readme): CORS-Konfiguration und MCP-Endpoint mit curl-Handshake dokumentiert
Was geliefert wurde
1. CORS-Tür (nur POST /speak)
internal/api/cors.go(neu):OPTIONS /speakbeantwortet Browser-Preflights mit204 No Content+Access-Control-Allow-Origin(exakt gematchte Origin bzw. literales*bei Wildcard-Konfiguration),Access-Control-Allow-Methods: POST, OPTIONS,Access-Control-Allow-Headers: Content-Type,Access-Control-Max-Age: 600. Echte (non-preflight)POST /speak-Antworten tragen ACAO, wenn die Anfrage-Origin gelistet ist.Vary: Originsteht immer auf /speak (Cache-Korrektheit).- Ungelistete Origin: sauberer 204/202 ohne jegliche CORS-Header — die Blockade macht der Browser; HTTP bleibt für Nicht-Browser-Clients sauber erreichbar.
- Leere Liste (Default): gar keine CORS-Header — Tür zu, bis konfiguriert.
- Nur /speak:
OPTIONS /status→ 405 (kein Preflight-Support),GET /statusohne ACAO; kein anderer Endpunkt sendet CORS-Header (Prozessgrenzen-TestTestCORSNurAufSpeak). internal/config:server.allowed_origins(TOML-Liste) + Env-OverrideTALK_ALLOWED_ORIGINS(kommagetrennt, Whitespace toleriert;offleert die Liste). Wildcard*ist explizit erlaubt, NICHT Default. Leere Einträge → klarer Fehler. Unit-Tests:TestLoadAllowedOrigins(Defaults, TOML, Env schlägt Datei, off, Wildcard, Fehlerfälle).GET /status: additives Feldcors: [origins]— immer vorhanden,[](nichtnull) bei geschlossener Tür (Compat-Anforderung des Briefs; TestTestStatusCorsFeld).
2. MCP-Endpoint (Streamable HTTP)
internal/api/mcp.go(neu):POST /mcp— eigene schlanke Implementierung des JSON-RPC-2.0-Subsets, kein SDK-Dependency (bindende Decision des Briefs; es wurde auch keins in Betracht gezogen — der benötigte Umfang sind ~370 Zeilen inkl. Doku, ein SDK wäre schwerer als der Payload). Methoden:initialize,notifications/initialized,ping,tools/list,tools/call.- Ein Tool
speak: Input-Schema exakt nach Brief —{text: string, priority?, provider?, lang?, now?},required: ["text"], enums für priority/lang (Test prüft Exactly-those-5-Properties). tools/call=POST /speak: Beide Türen fahren denselben Aufnahme-PfadadmitSpeak(Refactoring ininternal/api/server.go): Validierung → Mute-Check → Enqueue inkl. Gesetz (Rate-Limit-Budget, Ruhezeit; Rangordnung mute > now > quiet > rate aus Task 05 bleibt unverändert ininternal/queue). Kein Contract-Drift möglich.- Ergebnis-Texte: Erfolg →
queued: <id>; Mute →muted: Ansage verworfen (Mute ist aktiv); Verwurf →discarded: rate_limit …/discarded: quiet_hours …(+ Toast-Hinweis). BewusstisError: falsebei muted/discarded (deliberate Decision laut Brief: dokumentiertes Aufnahme-Ergebnis, kein Protokollfehler — der Agent soll den Grund lesen, nicht crashen). GET /status: additives Feldmcp: true(TestTestStatusMCPFeld).- Daemon-Log-Zeile nennt jetzt cors-Liste und mcp.
MCP-Protokoll-Entscheidungen (dokumentationspflichtig)
- Version-Handling:
initializeechot die angefragteprotocolVersion(akzeptiert sie); fehlt sie, antwortet talkd mit2025-06-18. Inhaltlich implementiert ist der 2025-06-18-Umfang: Initialize-Ergebnis mitprotocolVersion,capabilities: {tools: {}},serverInfo: {name: "talkd", version: <daemon-version>}; Streamable HTTP auf einem Endpoint; keine Batches (seit 2025-06-18 entfernt →-32600mit klarer Meldung). Getestet: Echo von2025-06-18und2025-03-26. - GET /mcp: talkd pusht nie (statische Tool-Liste, keine Server-Notifications,
kein Sampling) — server-initiierte SSE-Streams entfallen daher komplett.
GET /mcpantwortet405 Method Not AllowedmitAllow: POST, OPTIONS— exakt die im Streamable-HTTP-Transport vorgesehene Antwort für Server ohne Server-Streams.DELETE /mcp(Session-Terminierung) fällt auf den auto-405 des Routers. - Content-Types: Antworten auf Requests:
200 OK+application/json— auch für JSON-RPC-Fehlerobjekte (der HTTP-Teil war erfolgreich; getestet inTestMCPFehlerPfade). Notifications:202 Acceptedmit leerem Body (JSON-RPC 2.0: nie eine Antwort). Requests: der Daemon erzwingt keinContent-Type(curl-freundlich, strikter wäre SDK-Komfort ohne Vertragsgewinn). - Zustandslos, keine Session-Enforcement: Jeder POST ist unabhängig;
Mcp-Session-Idwird ignoriert (nie gesetzt), und die Initialize-vor-allem-Regel wird NICHT erzwungen — ein curl kanntools/calldirekt rufen. MCP-Clients fahren den Handshake freiwillig; Tests fahren ihn als volle Sequenz. - JSON-RPC-Subset-Details: Notification =
idfehlt oder ist explizitnull(→ 202; unbekannte Notifications werden still ignoriert). Fehlercodes: kaputtes JSON-32700, falsche jsonrpc-Version/Batch-32600, unbekannte Methode-32601, Validierung/unbekanntes Tool-32602, interner Fehler (enqueue, HTTP-Äquivalent 503)-32603. HTTP-400-Validierung mapped also auf-32602+ deutscher Klartext (identische Meldungen wie POST /speak).idwird roh geechot (number/string/ null-sicher viajson.RawMessage).ping→ leeres Result (billig, hilft Clients). Body-Limit 64 KiB wie /speak. - Kein CORS auf /mcp (bindend: CORS nur auf POST /speak) — MCP-Clients sind keine Browser. Sollte später ein Browser-MCP-Client entstehen, muss die Tür explizit erweitert werden (bewusste Grenze, Spec: „LibreChat-Frontend-Anbindung out of scope").
Tests (Prozessgrenze, primäre Seam — harness erweitert, nicht ersetzt)
test/harness_test.go: statusBody +mcp/cors; neue Helfer doRequest (rohe
HTTP-Requests mit Headern), preflight, mcpPost (literale JSON-RPC-Payloads, kein
Client-SDK), mcpDecode.
test/cors_test.go: Preflight von gelisteter Origin → 204 + alle Header (Abnahmekriterium); ungeliste Origin → kein ACAO; echter POST mit/ohne gelistete Origin; leere Liste → gar keine CORS-Header (Default); Wildcard*→ ACAO*; CORS nur auf /speak (OPTIONS /status 405, GET /status ohne ACAO);cors-Feld in /status (gelistet +[]bei leerer Liste).test/mcp_test.go: kompletter Handshake als literal JSON-RPC über HTTP — initialize (Version-Echo 2025-06-18 und 2025-03-26, capabilities.tools, serverInfo) → notifications/initialized (202, leerer Body) → tools/list (genau speak, Schema mit required=[text], genau die 5 Properties) → tools/call (queued: <id>, isError falsch, Ansage am Fake-Provider sichtbar = echte Queue-Aufnahme, Abnahmekriterium). Gesetze: muted → isError:false + „muted …", keine Synthese; rate_limit (30 s) → zweite low verworfen als „discarded: rate_limit …"; quiet_hours → „discarded: quiet_hours …". Fehlerpfade: text fehlt/leer, bad priority, unbekanntes Tool, unbekannte Methode, kaputtes JSON, falsche jsonrpc-Version, Batch — je korrekter Code + Meldungsanteil, Status 200; ping; GET /mcp → 405 + Allow; unbekannte Notification still 202. Alle Daemons auf ephemeralen Ports (TALK_PORT), isoliertes HOME/XDG, sink=null, Fake-Provider.
Verifikation
gofmt -l .leer;go vet ./...grün;go build ./...grün;go test ./...komplett grün (inkl. aller Tasks 1–5-Tests, keine Regressionen — die CLI dekodiert /status mit den additiven Feldern ohne Anpassung).- Cross-Compile-Smoke (CGO_ENABLED=0):
GOOS=darwin GOARCH=amd64undGOOS=windows GOARCH=amd64fürtalkUNDtalkdgebaut — erfolgreich.
Self-Review (git diff 24ceff3..HEAD)
- Ein Verantwortlicher pro Datei: cors.go (CORS), mcp.go (MCP), server.go (Routing + admitSpeak-Refactoring), config.go (Liste + Env), Tests getrennt nach(cors|mcp).
admitSpeakist die einzige Aufnahme-Stelle — POST /speak-Verhalten unverändert (alle Task-04/05-Prozesstests laufen unverändert grün).- Keine Abnahme-Lücken gegenüber dem Brief: alle vier bindenden Testszenarien (preflight gelistet / ungelistet / leere Liste / *), Handshake + tools/call mit Fake-Beobachtung, /status-Felder, curl-Sequenz in README.
Concerns / Grenzen
notifications/initializedMIT id (falsch benutzter Client) bekommt ein leeres Result statt Fehler (tolerante Wahl, dokumentiert im Code).- CORS-Preflight wird bewusst auch bei fehlendem
Origin-Header mit 204 beantwortet (kein CORS-Kontext, keine Header) — harmlos, dokumentiert. - OS-Toast in Ruhezeit bleibt wie in Task 5 eine Log-Zeile (Notification-Anbindung folgt später) — betrifft MCP-/CORS-Pfad nicht.