Improve MCP tool selection guidance
This commit is contained in:
+1
-1
@@ -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:
|
||||
|
||||
@@ -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,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.
|
||||
|
||||
@@ -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:
|
||||
|
||||
@@ -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"
|
||||
)
|
||||
@@ -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",
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
@@ -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",
|
||||
|
||||
Reference in New Issue
Block a user