Improve MCP tool selection guidance

This commit is contained in:
Mikei386
2026-08-21 20:34:40 +02:00
parent 6f14f62dd1
commit a194a2941d
8 changed files with 184 additions and 31 deletions
+1 -1
View File
@@ -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:
+1
View File
@@ -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. |
+19
View File
@@ -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.
+3
View File
@@ -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:
+38
View File
@@ -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"
)
+3 -3
View File
@@ -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",
+85 -1
View File
@@ -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
+34 -26
View File
@@ -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",