diff --git a/compose.yaml b/compose.yaml index 4627f49..dc9381e 100644 --- a/compose.yaml +++ b/compose.yaml @@ -398,7 +398,7 @@ services: # Seed native MCP connections on a fresh Open WebUI database. Secrets # stay inside the tool containers, so these internal URLs need no keys. TOOL_SERVER_CONNECTIONS: >- - [{"url":"http://mike-ai-mcp-web:8000/mcp","path":"","type":"mcp","auth_type":"none","headers":null,"key":"","config":{"enable":true,"access_grants":[]},"info":{"id":"web-local","name":"Web (lokal)","description":"Kompakte Websuche und Quellenvergleich"}},{"url":"http://mike-ai-mcp-homeassistant:8000/mcp","path":"","type":"mcp","auth_type":"none","headers":null,"key":"","config":{"enable":true,"access_grants":[]},"info":{"id":"homeassistant-local","name":"Home Assistant (lokal)","description":"Home-Assistant-Werkzeuge mit serverseitigem Token"}},{"url":"http://mike-ai-mcp-arr:8000/mcp","path":"","type":"mcp","auth_type":"none","headers":null,"key":"","config":{"enable":true,"access_grants":[]},"info":{"id":"arr-local","name":"ARR (lokal)","description":"Sonarr- und Radarr-Werkzeuge"}},{"url":"http://mike-ai-mcp-unraid-official:8000/mcp","path":"","type":"mcp","auth_type":"none","headers":null,"key":"","config":{"enable":true,"access_grants":[]},"info":{"id":"unraid-readonly-local","name":"Unraid (lokal, read-only)","description":"Begrenzte Unraid-Diagnose"}}] + [{"url":"http://mike-ai-mcp-web:8000/mcp","path":"","type":"mcp","auth_type":"none","headers":null,"key":"","config":{"enable":true,"access_grants":[]},"info":{"id":"web-local","name":"Web (öffentlich, read-only)","description":"Für aktuelle öffentliche Internetdaten, Quellenprüfung, GitHub/Hugging Face und Produktsuche. Nicht für Home Assistant, Medienverwaltung oder NAS-Diagnose."}},{"url":"http://mike-ai-mcp-homeassistant:8000/mcp","path":"","type":"mcp","auth_type":"none","headers":null,"key":"","config":{"enable":true,"access_grants":[]},"info":{"id":"homeassistant-local","name":"Home Assistant (lokal)","description":"Nur für Home-Assistant-Entitäten, Zustände, Historie, Automationen, Dashboards und HA-Diagnose. Nicht für Unraid, Sonarr/Radarr oder allgemeine Websuche."}},{"url":"http://mike-ai-mcp-arr:8000/mcp","path":"","type":"mcp","auth_type":"none","headers":null,"key":"","config":{"enable":true,"access_grants":[]},"info":{"id":"arr-local","name":"Sonarr und Radarr (lokal)","description":"Nur für verwaltete Serien/Filme, fehlende Episoden, Queue und Suche über konfigurierte Indexer. Keine allgemeine Websuche; Schreibaktionen benötigen Vorschau und Freigabe."}},{"url":"http://mike-ai-mcp-unraid-official:8000/mcp","path":"","type":"mcp","auth_type":"none","headers":null,"key":"","config":{"enable":true,"access_grants":[]},"info":{"id":"unraid-readonly-local","name":"Unraid (Systemdiagnose)","description":"Nur für Unraid-Host, Array, Datenträger, Docker-Container, Shares, Netzwerk, UPS und Systemlogs. Nicht für Home Assistant oder Medieninhalte; Standardzugriff read-only."}}] DO_NOT_TRACK: "true" SCARF_NO_ANALYTICS: "true" ports: diff --git a/docs/ATHENA_REBUILD_LOG.md b/docs/ATHENA_REBUILD_LOG.md index 10012c1..55067a6 100644 --- a/docs/ATHENA_REBUILD_LOG.md +++ b/docs/ATHENA_REBUILD_LOG.md @@ -36,6 +36,7 @@ Chats oder privaten Nutzdaten. | Reasoning-Filter | Beide globalen Filter besaßen Priorität 0; bei gleicher Priorität entschied die ID-Sortierung statt der gewünschten Logik. | `Reasoning Default Off` läuft mit Priorität 10 sicher vor dem optionalen `Thinking`-Override mit Priorität 20. Filter und sicherer Installer liegen versioniert im Repository. | | Folgefragen | OpenWebUI erzeugte nach Antworten zusätzliche Vorschläge und verbrauchte dafür einen weiteren Modellaufruf. | Folgefragengenerierung ist in der persistenten OpenWebUI-Konfiguration und im Compose-Standard deaktiviert. | | Werkzeug-/Kontextschutz | Große MCP-Antworten und wiederholte identische Aufrufe konnten Kontextfenster sprengen beziehungsweise Tool-Schleifen erzeugen. | Globaler Stability Guard begrenzt Resultate, verdichtet alte Inhalte profilabhängig und stoppt Wiederholungen; JSON-Schemas und Bilder werden nicht beschädigt. | +| Werkzeugauswahl | Überlappende oder zu allgemeine MCP-Beschreibungen führten zu falschen Werkzeugen, unnötigen Wiederholungen und paralleler Nutzung von MUA und Unraid-Diagnose. | Klare USE-/DO-NOT-USE-Texte auf Server- und Werkzeugebene; Web-Unterwerkzeuge sind nach Lookup, Verifikation, Shopping und Tiefenrecherche getrennt. Bestehende OpenWebUI-Verbindungen werden ohne Änderung von URL, Schlüssel oder Berechtigungen aktualisiert. | | Leistungsdaten | Benchmarkdaten sollten sichtbar sein, ohne private Chat-Inhalte zu protokollieren. | Ein globaler Abschlussfilter schreibt ausschließlich technische Zahlen in eine lokal rotierende JSONL-Datei und zeigt eine knappe Statuszeile. | | Antwortaktionen | Wiederkehrende Nachbearbeitungen sollten bewusst per Klick statt als permanenter Zusatzprompt laufen. | Eine versionierte globale Action bietet lokale Kurzfassung, Checkliste, Diagnose, Unsicherheits-/Quellenprüfung, Thinking-Verbesserung und Markdown-Kopie; keine Schreib- oder Profilwechselaktion. | | Sprachausgabe | Beim ersten Leerhostaufbau war kein TTS-Dienst Bestandteil des Compose-Stacks. | Piper `piper-tts` 1.6.0 läuft als eigener interner CPU-Container mit persistenter deutscher Stimme; Open WebUI nutzt ihn ausschließlich über den authentifizierten Router. | diff --git a/platform/mcp/README.md b/platform/mcp/README.md index bd4c5d9..57d63fb 100644 --- a/platform/mcp/README.md +++ b/platform/mcp/README.md @@ -19,6 +19,25 @@ Prompts heraus, verhindert den früher beobachteten Kontextverbrauch von über TinySearch und SearXNG sind interne Abhängigkeiten des Web-MCPs und werden nicht direkt als allgemeine Werkzeuge angeboten. +## Entscheidungshilfe für das Modell + +Die Server- und Werkzeugbeschreibungen grenzen die Zuständigkeiten absichtlich +deutlich voneinander ab. Das Modell soll pro Aufgabe zunächst genau **einen** +passenden Server wählen: + +| Aufgabe | Werkzeugserver | Nicht zusätzlich verwenden | +|---|---|---| +| Aktuelle öffentliche Informationen, Quellen, GitHub/Hugging Face, Produkte | Web | HA, ARR, Unraid | +| Entitäten, Zustände, Historie, Automationen und Dashboards | Home Assistant | Web, Unraid | +| Serien, Filme, fehlende Episoden und Indexer-Releases | Sonarr und Radarr | Web | +| Lesende NAS-, Docker-, Array-, Netzwerk- und Logdiagnose | Unraid (Systemdiagnose) | MUA | +| Ausdrücklich benötigte MUA-Verwaltungsaktion | MUA | Unraid-Diagnose nicht parallel | + +Ein leeres Ergebnis ist kein Grund, dieselbe Frage über mehrere unpassende +Werkzeuge oder leicht veränderte Suchbegriffe erneut auszuführen. Das Modell +soll die Grenze transparent nennen und gezielt nachfragen, wenn eine Freigabe +oder ein anderes Werkzeug benötigt wird. + ## Sicherheitsmodell - Kein MCP-Port wird auf eine Host-Adresse veröffentlicht. diff --git a/platform/mcp/compose.yaml b/platform/mcp/compose.yaml index 1f4c786..97266fb 100644 --- a/platform/mcp/compose.yaml +++ b/platform/mcp/compose.yaml @@ -92,6 +92,9 @@ services: # The local fork adds bounded read-only Sonarr pseudo-actions. Keep the # patch explicit until upstream publishes a self-contained 2.x image. - ${ARR_SONARR_PATCH:-./patches/mcp_sonarr.py}:/usr/local/lib/python3.13/site-packages/arr_mcp/mcp/mcp_sonarr.py:ro + # Upstream's generic "Execute any Radarr API action" text gives small + # models no routing boundary. This overlay changes guidance only. + - ${ARR_RADARR_PATCH:-./patches/mcp_radarr.py}:/usr/local/lib/python3.13/site-packages/arr_mcp/mcp/mcp_radarr.py:ro networks: [tools, egress] mcp-unraid-official: diff --git a/platform/mcp/patches/mcp_radarr.py b/platform/mcp/patches/mcp_radarr.py new file mode 100644 index 0000000..113ce79 --- /dev/null +++ b/platform/mcp/patches/mcp_radarr.py @@ -0,0 +1,38 @@ +"""Radarr action-routed MCP tool with model-oriented routing guidance.""" + +import json +from typing import Any + +from agent_utilities.mcp_utilities import dispatch, run_blocking +from fastmcp import FastMCP +from pydantic import Field + +from arr_mcp.auth import get_radarr_client + + +def register_radarr_tools(mcp: FastMCP) -> None: + @mcp.tool(tags={"radarr"}) + async def radarr_action( + action: str = Field( + description=( + "Choose one Radarr operation. Common read choices include get_movie for the " + "movie library and get_system_status/get_health for Radarr diagnostics. Use " + "list_actions only when an unusual Radarr operation is genuinely required. " + "Never guess a modifying action and never change Radarr without explicit user approval." + ) + ), + params_json: str = Field( + default="{}", + description=( + "JSON object encoded as a string containing only parameters required by the " + "selected Radarr action. Use \"{}\" for actions without parameters; never " + "invent movie IDs, paths, profile IDs or monitoring settings." + ), + ), + ) -> Any: + """USE ONLY for movies managed by Radarr: inspect the movie library, wanted/queue/history state, releases, profiles, or Radarr health. DO NOT use for TV episodes (use Sonarr), public-web research, media playback, filesystem copying, or direct downloads. Prefer read actions; any mutation requires explicit user approval.""" + client = get_radarr_client() + kwargs = {k: v for k, v in json.loads(params_json).items() if v is not None} + return await run_blocking( + dispatch, client, action, kwargs, service="arr-radarr" + ) diff --git a/platform/mcp/patches/mcp_sonarr.py b/platform/mcp/patches/mcp_sonarr.py index 50367d6..18019f5 100644 --- a/platform/mcp/patches/mcp_sonarr.py +++ b/platform/mcp/patches/mcp_sonarr.py @@ -411,14 +411,14 @@ def register_sonarr_tools(mcp: FastMCP) -> None: @mcp.tool(tags={"sonarr"}) async def sonarr_action( action: str = Field( - description="Sonarr action. Read with find_series {query}, get_season_summary {series_id, season_number}, or search_releases {series_id, season_number}. To download missing episodes, first call preview_episode_search {series_id, season_number, episode_numbers:[2,3,...]}, show its exact preview to the user, then only after explicit approval call start_episode_search with the same scope plus confirm:true and approval_ticket. Monitoring is never changed." + description="Choose one Sonarr operation. Normal choices: find_series to resolve a TV-series name; get_season_summary to list present/missing episodes; search_releases to query Sonarr's configured indexers without downloading; preview_episode_search before any download search; start_episode_search only after the user explicitly approves that exact preview. Use list_actions only for an unusual read operation." ), params_json: str = Field( default="{}", - description="JSON string of parameters to pass to the action.", + description="JSON object encoded as a string. Common forms: find_series {\"query\":\"Title\"}; get_season_summary/search_releases {\"series_id\":123,\"season_number\":2}; preview_episode_search {\"series_id\":123,\"season_number\":2,\"episode_numbers\":[2,3]}. For start_episode_search reuse the exact preview scope and add confirm:true plus approval_ticket.", ), ) -> Any: - """Query Sonarr through a server-side allowlist (read-only by default; write actions when ARR_MCP_WRITE=1).""" + """USE ONLY for TV-series tasks managed by Sonarr: identify a series, inspect missing episodes, search configured indexers, or start an explicitly approved missing-episode search. DO NOT use for movies (use Radarr), public-web research, media playback, filesystem copying, or direct URL downloads. Read-only by default; monitoring is never changed.""" if action in {"list_actions", "help", "actions"}: return { "service": "sonarr", diff --git a/platform/openwebui/install-filters.sh b/platform/openwebui/install-filters.sh index 361b023..7ffc8a6 100755 --- a/platform/openwebui/install-filters.sh +++ b/platform/openwebui/install-filters.sh @@ -134,10 +134,94 @@ with con: """, (key, json.dumps(value), now), ) + + # Improve model-side tool selection without touching URLs, credentials, + # access grants or enable flags from a restored Open WebUI database. + row = con.execute( + "select value from config where key=?", + ("tool_server.connections",), + ).fetchone() + if row: + connections = json.loads(row[0]) + if not isinstance(connections, list): + raise SystemExit("Unbekanntes Format in tool_server.connections.") + descriptions = { + "web-local": ( + "Web (öffentlich, read-only)", + "Für aktuelle öffentliche Internetdaten, Quellenprüfung, GitHub/Hugging Face " + "und Produktsuche. Nicht für Home Assistant, Medienverwaltung oder NAS-Diagnose.", + ), + "homeassistant-local": ( + "Home Assistant (lokal)", + "Nur für Home-Assistant-Entitäten, Zustände, Historie, Automationen, Dashboards " + "und HA-Diagnose. Nicht für Unraid, Sonarr/Radarr oder allgemeine Websuche.", + ), + "arr-local": ( + "Sonarr und Radarr (lokal)", + "Nur für verwaltete Serien/Filme, fehlende Episoden, Queue und Suche über " + "konfigurierte Indexer. Keine allgemeine Websuche; Schreibaktionen benötigen " + "Vorschau und Freigabe.", + ), + "unraid-readonly-local": ( + "Unraid (Systemdiagnose)", + "Bevorzugtes Werkzeug für lesende Unraid-Diagnose: Host, Array, Datenträger, " + "Docker, Shares, Netzwerk, UPS und Logs. Für dieselbe Anfrage nicht zusätzlich " + "MUA aufrufen; MUA nur für dessen spezielle oder freigegebene Verwaltungsaktionen.", + ), + "mua": ( + "MUA (Unraid-Verwaltung)", + "Nur für ausdrücklich benötigte MUA-spezifische oder freigegebene Unraid-" + "Verwaltungsaktionen. Für reine Statusabfragen und Diagnosen stattdessen " + "Unraid (Systemdiagnose) verwenden; niemals beide parallel ausprobieren.", + ), + } + changed = False + for connection in connections: + if not isinstance(connection, dict): + continue + info = connection.get("info") + if not isinstance(info, dict): + info = {} + connection["info"] = info + identity = str(info.get("id", "")).lower() + url = str(connection.get("url", "")).lower() + if identity in descriptions: + match = identity + elif "192.168.1.2:3002" in url: + match = "mua" + elif "mike-ai-mcp-web" in url: + match = "web-local" + elif "mike-ai-mcp-homeassistant" in url: + match = "homeassistant-local" + elif "mike-ai-mcp-arr" in url: + match = "arr-local" + elif "mike-ai-mcp-unraid-official" in url: + match = "unraid-readonly-local" + else: + continue + name, description = descriptions[match] + if info.get("name") != name or info.get("description") != description: + info["name"] = name + info["description"] = description + changed = True + if changed: + con.execute( + """ + insert into config (key,value,updated_at) values (?,?,?) + on conflict(key) do update set + value=excluded.value, + updated_at=excluded.updated_at + """, + ( + "tool_server.connections", + json.dumps(connections, ensure_ascii=False), + now, + ), + ) print( "OpenWebUI konfiguriert: Default Off=10, Thinking=20, " "Stability Guard=30, Secret Redaction=40, Local Metrics=90, " - "Quick Actions=100, Folgefragen=aus, Piper-TTS=aktiv" + "Quick Actions=100, Folgefragen=aus, Piper-TTS=aktiv, Werkzeugwahl=optimiert" ) PY diff --git a/platform/web-search/web_search_mcp.py b/platform/web-search/web_search_mcp.py index 1aedd82..1902e38 100644 --- a/platform/web-search/web_search_mcp.py +++ b/platform/web-search/web_search_mcp.py @@ -56,12 +56,11 @@ TOOLS = [ { "name": "web_search", "description": ( - "Fast read-only web discovery. Returns a compact list of titles, direct URLs, " - "previews and upstream dates. Search previews are discovery hints, not verified " - "facts. Use web_compare when factual claims must be checked, and web_shop for " - "products or prices. The result already includes the retrieval timestamp; do not " - "call another clock tool. One call is normally sufficient. If no direct match is " - "returned, report not found; do not retry wording variants." + "USE for a quick lookup of current public internet information or candidate URLs. " + "DO NOT use for Home Assistant, Sonarr/Radarr, Unraid, product prices (use " + "web_shop), source-verified claims (use web_compare), or difficult multi-source " + "research (use web_research). Results are unverified discovery hints. Make one " + "call; if nothing relevant is found, say so instead of retrying variants." ), "inputSchema": { "type": "object", @@ -70,7 +69,7 @@ TOOLS = [ "type": "string", "minLength": 2, "maxLength": 500, - "description": "Search query preserving names, constraints and intent.", + "description": "One precise public-web query. Preserve exact names, versions, dates and constraints from the user.", }, "max_results": { "type": "integer", @@ -82,7 +81,7 @@ TOOLS = [ "type": "string", "enum": ["auto", "web", "github", "huggingface"], "default": "auto", - "description": "Use auto unless the requested source is explicit.", + "description": "Use auto normally; choose github or huggingface only when that source is explicitly requested.", }, "include_domains": { "type": "array", @@ -106,15 +105,20 @@ TOOLS = [ { "name": "web_compare", "description": ( - "Read-only evidence comparison for factual research. Discovers sources, crawls up " - "to five public pages, and returns short source-bound evidence. Treat a claim as " - "verified only when it appears in page_evidence; never promote a search preview to " - "a fact. Prefer primary sources when available and cite the supplied URL." + "USE when the user asks to verify a factual claim, check whether information is " + "correct, or answer with trustworthy citations. It discovers and reads up to five " + "public pages. DO NOT use for a simple URL lookup, shopping, or private systems. " + "Only page_evidence is verified; cite its URLs and expose conflicts or missing evidence." ), "inputSchema": { "type": "object", "properties": { - "query": {"type": "string", "minLength": 2, "maxLength": 500}, + "query": { + "type": "string", + "minLength": 2, + "maxLength": 500, + "description": "The exact claim or question to verify, including relevant date/version context.", + }, "max_sources": { "type": "integer", "minimum": 2, @@ -142,11 +146,10 @@ TOOLS = [ { "name": "web_shop", "description": ( - "Read-only product and price lookup designed for small models. Separates the " - "requested retailer from comparison sites, extracts pack-size and price evidence, " - "and marks whether price and direct product URL were actually verified. Recommend " - "only candidates with price_verified_on_retailer=true when the user requested a " - "specific retailer. Never invent a missing link, pack size, availability or price." + "USE ONLY for finding products, current prices, availability, pack sizes, or a " + "purchase link. DO NOT use web_search for shopping. Preserve the requested retailer, " + "brand and total budget. Recommend retailer-specific results only when " + "price_verified_on_retailer=true; never invent price, stock, quantity or URL." ), "inputSchema": { "type": "object", @@ -196,27 +199,32 @@ TOOLS = [ { "name": "web_research", "description": ( - "Read-only multi-source research for difficult questions. Uses local hybrid " - "BM25 plus dense ONNX reranking, crawls only the best pages, removes duplicates, " - "and returns short source-bound evidence. Use depth=deep only when one search is " - "unlikely to be enough. The tool never decides unsupported facts: cite evidence " - "URLs and say insufficient when evidence is missing or conflicting. This tool " - "already performs bounded variants internally; never follow it with more searches " - "for the same request." + "USE ONLY for difficult, broad, or niche questions that require several sources, " + "cross-checking, or discovery variants. For one factual claim use web_compare; for " + "a quick lookup use web_search; for products use web_shop. This tool already runs " + "bounded variants internally, so never repeat the same research with another web " + "tool. Cite returned evidence URLs and state when evidence is insufficient." ), "inputSchema": { "type": "object", "properties": { - "query": {"type": "string", "minLength": 2, "maxLength": 500}, + "query": { + "type": "string", + "minLength": 2, + "maxLength": 500, + "description": "The complete research question, including scope, timeframe, versions and comparison criteria.", + }, "depth": { "type": "string", "enum": ["quick", "deep"], "default": "quick", + "description": "Use quick by default. Use deep only for genuinely complex or poorly indexed topics.", }, "backend": { "type": "string", "enum": ["auto", "web", "github", "huggingface"], "default": "auto", + "description": "Use auto unless the user explicitly restricts research to GitHub or Hugging Face.", }, "max_sources": { "type": "integer",