298 lines
13 KiB
Markdown
298 lines
13 KiB
Markdown
# Loogle MCP Hub — Architettura e parametri
|
||
|
||
Documento di riferimento per la piattaforma MCP domestica LOOGLE.IT.
|
||
Ultimo aggiornamento: 2026-08-22
|
||
|
||
---
|
||
|
||
## Scopo
|
||
|
||
**Loogle MCP Hub** espone agli assistenti AI (Claude, ChatGPT, Gemini, Cursor) due capacità principali:
|
||
|
||
1. **Archivio contesto** — memoria persistente per progetti e task, isolata per utente
|
||
2. **Knowledge base RAG** — ricerca semantica su documenti Paperless e sui contesti salvati
|
||
|
||
Tutti i dati restano in LAN/NAS; l’accesso esterno avviene solo via HTTPS + OAuth.
|
||
|
||
---
|
||
|
||
## Architettura logica
|
||
|
||
```
|
||
┌─────────────────────────────────────────────────────────────┐
|
||
│ Client AI (ChatGPT / Claude / Gemini / Cursor) │
|
||
└───────────────────────────┬─────────────────────────────────┘
|
||
│ HTTPS + OAuth Bearer
|
||
▼
|
||
┌─────────────────────────────────────────────────────────────┐
|
||
│ NPM VIP 192.168.128.85 → mcp.loogle.it:443 │
|
||
└───────────────────────────┬─────────────────────────────────┘
|
||
│
|
||
▼
|
||
┌─────────────────────────────────────────────────────────────┐
|
||
│ Pi-1 :8700 loogle-mcp (FastAPI) │
|
||
│ ├── /mcp JSON-RPC MCP │
|
||
│ ├── /oauth/* OAuth 2.1 PKCE │
|
||
│ └── /dashboard UI famiglia │
|
||
│ Pi-1 :8700 loogle-mcp-indexer (worker Paperless→vector) │
|
||
└───────┬─────────────────────────────┬───────────────────────┘
|
||
│ │
|
||
▼ ▼
|
||
┌───────────────┐ ┌─────────────────────┐
|
||
│ SQLite │ │ Vector store │
|
||
│ /data/*.db │ │ Qdrant DS920 :6333 │
|
||
│ utenti OAuth │ │ o fallback SQLite │
|
||
└───────────────┘ └─────────────────────┘
|
||
│
|
||
▼
|
||
┌─────────────────────────────────────────────────────────────┐
|
||
│ NFS /mnt/ha-apps/mcp/context/{utente}/projects/... │
|
||
└─────────────────────────────────────────────────────────────┘
|
||
│
|
||
▼
|
||
┌─────────────────────────────────────────────────────────────┐
|
||
│ Paperless docs.loogle.it (sorgente documenti + OCR) │
|
||
└─────────────────────────────────────────────────────────────┘
|
||
```
|
||
|
||
---
|
||
|
||
## Componenti e percorsi
|
||
|
||
| Componente | Host | Porta | Percorso / container |
|
||
|------------|------|-------|----------------------|
|
||
| MCP Gateway | Pi-1 | 8700 | `loogle-mcp` |
|
||
| Indexer | Pi-1 | — | `loogle-mcp-indexer` |
|
||
| Qdrant (primario) | DS920 | 6333 | opzionale, compose extrema |
|
||
| Vector fallback | Pi-1 | — | `/data/vector_fallback.db` |
|
||
| Context NFS | NAS | — | `/mnt/ha-apps/mcp/context/` |
|
||
| DB SQLite | Pi-1 | — | `/home/daniely/docker/loogle-mcp/data/loogle_mcp.db` |
|
||
| Compose locale | Pi-1 | — | `/home/daniely/docker/loogle-mcp/` |
|
||
| Compose failover | Pi-1/Pi-2 | — | `/home/daniely/rete/compose/failover/loogle-mcp-compose.yml` |
|
||
| Runbook | — | — | `/home/daniely/rete/ha/RUNBOOK-mcp.md` |
|
||
|
||
---
|
||
|
||
## URL e endpoint
|
||
|
||
| Servizio | URL |
|
||
|----------|-----|
|
||
| MCP (client AI) | `https://mcp.loogle.it/mcp` |
|
||
| Dashboard web | `https://mcp.loogle.it/dashboard` |
|
||
| Health | `https://mcp.loogle.it/health` |
|
||
| OAuth authorize | `https://mcp.loogle.it/oauth/authorize` |
|
||
| OAuth token | `https://mcp.loogle.it/oauth/token` |
|
||
| Metadata OAuth | `https://mcp.loogle.it/.well-known/oauth-authorization-server` |
|
||
| Metadata risorsa MCP | `https://mcp.loogle.it/.well-known/oauth-protected-resource` |
|
||
|
||
---
|
||
|
||
## DNS
|
||
|
||
| Record | Tipo | Valore | Dove configurato |
|
||
|--------|------|--------|------------------|
|
||
| `mcp.loogle.it` | A | `192.168.128.85` (VIP NPM) | Pi-hole `/etc/pihole/pihole.toml` → hosts |
|
||
| Risoluzione LAN | — | Pi-hole → Unbound → split-horizon | Come `docs.loogle.it`, `pass.loogle.it` |
|
||
|
||
**Script setup:** `/home/daniely/rete/scripts/setup-mcp-dns-npm.sh`
|
||
|
||
**Verifica:**
|
||
```bash
|
||
dig +short mcp.loogle.it @127.0.0.1 # → 192.168.128.85
|
||
curl -sf https://mcp.loogle.it/health
|
||
```
|
||
|
||
---
|
||
|
||
## NPM (reverse proxy)
|
||
|
||
| Parametro | Valore |
|
||
|-----------|--------|
|
||
| Proxy host ID | 30 |
|
||
| Dominio | `mcp.loogle.it` |
|
||
| Upstream normale | `192.168.128.80:8700` |
|
||
| Certificato TLS | `npm-65` (Let's Encrypt) |
|
||
| Failover route | `rete/ha/npm-routes.conf` riga `30\|mcp.loogle.it\|8700\|...` |
|
||
| Websocket | ON |
|
||
| Force SSL | ON |
|
||
|
||
**Failover:** `npm-failover-route.sh` aggiorna upstream su Pi-2 / DS920 in base allo scenario HA.
|
||
|
||
---
|
||
|
||
## Variabili d'ambiente (`.env`)
|
||
|
||
File: `/home/daniely/docker/loogle-mcp/.env`
|
||
|
||
| Variabile | Descrizione | Esempio |
|
||
|-----------|-------------|---------|
|
||
| `MCP_BASE_URL` | URL pubblico (issuer JWT) | `https://mcp.loogle.it` |
|
||
| `MCP_JWT_SECRET` | Segreto firma token OAuth | *(stringa lunga casuale)* |
|
||
| `MCP_OAUTH_CLIENT_ID` | Client OAuth pre-registrato | `loogle-mcp-public` |
|
||
| `PAPERLESS_URL` | API documenti | `https://docs.loogle.it` |
|
||
| `PAPERLESS_API_TOKEN` | Token API admin (fallback) | vedi [PAPERLESS-TOKEN.md](PAPERLESS-TOKEN.md) |
|
||
| `PAPERLESS_API_TOKEN_{USER}` | Token API per utente | `DANIELE`, `LUCIA`, `DAVIDE`, `LUCA` |
|
||
| `QDRANT_URL` | Vector DB remoto | `http://192.168.128.100:6333` |
|
||
| `OLLAMA_URL` | Embedding locale | `http://192.168.128.100:11434` |
|
||
| `OLLAMA_EMBED_MODEL` | Modello embedding | `nomic-embed-text` |
|
||
| `OPENAI_API_KEY` | Fallback embedding cloud | *(opzionale)* |
|
||
| `INDEXER_INTERVAL_MINUTES` | Intervallo sync Paperless | `30` |
|
||
| `THERMAL_TEMP_HARD_C` | Pausa embedding se CPU DS920 ≥ °C | `70` |
|
||
| `THERMAL_TEMP_SOFT_C` | Rallenta embedding sopra °C | `58` |
|
||
| `THERMAL_CPU_TARGET_PCT` | Cap load1 (nproc × %) | `40` |
|
||
| `OLLAMA_NUM_THREAD` | Thread CPU per richiesta embed | `1` |
|
||
| `OLLAMA_EMBED_DELAY_S` | Pausa tra embed (duty-cycle lento) | `10` |
|
||
| `DS920_THERMAL_URL` | Probe temp/load DS920 | `http://192.168.128.100:9191/thermal` |
|
||
|
||
---
|
||
|
||
## Utenti e isolamento
|
||
|
||
| Persona | Username | Ruolo | Password iniziale |
|
||
|---------|----------|-------|-------------------|
|
||
| Daniele | `daniele` | admin | `daniele` *(cambiarla)* |
|
||
| Lucia | `lucia` | user | `lucia` |
|
||
| Davide | `davide` | user | `davide` |
|
||
| Luca | `luca` | user | `luca` |
|
||
|
||
- Ogni utente vede **solo** i propri progetti in `/mnt/ha-apps/mcp/context/{username}/`
|
||
- I token OAuth JWT contengono `sub` = username; ogni tool filtra per utente
|
||
- L’admin può `reindex_document`, vedere audit completo, revocare refresh token
|
||
|
||
**Scope OAuth:** `context:read`, `context:write`, `knowledge:read`, `knowledge:write`, `gitea:read`, `gitea:write`, `home:read`, `irrigation:read`, `turni:read`, `admin` (solo daniele)
|
||
|
||
---
|
||
|
||
## Tool MCP esposti
|
||
|
||
| Tool | Scope minimo | Funzione |
|
||
|------|--------------|----------|
|
||
| `ping` | — | Test connettività |
|
||
| `whoami` | — | Utente e scope attivi |
|
||
| `list_projects` | context:read | Elenco progetti |
|
||
| `create_project` | context:write | Nuovo progetto |
|
||
| `get_project_context` | context:read | Legge memoria progetto |
|
||
| `save_context` | context:write | Salva/appende memoria |
|
||
| `archive_project` | context:write | Archivia progetto |
|
||
| `search_context` | context:read | Ricerca semantica contesti |
|
||
| `search_knowledge` | knowledge:read | RAG Paperless + contesti + Gitea + app |
|
||
| `search_gitea_knowledge` | knowledge:read | RAG solo Gitea |
|
||
| `search_apps_knowledge` | knowledge:read | RAG Irrigazione + Turni |
|
||
| `list_apps_indexed` | knowledge:read | Stato indicizzazione app |
|
||
| `reindex_apps` | admin | Re-sync RAG app |
|
||
| `get_document` | knowledge:read | Testo documento Paperless |
|
||
| `list_recent_documents` | knowledge:read | Ultimi doc indicizzati |
|
||
| `reindex_document` | admin | Re-indicizza documento |
|
||
| `get_home_dashboard` | home:read | Dashboard Loogle Casa |
|
||
| `get_home_weather` | home:read | Meteo casa |
|
||
| `get_network_overview` | home:read | Panoramica rete LAN |
|
||
| `get_network_failover_status` | home:read | Stato failover cluster |
|
||
| `get_ha_entity` | home:read | Entità Home Assistant |
|
||
| `list_ha_entities` | home:read | Elenco entità HA |
|
||
| `search_ha_entities` | home:read | Cerca entità HA |
|
||
| `get_irrigation_status` | irrigation:read | Stato irrigazione |
|
||
| `get_irrigation_zones` | irrigation:read | Zone irrigazione |
|
||
| `get_irrigation_history` | irrigation:read | Storico irrigazioni |
|
||
| `get_turni_status` | — | Stato servizio Turni |
|
||
| `get_my_shifts` | turni:read | Turni utente corrente |
|
||
| `list_turni_doctors` | turni:read | Medici Turni-Live |
|
||
|
||
Vedi anche tool Gitea in `docs/GITEA-TOKEN.md` e app homelab in `docs/APPS-INTEGRATION.md`.
|
||
|
||
---
|
||
|
||
## Storage contesto (per progetto)
|
||
|
||
```
|
||
/mnt/ha-apps/mcp/context/{username}/projects/{project-id}/
|
||
meta.json # titolo, tag, date, stato, gitea_repo (opz.)
|
||
context.md # memoria persistente
|
||
sessions/ # snapshot conversazioni
|
||
artifacts/ # file generati dagli agenti
|
||
```
|
||
|
||
---
|
||
|
||
## Vector store / RAG
|
||
|
||
| Collection Qdrant | Contenuto |
|
||
|-------------------|-----------|
|
||
| `kb_shared_family` | Documenti famiglia (tag non personali) |
|
||
| `kb_personal_{user}` | Documenti personali |
|
||
| `ctx_{user}` | Embedding dei contesti agente |
|
||
| `gitea_shared_family` | File testo da repo Gitea non privati |
|
||
| `gitea_personal_{user}` | File testo da repo Gitea privati dell'owner |
|
||
| `apps_shared_family` | Export Irrigazione + Turni (P7) |
|
||
|
||
**Nota Pi 5:** Qdrant locale su ARM (page size 16K) non è supportato. Default: Qdrant su DS920 o fallback SQLite in `/data/vector_fallback.db`.
|
||
|
||
**Pipeline indexer:** ogni 30 min:
|
||
1. Paperless → chunk → embedding (Ollama DS920) → upsert `kb_*`
|
||
2. Gitea (repo configurati in `GITEA_INDEX_REPOS_*`) → file `.md`/`.py`/`.sh` → chunk → upsert `gitea_*`
|
||
3. Irrigazione + Turni → snapshot/storico → upsert `apps_shared_family` (vedi `docs/APPS-INTEGRATION.md`)
|
||
|
||
---
|
||
|
||
## Sicurezza
|
||
|
||
- TLS terminato su NPM (Let's Encrypt)
|
||
- OAuth 2.1 Authorization Code + PKCE
|
||
- JWT access token 1h + refresh token 30 giorni (revocabili)
|
||
- Audit log: `{timestamp, user, tool, resource_id}` in SQLite
|
||
- CrowdSec attivo su NPM
|
||
- Nessun accesso cross-user ai dati
|
||
|
||
---
|
||
|
||
## Operazioni comuni
|
||
|
||
```bash
|
||
# Stato servizi
|
||
cd /home/daniely/docker/loogle-mcp && docker compose ps
|
||
|
||
# Log
|
||
docker logs -f loogle-mcp
|
||
docker logs -f loogle-mcp-indexer
|
||
|
||
# Redeploy
|
||
./scripts/deploy.sh
|
||
|
||
# DNS + NPM (se reinstall)
|
||
sudo /home/daniely/rete/scripts/setup-mcp-dns-npm.sh
|
||
|
||
# Failover route
|
||
/home/daniely/rete/scripts/npm-failover-route.sh normal
|
||
|
||
# Backup dati MCP
|
||
/home/daniely/rete/infra-monitor/mcp-restic-backup.sh
|
||
```
|
||
|
||
---
|
||
|
||
## Failover (tier-b)
|
||
|
||
| Scenario | Comportamento |
|
||
|----------|---------------|
|
||
| Pi-1 down, Pi-2 up | Stack MCP su Pi-2, NPM upstream → `.81:8700` |
|
||
| Extrema DS920 | NPM3 + upstream DS920 |
|
||
| Monitoraggio | Probe `mcp_ok` in `failover-status.sh` |
|
||
|
||
---
|
||
|
||
## Da completare (post-install)
|
||
|
||
1. **`PAPERLESS_API_TOKEN_*`** in `.env` — vedi [PAPERLESS-TOKEN.md](PAPERLESS-TOKEN.md)
|
||
2. **Ollama su DS920** con `nomic-embed-text` per embedding locali
|
||
3. **Qdrant su DS920** (opzionale): `rete/compose/extrema-ds920/loogle-mcp-compose.yml`
|
||
4. **Cambio password** per tutti gli utenti dal dashboard
|
||
5. **`MCP_JWT_SECRET`** — impostare valore robusto in produzione
|
||
|
||
---
|
||
|
||
## Riferimenti
|
||
|
||
- [Guida utenti famiglia](docs/GUIDA-UTENTI.md)
|
||
- [Onboarding tecnico client AI](docs/ONBOARDING.md)
|
||
- [Runbook operativo](/home/daniely/rete/ha/RUNBOOK-mcp.md)
|
||
- [README progetto](README.md)
|