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>
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-APIPOST /graph/v1.0/usersals admin).ocis_users[mcptest]. Tests legen/.mcp-testsselbst 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 viajoplin-cli-mcptest.service. NotizbuecherInbox+MCP Testmit Beispielnotizen.joplin_data_api[mcptest]. - Mail: statische Test-Maildir
/opt/mcp-servers/tests/testdata/maildir/mit Kontenmcp-test-mail(INBOX: "Willkommen" + "Rechnung"+PDF-Anhang, Sent) undmcp-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 APIhttp://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)
- Radicale:
htpasswd -bB /opt/radicale/config/htpasswd mcptest <pw>; dann MKCALENDAR/mcptest/calendar-test/+ extended-MKCOL/mcptest/contacts-test/. - oCIS:
POST /graph/v1.0/usersals admin (onPremisesSamAccountName=mcptest, passwordProfile). - Joplin: Profil anlegen (
joplin --profile DIR config api.token ...; sync.target 0),api.port=41186in settings.json,joplin-cli-mcptest.service(Kopie von -stefan), Notebooks/Notizen via Data API seeden. - Mail:
tests/testdata/maildir/(im Repo) — Maildir-Konten mit cur/-Nachrichten. config.json: mcptest in radicale_users, ocis_users, joplin_data_api, mail_roots, calendar_paths, addressbook_paths.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-PDFdocument.pdf+ Scan-PDFscanned.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, Markersmoke): NIGHTLY viamcp-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, Markeredge): 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, Markerstress): 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.logmussstefan:stefangehoeren (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 stattmd.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=120Wall-Clock -> Server heilt sich selbst, nie wieder Endlos-Haenger. Volltext-Scan ueber die echten 3.3GB dauert ~60-70s fuer seltene Begriffe; peraccount/foldereingrenzen 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_fileok (1 PROPFIND bzw. 25MB-Cap, Timeouts). ABERsearch_fileswar 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_eventsist server-seitig zeitgefiltert (gut). ABER bei ~6000 Test-Events gemessen:search_events6-8s (vobject parst ALLE Events im ~15-Mon- Fenster client-seitig),get_eventslieferte 10939 Zeilen (KEIN Result-Limit -> Token- Explosion). Real schon 5s bei 29 Kalendern. Fix:_parsemitPARSE_DEADLINE_S=12+get_events/get_tasksOutput-CapMAX_RESULTS=300+ Hinweise;search_eventsTop-30 + Deadline. Danach get_events 1501 statt 10939 Zeilen. - Contacts (Radicale):
search_contactsparst ALLE vCards client-seitig (real 0.2s, wenige Kontakte; skaliert linear). Fix:PARSE_DEADLINE_S=12in_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.