feat: MCP-Server für RAG-Retrieval + Webhook-Härtung
All checks were successful
CI / ci (push) Successful in 49s
Release / release (push) Successful in 1m2s

app/mcp_server.py: FastMCP (mcp SDK), streamable-http auf /mcp, statischer
Bearer-Token (constant-time ASGI-Middleware), Fail-Fast ohne RAG_MCP_TOKEN.
Tools rag_search (mit semester/fach/typ-Filter) + get_file_chunks. Läuft aus
demselben Image wie der Ingestor und reused den Embed-Pfad → Vektoren sind
garantiert kompatibel zum Ingest (der offizielle qdrant-MCP-Server kann nur
fastembed → Dimension-/Schema-Mismatch).

app/qdrant_store.py: search_chunks (query_points + optionaler Payload-Filter)
und get_chunks_by_path (scroll, nach chunk_index sortiert).

app/bulk.py: Amplification-Guard — /bulk-import lehnt mit 409 ab solange ein
vorheriger Bulk noch BackgroundTasks abarbeitet.

docker-compose.coolify.yml: rag-mcp-Service (nicht public, externes
metamcp-net statt Stack-Coupling) + Traefik-Rate-Limit-Middleware am ingestor.

tests/conftest.py: Settings-env_file in Tests neutralisieren (Dev-.env darf
die Suite nicht kontaminieren). 68 passed, ruff clean.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-05-18 22:08:37 +02:00
parent a6a2175f8b
commit 9643011e64
12 changed files with 935 additions and 8 deletions

View File

@@ -68,6 +68,46 @@ Embedding-Inferenz ist CPU-only und skaliert per Default auf alle verfügbaren C
Beide Werte sind in `docker-compose.yml` für die lokale Entwicklung gesetzt und sollten in Coolify entsprechend mitgepflegt werden. Folge: konstante ~2 CPU statt Peaks bis 8 CPU, dafür längere Bulk-Laufzeiten.
## MCP-Server (Retrieval via MetaMCP)
`app/mcp_server.py` exponiert das indexierte Wissen als MCP-Server (streamable-http, Endpoint `/mcp`). Er läuft aus **demselben Image** wie der Ingestor und nutzt denselben Embed-Pfad (Ollama `qwen3-embedding:0.6b`) — Query-Vektoren sind damit garantiert kompatibel zu den beim Ingest geschriebenen 1024-Dim-Vektoren. Der offizielle `qdrant/mcp-server-qdrant` ist *nicht* nutzbar: er kann nur fastembed (Dimension-/Modell-Mismatch), kennt unser Payload-Schema nicht und bietet kein Metadata-Filtering.
Tools:
- `rag_search(query, limit=5, semester?, fach?, typ?)` — semantische Suche mit optionalen Payload-Filtern; liefert `text` + Quell-Metadaten + `score`.
- `get_file_chunks(file_path)` — alle Chunks eines Dokuments in `chunk_index`-Reihenfolge.
### Lokal starten
```bash
RAG_MCP_TOKEN=dev-token uv run python -m app.mcp_server
```
Lauscht auf `0.0.0.0:${RAG_MCP_PORT:-9009}`. Ohne `RAG_MCP_TOKEN` verweigert der Server den Start (Fail-Fast). Jeder Request braucht `Authorization: Bearer <RAG_MCP_TOKEN>`.
### Coolify + MetaMCP
Der `rag-mcp`-Service in `docker-compose.coolify.yml` ist bewusst **nicht public** (keine Domain / kein `expose`). Erreichbar ist er nur über das dedizierte externe Netz `metamcp-net` — qdrant/ollama/ingestor bleiben außen vor (kein Shared-Network-Coupling des ganzen Stacks).
1. Netz einmalig anlegen: `docker network create metamcp-net`
2. In der MetaMCP-Compose `networks: [metamcp-net]` ergänzen, damit MetaMCP demselben Netz beitritt.
3. `RAG_MCP_TOKEN` als Secret in Coolify setzen (Wert aus `.env.coolify`).
4. In MetaMCP einen streamable-http-Downstream registrieren: URL `http://rag-mcp:9009/mcp`, Header `Authorization: Bearer <RAG_MCP_TOKEN>`.
Externe Kontrolle/Sharing macht MetaMCP als Single-Endpoint; der Token + die Netz-Isolation sind die zwei Kontrollschichten auf dem internen Hop.
### Webhook-Härtung
- **Rate-Limit:** `docker-compose.coolify.yml` definiert am `ingestor` eine Traefik-Middleware `rag-ratelimit` (30 req/min, Burst 15). Das *Binding* an den Coolify-generierten Router trägt eine env-spezifische UUID und gehört daher **nicht** ins Repo, sondern als Custom-Label in die Coolify-UI des ingestor-Service:
```
traefik.http.routers.https-0-<COOLIFY-UUID>-ingestor.middlewares=gzip,rag-ratelimit
```
(UUID aus der von Coolify gerenderten Compose entnehmen; bestehende `gzip`-Middleware beibehalten.)
- **Bulk-Amplification-Guard:** `/bulk-import` lehnt mit `409` ab, solange ein vorheriger Bulk noch Tasks abarbeitet — verhindert unbegrenztes Anhäufen von BackgroundTasks durch wiederholte Calls oder einen fehlfeuernden Nextcloud-Flow.
- CORS ist bewusst nicht konfiguriert: der Webhook wird Server-zu-Server gerufen, CORS ist kein Spam-Schutz und für Browser-Clients hier irrelevant.
## Tests
```bash