Mail: _safe_decode + _iter_messages (per-mail try/except) -> eine kaputte Mail (charset x-unknown) bricht die Suche nicht mehr ab, findet so auch uralte Mails. Vollscan + Datum-Sort + MAX_SCAN/DEADLINE_S (Selbstheilung, kein 24h-Haenger mehr). Files: search_files mit SEARCH_DEADLINE_S + SEARCH_MAX_DIRS begrenzt (war unbegrenzte rekursive PROPFIND -> real 35s/449 Calls). Calendar/Contacts: PARSE_DEADLINE_S fuer client-seitiges vobject-Parsen; MAX_RESULTS-Output-Cap in get_events/get_tasks (war 10939 Zeilen bei 6000 Events); search Top-N + Hinweise. Tests: in Schichten getrennt -> Smoke (nightly), Edge + Stress (on-demand). Neu: test_smoke/test_edge/test_stress + conftest-Marker + run_full_tests.sh, nightly laeuft nur Smoke. Kontakt-Foto-Roundtrip-Test, Mail-Edge-Mails (x-unknown, kaputter Header, 2009er Alt-Mail). Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
168 lines
9.5 KiB
Markdown
168 lines
9.5 KiB
Markdown
# mcptest — isoliertes Test-/Dev-Backend
|
|
|
|
Die MCP-Test-Suite (`test_all.py`, taeglich via `mcp-tests.timer`) laeuft **NICHT mehr
|
|
auf Stefans echten Daten**, sondern auf einem dedizierten `mcptest`-User mit eigenen
|
|
Backends. Dieselbe Umgebung dient als isolierte **Dev-Sandbox** fuer die MCP-Server.
|
|
|
|
## Routing
|
|
|
|
`common.py`: `USER_ALIASES = {"test": "mcptest"}`. Der Test-OAuth-Client `client_id=test`
|
|
(Secret in `config.json` -> `test.token`) wird also auf den User `mcptest` gemappt.
|
|
Alle Server lesen pro User aus `config.json`.
|
|
|
|
## Backends pro Dienst
|
|
|
|
- **Calendar/Contacts (Radicale):** User `mcptest` (htpasswd `/opt/radicale/config/htpasswd`,
|
|
bcrypt). Collections `/mcptest/calendar-test/` (VEVENT+VTODO) und `/mcptest/contacts-test/`
|
|
(VADDRESSBOOK, Test-Kontakt "Max Mustermann"). `calendar_paths/addressbook_paths[mcptest]=["/mcptest/"]`.
|
|
- **Files (oCIS):** kompletter User `mcptest` (angelegt via Graph-API `POST /graph/v1.0/users`
|
|
als admin). `ocis_users[mcptest]`. Tests legen `/.mcp-tests` selbst an + raeumen auf.
|
|
- **Notes (Joplin):** lokales joplin-cli-Profil `/mnt/ssd/joplin-mcp/profiles/mcptest`
|
|
(`sync.target=0`, kein Server-Sync), Data API auf **:41186** via `joplin-cli-mcptest.service`.
|
|
Notizbuecher `Inbox` + `MCP Test` mit Beispielnotizen. `joplin_data_api[mcptest]`.
|
|
- **Mail:** statische Test-**Maildir** `/opt/mcp-servers/tests/testdata/maildir/` mit Konten
|
|
`mcp-test-mail` (INBOX: "Willkommen" + "Rechnung"+PDF-Anhang, Sent) und `mcp-test-empty`.
|
|
`mail_roots[mcptest]` zeigt dahin. (Der Mail-MCP liest Maildirs, kein Live-IMAP.)
|
|
|
|
## Credentials
|
|
|
|
- Klartext-Backup: `/root/.mcptest-creds` (chmod 600).
|
|
- Aktiv genutzt: `config.json` (gitignored) — radicale/ocis Passwoerter, joplin Token.
|
|
- oCIS-Admin (fuer User-Anlage): `/mnt/ssd/ocis/auth.txt`.
|
|
|
|
## Tests laufen lassen
|
|
|
|
```bash
|
|
/opt/mcp-servers/venv/bin/python -m pytest /opt/mcp-servers/tests/test_all.py -q
|
|
# oder der taegliche Runner:
|
|
sudo /opt/mcp-servers/tests/run_tests.sh
|
|
```
|
|
|
|
## Als Dev-Sandbox nutzen
|
|
|
|
- Direkt gegen die Backends: Radicale `http://127.0.0.1:5232/mcptest/` (User mcptest),
|
|
Joplin Data API `http://127.0.0.1:41186` (Token aus config.json), Maildir s.o.
|
|
- Ueber die MCP-Server: mit dem `test`-OAuth-Client verbinden -> trifft automatisch mcptest.
|
|
- Testdaten zuruecksetzen: Collections/Notebooks neu seeden (siehe Provisioning unten).
|
|
|
|
## Provisioning (Recreate)
|
|
|
|
1. Radicale: `htpasswd -bB /opt/radicale/config/htpasswd mcptest <pw>`; dann MKCALENDAR
|
|
`/mcptest/calendar-test/` + extended-MKCOL `/mcptest/contacts-test/`.
|
|
2. oCIS: `POST /graph/v1.0/users` als admin (onPremisesSamAccountName=mcptest, passwordProfile).
|
|
3. Joplin: Profil anlegen (`joplin --profile DIR config api.token ...; sync.target 0`),
|
|
`api.port=41186` in settings.json, `joplin-cli-mcptest.service` (Kopie von -stefan),
|
|
Notebooks/Notizen via Data API seeden.
|
|
4. Mail: `tests/testdata/maildir/` (im Repo) — Maildir-Konten mit cur/-Nachrichten.
|
|
5. `config.json`: mcptest in radicale_users, ocis_users, joplin_data_api, mail_roots,
|
|
calendar_paths, addressbook_paths.
|
|
6. `common.py`: `USER_ALIASES = {"test": "mcptest"}`.
|
|
|
|
Verwandt: `/opt/mcp-servers/CLAUDE.md`.
|
|
|
|
## Testdaten (Dateien + Mail-Anhaenge)
|
|
|
|
Reichhaltiges Set ueber alle gaengigen Typen (fuer read_file-/Attachment-Tests + Dev):
|
|
|
|
- **oCIS** unter `/testdata/{images,audio,video,documents,text,archives}/`:
|
|
Bilder (jpg/png/webp/bmp/gif/tiff/svg), Audio (mp3/ogg/m4a/flac/wav), Video (mp4),
|
|
PDFs (Text-PDF `document.pdf` + Scan-PDF `scanned.pdf`), Office (docx/xlsx/pptx),
|
|
Text/Daten (md/txt/csv/json/xml/yaml/html/py/vcf/ics), Archive (zip/tar.gz).
|
|
- **Mail-Maildir** `tests/testdata/maildir/mcp-test-mail/INBOX`: Mails mit diversen
|
|
Anhaengen (Rechnung Text+Scan-PDF, Fotos, Word+Excel, MP3, ZIP+CSV, PPTX+MP4).
|
|
|
|
`TestFileTypes` (test_all.py) liest je Typ eine `/testdata`-Datei und prueft den
|
|
zurueckgegebenen Content-Typ (image/text/resource). Office-Docs liefert der Files-MCP
|
|
als extrahierten **Text**.
|
|
|
|
### Neu generieren
|
|
|
|
Wegwerf-venv + ffmpeg noetig:
|
|
```bash
|
|
sudo apt-get install -y ffmpeg
|
|
python3 -m venv /tmp/gen && /tmp/gen/bin/pip install fpdf2 python-docx openpyxl python-pptx Pillow
|
|
/tmp/gen/bin/python tests/testdata/gen_testfiles.py # -> /tmp/mcptest-files
|
|
sudo tests/testdata/upload_ocis.sh # -> mcptest oCIS /testdata/
|
|
/tmp/gen/bin/python tests/testdata/gen_maildir.py # -> maildir mit Anhaengen
|
|
```
|
|
|
|
### Edge-Cases
|
|
|
|
`/testdata/edge/` + `TestFileEdgeCases`: leere Datei, 0-Byte-Binary, Name mit
|
|
Umlauten/Leerzeichen/Klammern, Unicode/Emoji/RTL-Inhalt, Datei ohne Endung,
|
|
passwortgeschuetztes PDF + ZIP, uebergrosse Datei (26 MB > 25-MB-Limit ->
|
|
"Datei zu gross"). Alle werden graceful behandelt (kein Crash). Generator:
|
|
`gen_edge.py` (braucht pikepdf + zip), Upload via `upload_ocis.sh`.
|
|
|
|
## Verbesserung: bildbasierte/gescannte PDFs (2026-06-19)
|
|
|
|
Frueher gab `read_file` bei Scan-PDFs (kein extrahierbarer Text) nur Rohbytes
|
|
(`EmbeddedResource`) zurueck — claude.ai konnte den Inhalt nicht lesen. Jetzt werden
|
|
solche PDFs mit **PyMuPDF** seitenweise als **PNG-Bilder** (150 dpi, max 20 Seiten)
|
|
gerendert und als `ImageContent` zurueckgegeben -> das LLM liest sie per Vision.
|
|
Zusaetzlich **OCR** (tesseract deu+eng) -> durchsuchbarer Text neben den Bildern.
|
|
Gemeinsames Modul `pdfutil.py` wird von Files-MCP (`read_file`) UND Mail-MCP
|
|
(`read_attachment`) genutzt -> Scan-PDF-Mailanhaenge werden genauso gerendert.
|
|
Produktiv-Feature (alle User). Test: `TestFileTypes` scanned.pdf -> `image`.
|
|
Runtime-Deps: `pymupdf`, `pytesseract` + System `tesseract-ocr`/`-deu` (s. `requirements-extra.txt`).
|
|
|
|
## Test-Schichten (2026-06-24)
|
|
|
|
Drei Schichten, NUR Smoke laeuft naechtlich:
|
|
|
|
- **Smoke** (`test_smoke.py`, Marker `smoke`): NIGHTLY via `mcp-tests.timer` (05:00) ->
|
|
`run_tests.sh` -> `pytest test_smoke.py`. Pro Connector: Server up + OAuth + tools/list +
|
|
EIN read-only Call. Schnell (~0.5s), schreibt keine Testdaten. Faengt Totalausfaelle ab.
|
|
- **Edge** (`test_edge.py`, Marker `edge`): ON-DEMAND. Boese/ungewoehnliche Eingaben pro
|
|
Connector: Unicode/Emoji-Roundtrips, Sonderzeichen, riesige Limits/Bodies, kaputte
|
|
Datumsangaben, **Path-Traversal-Block** (Files). Muss graceful sein, nichts leaken/crashen.
|
|
- **Stress** (`test_stress.py`, Marker `stress`): ON-DEMAND. 40-60 gleichzeitige Requests
|
|
pro Connector + Mischlast ueber alle 5; danach Responsiveness-Check. Faengt den Klassiker
|
|
"single-threaded Server haengt unter Last" (genau der Mail-Haenger 2026-06-23).
|
|
- **Funktional** (`test_all.py`): CRUD + OAuth + Datei-Typen/-Edge. ON-DEMAND.
|
|
|
|
Helper (`SERVERS`, `get_token`, `tool_call`, `mcp_call`) liegen in `test_all.py`; smoke/edge/
|
|
stress importieren sie. Marker in `conftest.py`.
|
|
|
|
**Runner:**
|
|
- Nightly (Smoke): automatisch via Timer, ODER `run_tests.sh`.
|
|
- On-demand (Funktional+Edge+Stress): `run_full_tests.sh` (Log `/var/log/mcp-tests-full.log`),
|
|
oder eine Schicht: `run_full_tests.sh -m stress` (bzw. `-m edge`).
|
|
- Log `/var/log/mcp-tests.log` muss `stefan:stefan` gehoeren (Service laeuft als User=stefan).
|
|
|
|
## Mail-Server-Haertung (2026-06-23/24)
|
|
|
|
`mcp-mail` hing 24h an einem Such-`CallToolRequest` (single-threaded -> Connector tot).
|
|
Ursache: eine kaputte Mail (charset `x-unknown` -> `LookupError`) plus fehlende per-Mail-
|
|
Fehlerbehandlung. Fix in BEIDEN Servern (`/opt/mcp-servers/mail/server.py` remote,
|
|
`/opt/mcp-mail/server.py` lokal fuer Claude Code):
|
|
- `_safe_decode` (unbekannte Charsets), `_iter_messages` (per-Mail try/except statt
|
|
`md.items()`), per-Treffer try/except -> eine kaputte Mail killt die Suche NICHT mehr
|
|
(findet so auch uralte Mails nach der kaputten).
|
|
- Vollscan + Datum-Sortierung (neueste zuerst) statt frueher Abbruch bei `limit`.
|
|
- `MAX_SCAN=2000` + `DEADLINE_S=120` Wall-Clock -> Server heilt sich selbst, nie wieder
|
|
Endlos-Haenger. Volltext-Scan ueber die echten 3.3GB dauert ~60-70s fuer seltene Begriffe;
|
|
per `account`/`folder` eingrenzen ist deutlich schneller.
|
|
|
|
## Connector-Skalierung (Stand 2026-06-24, gegen ECHTE Daten gemessen)
|
|
|
|
Frage: verkraften die Connector "viel durchwuehlen" wie der Mail-Vollscan?
|
|
- **Notes:** unkritisch. Joplin server-seitige FTS (`/search`), paginiert mit Cap (max 50
|
|
Seiten), Timeouts. Alle Ops <0.1s auch bei vielen Notizen.
|
|
- **Files:** `list_files`/`read_file` ok (1 PROPFIND bzw. 25MB-Cap, Timeouts). ABER
|
|
`search_files` war riskant: client-seitige Rekursion = **1 PROPFIND pro Ordner**, kein
|
|
Deadline. Real gemessen: Miss-Suche 35s/449 PROPFINDs, wachsend mit dem Baum -> haette
|
|
den single-threaded Server blockiert. **Fix:** `SEARCH_DEADLINE_S=20` + `SEARCH_MAX_DIRS=400`
|
|
+ Truncation-Hinweis ("Begriff praezisieren / 'path' enger"). Worst-Case jetzt ~20s gekappt.
|
|
Langfristig besser: oCIS server-seitige Suche (Graph/REPORT) statt Client-Rekursion.
|
|
- **Calendar (Radicale):** `get_events` ist server-seitig zeitgefiltert (gut). ABER bei
|
|
~6000 Test-Events gemessen: `search_events` 6-8s (vobject parst ALLE Events im ~15-Mon-
|
|
Fenster client-seitig), `get_events` lieferte **10939 Zeilen** (KEIN Result-Limit -> Token-
|
|
Explosion). Real schon 5s bei 29 Kalendern. **Fix:** `_parse` mit `PARSE_DEADLINE_S=12` +
|
|
`get_events`/`get_tasks` Output-Cap `MAX_RESULTS=300` + Hinweise; `search_events` Top-30 +
|
|
Deadline. Danach get_events 1501 statt 10939 Zeilen.
|
|
- **Contacts (Radicale):** `search_contacts` parst ALLE vCards client-seitig (real 0.2s, wenige
|
|
Kontakte; skaliert linear). **Fix:** `PARSE_DEADLINE_S=12` in `_get_contacts` + Hinweis.
|
|
- Rest-Kosten = CalDAV/CardDAV-REPORT-Transfer selbst (kein Server-Result-Limit im Protokoll);
|
|
in der Praxis durch Datumsbereich/Begriff eingrenzbar. Langfristig: server-seitige Suche.
|