Files
Stefan LohmaierandClaude Opus 4.8 c9ccb4392b harden: per-item robustness + deadlines/caps across connectors; layered tests
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>
2026-06-24 21:17:26 +02:00

9.5 KiB

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

/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:

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.