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

@@ -1,20 +1,30 @@
# Coolify production stack — self-contained: qdrant + ollama + ingestor.
# Coolify production stack — self-contained: qdrant + ollama + ingestor + rag-mcp.
#
# Services kommunizieren nur über das compose-interne Netz. Nichts wird
# publiziert außer dem Ingestor: in der Coolify-UI eine Domain auf den
# Service "ingestor" / Port 8000 mappen, damit Nextcloud den Webhook
# erreicht. qdrant + ollama bleiben bewusst nicht öffentlich.
# Services kommunizieren nur über das compose-interne Netz. Öffentlich ist
# allein der ingestor (Coolify-Domain → Port 8000) für den Nextcloud-Webhook.
# qdrant + ollama bleiben intern. rag-mcp ist NICHT public (keine Domain) —
# nur MetaMCP erreicht ihn über das dedizierte externe Netz "metamcp-net".
#
# Coolify rendert container_name/labels/COOLIFY_*-env/das Stack-Netz selbst.
# Hier stehen nur Extra-Labels und das, was zusätzlich nötig ist.
#
# Voraussetzungen in Coolify:
# - Pull-Credentials für gitea.jeanlucmakiola.de Registry hinterlegen
# - Folgende Env-Vars setzen (Secrets wo markiert):
# - Externes Netz einmalig anlegen: docker network create metamcp-net
# und es in der MetaMCP-Compose ergänzen (networks: [metamcp-net])
# - Env-Vars setzen (Secrets wo markiert):
# NEXTCLOUD_WEBDAV_URL
# NEXTCLOUD_USER
# NEXTCLOUD_APP_PASSWORD (Secret)
# WEBHOOK_SECRET (Secret)
# RAG_MCP_TOKEN (Secret — Bearer-Token für MetaMCP)
# QDRANT_COLLECTION (optional, Default rag_thb_studium)
# INGEST_ROOT (optional, Default Documents/THB)
# LOG_LEVEL (optional, Default INFO)
# - Traefik-Rate-Limit am ingestor-Router aktivieren: siehe README
# ("MetaMCP & Härtung") — das Router-Binding-Label trägt die
# env-spezifische Coolify-UUID und gehört daher in die Coolify-UI,
# nicht ins versionierte Compose.
services:
qdrant:
@@ -79,6 +89,13 @@ services:
WEBHOOK_SECRET: ${WEBHOOK_SECRET}
INGEST_ROOT: ${INGEST_ROOT:-Documents/THB}
LOG_LEVEL: ${LOG_LEVEL:-INFO}
# Extra-Label: definiert eine Traefik-Rate-Limit-Middleware (30 req/min,
# Burst 15) gegen Spam des öffentlichen Webhooks. Das Binding an den
# Coolify-Router (env-spezifische UUID) erfolgt in der Coolify-UI, s. README.
labels:
- "traefik.http.middlewares.rag-ratelimit.ratelimit.average=30"
- "traefik.http.middlewares.rag-ratelimit.ratelimit.period=1m"
- "traefik.http.middlewares.rag-ratelimit.ratelimit.burst=15"
expose:
- "8000"
healthcheck:
@@ -91,6 +108,48 @@ services:
timeout: 5s
retries: 5
# MCP-Server für MetaMCP. Gleiches Image wie der ingestor (teilt den
# Embed-Pfad → identische Vektoren wie beim Ingest), nur anderer command.
# KEIN SERVICE_FQDN / kein expose → nicht public. Erreichbar ausschließlich
# von MetaMCP über das externe Netz metamcp-net. Bearer-Token als zweite
# Kontrolle hinter der Netz-Isolation.
rag-mcp:
image: gitea.jeanlucmakiola.de/makiolaj/rag-ingestor:latest
restart: unless-stopped
pull_policy: always
command: ["python", "-m", "app.mcp_server"]
depends_on:
qdrant:
condition: service_started
ollama:
condition: service_healthy
ollama-pull:
condition: service_completed_successfully
environment:
OLLAMA_URL: http://ollama:11434
OLLAMA_EMBED_MODEL: qwen3-embedding:0.6b
QDRANT_URL: http://qdrant:6333
QDRANT_COLLECTION: ${QDRANT_COLLECTION:-rag_thb_studium}
RAG_MCP_TOKEN: ${RAG_MCP_TOKEN}
LOG_LEVEL: ${LOG_LEVEL:-INFO}
# Vom MCP-Server ungenutzt, aber das geteilte Settings-Modell verlangt
# diese Felder — harmlose Platzhalter, damit die Validierung durchläuft.
NEXTCLOUD_WEBDAV_URL: http://unused
NEXTCLOUD_USER: unused
NEXTCLOUD_APP_PASSWORD: unused
WEBHOOK_SECRET: unused
networks:
- default
- metamcp-net
volumes:
qdrant_data:
ollama_data:
networks:
# Coolify injiziert sein Stack-Netz als "default" automatisch. metamcp-net
# ist ein eigens angelegtes externes Netz, dem nur rag-mcp und MetaMCP
# beitreten — qdrant/ollama/ingestor bleiben außen vor.
metamcp-net:
external: true
name: metamcp-net