#!/usr/bin/env bash set -Eeuo pipefail CONTAINER=${OPENWEBUI_CONTAINER:-mike-ai-open-webui} ENV_FILE=${NAVIDROME_MCP_ENV_FILE:-/etc/mike-ai/navidrome-mcp.env} die() { printf 'FEHLER: %s\n' "$*" >&2; exit 1; } [[ $EUID -eq 0 ]] || die "Bitte als root ausführen." [[ -s $ENV_FILE ]] || die "Navidrome-Secret-Datei fehlt." [[ $(docker inspect -f '{{.State.Health.Status}}' mike-ai-mcp-navidrome 2>/dev/null || true) == healthy ]] || \ die "Navidrome-MCP ist nicht gesund." [[ $(docker inspect -f '{{.State.Health.Status}}' "$CONTAINER" 2>/dev/null || true) == healthy ]] || \ die "OpenWebUI ist nicht gesund." expect_lastfm=false grep -q '^LASTFM_API_KEY=..' "$ENV_FILE" && expect_lastfm=true result=$(docker exec -i -e EXPECT_LASTFM="$expect_lastfm" "$CONTAINER" python - <<'PY' import asyncio import json import os import sqlite3 from mcp import ClientSession from mcp.client.streamable_http import streamablehttp_client EXPECTED_LASTFM = { "get_similar_artists", "get_similar_tracks", "get_artist_info", "get_top_tracks_by_artist", "get_trending_music", "get_artist_albums", "get_album_info", } PLAYBACK = {"play_songs", "pause", "set_volume"} def find_unanchored(value, path=""): bad = [] if isinstance(value, dict): for key, child in value.items(): here = f"{path}.{key}" if path else key if key == "pattern" and ( not isinstance(child, str) or not child.startswith("^") or not child.endswith("$") ): bad.append(here) bad.extend(find_unanchored(child, here)) elif isinstance(value, list): for index, child in enumerate(value): bad.extend(find_unanchored(child, f"{path}[{index}]")) return bad async def verify(): async with streamablehttp_client( "http://mike-ai-mcp-navidrome:3000/mcp" ) as (read, write, _): async with ClientSession(read, write) as session: await session.initialize() result = await session.list_tools() names = {tool.name for tool in result.tools} bad = [] for tool in result.tools: bad.extend(find_unanchored(tool.inputSchema, tool.name)) if bad: raise SystemExit("Unverankerte JSON-Schema-Patterns: " + ", ".join(bad)) if PLAYBACK & names: raise SystemExit("Playback-Werkzeuge sind auf dem Headless-Host aktiv.") expect_lastfm = os.environ.get("EXPECT_LASTFM") == "true" if expect_lastfm and not EXPECTED_LASTFM <= names: raise SystemExit("Last.fm-Werkzeugkatalog ist unvollständig.") if not expect_lastfm and EXPECTED_LASTFM & names: raise SystemExit("Last.fm-Werkzeuge sind ohne konfigurierten Schlüssel aktiv.") if expect_lastfm: # Public metadata only. Do not print the returned chart data. response = await session.call_tool( "get_trending_music", {"type": "artists", "limit": 1} ) if response.isError: raise SystemExit("Öffentliche Last.fm-Testabfrage ist fehlgeschlagen.") con = sqlite3.connect("/app/backend/data/webui.db") row = con.execute( "select value from config where key=?", ("tool_server.connections",) ).fetchone() connections = json.loads(row[0]) if row else [] ids = { str((connection.get("info") or {}).get("id", "")) for connection in connections if isinstance(connection, dict) } if "navidrome-local" not in ids: raise SystemExit("OpenWebUI-Verbindung navidrome-local fehlt.") print(f"NAVIDROME_ACCEPTANCE_OK tools={len(names)} lastfm={str(expect_lastfm).lower()}") asyncio.run(verify()) PY ) [[ $result == NAVIDROME_ACCEPTANCE_OK\ * ]] || \ die "Navidrome-Abnahme lieferte keinen gültigen Erfolgsmarker." printf '%s\n' "$result"