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>
This commit is contained in:
co-authored by
Claude Opus 4.8
parent
936ebc2f56
commit
c9ccb4392b
+20
-3
@@ -5,6 +5,7 @@ from xml.etree import ElementTree as ET
|
||||
from typing import Annotated
|
||||
|
||||
import httpx
|
||||
import time
|
||||
from pydantic import Field
|
||||
from mcp.server.fastmcp import FastMCP
|
||||
from mcp.types import TextContent, ImageContent, EmbeddedResource, BlobResourceContents
|
||||
@@ -145,18 +146,31 @@ def file_info(
|
||||
return "\n".join(parts)
|
||||
|
||||
|
||||
SEARCH_DEADLINE_S = 20 # Wall-Clock-Limit fuer search_files
|
||||
SEARCH_MAX_DIRS = 400 # max Verzeichnisse (PROPFINDs) pro Suche
|
||||
|
||||
|
||||
@mcp.tool()
|
||||
def search_files(
|
||||
query: Annotated[str, Field(description="Search term — matches file names. Example: 'Rechnung', '.pdf', 'backup'")],
|
||||
path: Annotated[str, Field(description="Start directory for search")] = "/",
|
||||
) -> str:
|
||||
"""Search for files by name recursively (up to 5 levels deep, max 50 results)."""
|
||||
"""Search for files by name recursively (max 5 Ebenen, 50 Treffer; bricht nach
|
||||
SEARCH_MAX_DIRS Ordnern bzw. SEARCH_DEADLINE_S ab, damit der Server nie haengt).
|
||||
Bei grossem Baum/vielen Treffern: 'path' enger setzen."""
|
||||
user = get_current_user()
|
||||
if not user: return "Error: not authenticated"
|
||||
q = query.lower()
|
||||
results = []
|
||||
state = {"dirs": 0, "truncated": False}
|
||||
t0 = time.monotonic()
|
||||
def _s(p, d=0):
|
||||
if d > 5 or len(results) >= 50: return
|
||||
if d > 5 or len(results) >= 50 or state["truncated"]:
|
||||
return
|
||||
if state["dirs"] >= SEARCH_MAX_DIRS or (time.monotonic() - t0) > SEARCH_DEADLINE_S:
|
||||
state["truncated"] = True
|
||||
return
|
||||
state["dirs"] += 1
|
||||
xml, _ = _propfind(user, p)
|
||||
for e in _parse_pf(xml, user)[1:]:
|
||||
if q in e["name"].lower(): results.append(e)
|
||||
@@ -166,7 +180,10 @@ def search_files(
|
||||
for e in results:
|
||||
prefix = "[DIR]" if e["is_dir"] else f"({e['size']:,}b)"
|
||||
lines.append(f"{prefix} {e['path']}")
|
||||
return "\n".join(lines) if lines else "Keine Dateien gefunden"
|
||||
out = "\n".join(lines) if lines else "Keine Dateien gefunden"
|
||||
if state["truncated"]:
|
||||
out += f"\n\n[Suche begrenzt nach {state['dirs']} Ordnern/{SEARCH_DEADLINE_S}s — Begriff praezisieren oder 'path' enger setzen]"
|
||||
return out
|
||||
|
||||
|
||||
@mcp.tool()
|
||||
|
||||
Reference in New Issue
Block a user