#!/usr/bin/env python3 """ Mission Control 2.0 — Web Fetch MCP Server. Stdio-MCP-Server. Läuft als Subprocess von Hermes Gateway (AI Box). Fetcht URLs und extrahiert sauberen Text — kein API-Key nötig. """ import httpx from mcp.server.fastmcp import FastMCP from guard import wrap_untrusted # Injection-Schutz für untrusted Web-Inhalt (Stufe 0 Security) mcp = FastMCP("hermes-web-fetch") HEADERS = { "User-Agent": ( "Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 " "(KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36" ) } @mcp.tool() def fetch_url(url: str, max_chars: int = 8000) -> str: """Lädt eine URL und extrahiert den Haupttext (Artikel, News, Seiteninhalte). Nutze dies wenn du den vollen Inhalt einer Webseite lesen willst. Gibt sauberen Text zurück — kein HTML-Rauschen. max_chars: maximale Zeichenanzahl des Ergebnisses (default 8000).""" try: import trafilatura resp = httpx.get(url, headers=HEADERS, timeout=20, follow_redirects=True) resp.raise_for_status() text = trafilatura.extract( resp.text, include_comments=False, include_tables=True, no_fallback=False, ) if not text: # Fallback: strip HTML tags manually import re text = re.sub(r"<[^>]+>", " ", resp.text) text = re.sub(r"\s+", " ", text).strip() if len(text) > max_chars: text = text[:max_chars] + f"\n\n[... gekürzt auf {max_chars} Zeichen]" # Untrusted Web-Inhalt als DATEN markieren (+ Warnung bei Injection-Mustern). return wrap_untrusted(text or "Kein Text extrahierbar.", label=f"WEBSEITE {url}") except httpx.TimeoutException: return f"FEHLER: Timeout beim Laden von {url}" except httpx.HTTPStatusError as e: return f"FEHLER: HTTP {e.response.status_code} für {url}" except Exception as e: return f"FEHLER: {e}" @mcp.tool() def web_search(query: str, max_results: int = 6) -> str: """Websuche (DuckDuckGo, kein API-Key). Liefert Titel, URL und Kurzbeschreibung je Treffer. Nutze dies für aktuelle Informationen, Dokumentation oder Fehlermeldungen — und lies vielversprechende Treffer danach mit fetch_url im Volltext.""" try: from ddgs import DDGS rows = list(DDGS().text(query, max_results=max_results) or []) if not rows: return "Keine Treffer." lines = [f"- {r.get('title', '(ohne Titel)')}\n {r.get('href', '')}\n {r.get('body', '')}" for r in rows] return wrap_untrusted("\n".join(lines), label=f"WEBSUCHE {query}") except Exception as e: return f"FEHLER: {e}" @mcp.tool() def fetch_urls(urls: list[str], max_chars_each: int = 4000) -> str: """Lädt mehrere URLs gleichzeitig und gibt die Texte zurück. Nützlich um mehrere Suchergebnisse auf einmal zu lesen. urls: Liste von URLs (max 5) max_chars_each: max Zeichen pro Seite""" if len(urls) > 5: urls = urls[:5] results = [] for url in urls: text = fetch_url(url, max_chars_each) results.append(f"=== {url} ===\n{text}") return "\n\n".join(results) if __name__ == "__main__": mcp.run()