diff --git a/README.md b/README.md index 5f478cc..552dc50 100644 --- a/README.md +++ b/README.md @@ -33,7 +33,7 @@ mit der kompletten Provider-Kette und Daemon-Anbindung (non-blocking über `talkd`, siehe unten), `mute`, `unmute`, `status`, `cancel`. Die übrigen Befehle sind Stubs und melden `not implemented yet` (Exit-Code ≠ 0). -## Daemon talkd (Task 04 + 05) +## Daemon talkd (Task 04–06) `talkd` besitzt die Ansage-Queue und spricht seriell ab — genau eine Ansage gleichzeitig, Reihenfolge `now` > `high` > `normal` > `low` (FIFO je @@ -43,9 +43,12 @@ config `[server] port`, Env `TALK_PORT` (schlagen den Default). | Endpunkt | Wirkung | | --- | --- | | `POST /speak` | `{text, priority?, provider?, voice?, lang?, now?}` → `202 {"id":"…"}`; bei Mute: `202 {"id":"","muted":true}`; bei Gesetz-Verwurf: `202 {"id":"","discarded":"rate_limit"\|"quiet_hours"}` (+ `"toast":true` im Toast-Modus) — verworfen, dokumentiertes Verhalten | -| `GET /status` | `{"version", "queue", "queue_ids", "current", "muted", "provider", "rate_limit": {"interval", "allow_high_burst", "next_slot"}, "quiet_hours": {"window", "active", "mode", "until"}}` | +| `OPTIONS /speak` | CORS-Preflight → `204` mit Freigabe-Headern, wenn die `Origin` in `server.allowed_origins` steht (siehe unten); sonst `204` ohne CORS-Header | +| `GET /status` | `{"version", "queue", "queue_ids", "current", "muted", "provider", "rate_limit": {"interval", "allow_high_burst", "next_slot"}, "quiet_hours": {"window", "active", "mode", "until"}, "mcp": true, "cors": [origins]}` | | `POST /cancel` | bricht die laufende Ansage ab + leert die Queue → `200 {"cancelled":n}` | | `POST /mute` / `POST /unmute` | persistenter Mute-State → `200 {"muted":bool}` | +| `POST /mcp` | MCP (Streamable HTTP): JSON-RPC `initialize`, `notifications/initialized`, `ping`, `tools/list`, `tools/call` — ein Tool `speak` (siehe unten) | +| `GET /mcp` | `405` — talkd bietet keine server-initiierten Streams an | - **CLI-Verhalten:** `talk say` (und `talk "Text"`) erkennt den Daemon via `GET /status`. Läuft er: enqueue + sofortige Rückkehr (non-blocking, @@ -105,6 +108,62 @@ quiet_mode = "discard" # discard | toast (TALK_QUIET_MODE) erlaubt), nächster slot …` und `ruhezeit: 18:00-08:00 (modus discard, aktiv bis 08:00)`. +### CORS-Tür + MCP-Endpoint (Task 06) + +**CORS** gilt bewusst nur für `POST /speak` — genau die eine Tür, die eine +spätere Browser-Anbindung (z. B. LibreChat) braucht; Steuer-Endpunkte +bleiben browserseitig unerreicht. Die Erlaubnis ist eine exakte Origin- +Liste; `"*"` ist erlaubt, aber nicht Default; **leere Liste (Default) = +gar keine CORS-Header = der Browser blockt**: + +```toml +[server] +allowed_origins = ["https://chat.az-gruppe.com"] # Env TALK_ALLOWED_ORIGINS +``` + +- `OPTIONS /speak` (Preflight) von gelisteter Origin → `204` mit + `Access-Control-Allow-Origin` (die Origin bzw. `*`), `-Methods: POST, + OPTIONS`, `-Headers: Content-Type`, `Max-Age: 600`. +- Echter `POST /speak` mit gelisteter `Origin` trägt denselben ACAO-Header. +- Ungelistete Origins bekommen saubere `204`/`202` **ohne** CORS-Header — + die Blockade macht der Browser. + +**MCP** (`POST /mcp`, Streamable HTTP): schlanke eigene Implementierung +des JSON-RPC-Subsets ohne SDK — `initialize` (angefragte +`protocolVersion` wird geechot; Ergebnis `protocolVersion`, +`capabilities: {tools:{}}`, `serverInfo`), `notifications/initialized` +(Notification → `202` ohne Body), `ping`, `tools/list`, +`tools/call`. Der Server ist zustandslos (keine Session-IDs) und pusht +nichts (daher `GET /mcp` → `405`). Ein Tool: + +```json +{"name": "speak", + "inputSchema": {"type": "object", + "properties": {"text": {"type": "string"}, + "priority": {"enum": ["low","normal","high"]}, + "provider": {"type": "string"}, + "lang": {"enum": ["de","en"]}, + "now": {"type": "boolean"}}, + "required": ["text"]}} +``` + +`tools/call` fährt exakt denselben Aufnahme-Pfad wie `POST /speak` +(inklusive Mute, Rate-Limit, Ruhezeiten). Ergebnis ist immer Text-Content: +`"queued: "` bei Aufnahme; `muted`/`discarded: ` mit Erklärung +— bewusst `isError: false`, denn ein Verwurf ist das korrekte +dokumentierte Ergebnis, kein Protokollfehler. Validierungsfehler +(fehlender Text, ungültige Priorität, unbekanntes Tool) sind dagegen +JSON-RPC-Fehler (`-32602`). Handshake als curl-Sequenz: + +```sh +curl -s http://127.0.0.1:7800/mcp -H 'Content-Type: application/json' \ + -d '{"jsonrpc":"2.0","id":1,"method":"initialize","params":{"protocolVersion":"2025-06-18","capabilities":{},"clientInfo":{"name":"curl","version":"0"}}}' +curl -s http://127.0.0.1:7800/mcp -H 'Content-Type: application/json' \ + -d '{"jsonrpc":"2.0","method":"notifications/initialized"}' +curl -s http://127.0.0.1:7800/mcp -H 'Content-Type: application/json' \ + -d '{"jsonrpc":"2.0","id":2,"method":"tools/call","params":{"name":"speak","arguments":{"text":"Build fertig"}}}' +``` + ### Provider-Kette (Task 03) Ohne Pin läuft die Vier-Stufen-Fallback-Kette