Improve external service discovery and tool routing
This commit is contained in:
@@ -65,6 +65,15 @@ failure. For public information make at most one focused fallback attempt with
|
||||
the general web tool, then synthesize the available evidence or stop clearly.
|
||||
Never enter a fallback or synonym-search loop.
|
||||
|
||||
Before designing, installing or migrating a backend, query the versioned
|
||||
external-service catalog and then the listed specialist tool. Existing services
|
||||
on Unraid or elsewhere in the home network are dependencies to integrate, not
|
||||
components to duplicate. If inventory or specialist verification is missing,
|
||||
disabled, unreachable or inconclusive, stop and ask the user. Never fill that
|
||||
knowledge gap by proposing or deploying a replacement service. A duplicate is
|
||||
allowed only when the user explicitly requests migration, replacement,
|
||||
redundancy or an isolated experiment after the existing service was identified.
|
||||
|
||||
For models and GPU services, introduce changes only through the experimental
|
||||
profile or an isolated container. Change one variable at a time, record source,
|
||||
license, revision, size and SHA256, account for weights, KV cache, projector,
|
||||
|
||||
@@ -0,0 +1,44 @@
|
||||
{
|
||||
"version": 1,
|
||||
"updated": "2026-08-23",
|
||||
"scope": "Verified external services used by Athena; no credentials or secret values.",
|
||||
"services": [
|
||||
{
|
||||
"id": "unraid",
|
||||
"name": "HomeServer Unraid",
|
||||
"location": "home-network",
|
||||
"host": "homeserver.fritz.box",
|
||||
"address": "192.168.1.2",
|
||||
"port": 5001,
|
||||
"protocol": "https",
|
||||
"probe": "tcp",
|
||||
"specialist_tool": "unraid-readonly-local",
|
||||
"purpose": "Existing NAS, Docker host and storage platform. Inspect it before planning any replacement service on Athena."
|
||||
},
|
||||
{
|
||||
"id": "mua",
|
||||
"name": "MUA Unraid management",
|
||||
"location": "home-network",
|
||||
"host": "192.168.1.2",
|
||||
"address": "192.168.1.2",
|
||||
"port": 3002,
|
||||
"protocol": "http",
|
||||
"probe": "tcp",
|
||||
"specialist_tool": "mua",
|
||||
"purpose": "Explicitly enabled Unraid management actions; not the default diagnostic path."
|
||||
},
|
||||
{
|
||||
"id": "deemix",
|
||||
"name": "Existing Deemix on Unraid",
|
||||
"location": "unraid-container",
|
||||
"host": "192.168.1.2",
|
||||
"address": "192.168.1.2",
|
||||
"port": 6595,
|
||||
"protocol": "http",
|
||||
"probe": "http-head",
|
||||
"probe_path": "/",
|
||||
"specialist_tool": "unraid-readonly-local",
|
||||
"purpose": "Existing Deemix backend and download location. An Athena MCP must integrate this instance over WireGuard; it must not create a second Deemix unless the user explicitly requests migration or replacement."
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -187,6 +187,16 @@ class AutoToolSelectorTests(unittest.IsolatedAsyncioTestCase):
|
||||
["server:mcp:github-local", "server:mcp:athena-operator-local"],
|
||||
)
|
||||
|
||||
async def test_existing_unraid_backend_beats_duplicate_operator_plan(self):
|
||||
result = await self._select(
|
||||
"Baue aus https://github.com/foo/deemix einen MCP. Deemix läuft bereits als Container auf Unraid; prüfe ihn zuerst."
|
||||
)
|
||||
self.assertEqual(
|
||||
result["tool_ids"],
|
||||
["server:mcp:github-local", "server:mcp:unraid-readonly-local"],
|
||||
)
|
||||
self.assertIn("Never compensate", result["messages"][0]["content"])
|
||||
|
||||
async def test_github_and_explicit_web_are_bounded_to_two(self):
|
||||
result = await self._select(
|
||||
"Prüfe dieses GitHub Repository und suche zusätzlich im Netz nach Nutzerstimmen."
|
||||
|
||||
@@ -30,16 +30,22 @@ def main():
|
||||
state = base / "state"
|
||||
runtime = base / "runtime.json"
|
||||
(repo / "docs").mkdir(parents=True)
|
||||
(repo / "config").mkdir(parents=True)
|
||||
docs.mkdir()
|
||||
(repo / "docs/PLATFORM_OVERVIEW.md").write_text("# Athena\nRouter and recovery.\n")
|
||||
(repo / "docs/OPERATIONS.md").write_text("# Operations\nUse bounded tools.\n")
|
||||
(docs / "PLATFORM_OVERVIEW.md").write_text("# Athena\nRouter and recovery.\n")
|
||||
(repo / "config/service-catalog.json").write_text(json.dumps({
|
||||
"version": 1,
|
||||
"services": [{"id": "test", "name": "Test", "address": "127.0.0.1", "port": 9, "protocol": "tcp", "probe": "tcp"}],
|
||||
}))
|
||||
runtime.write_text(json.dumps({"generated_unix": 4102444800, "generated_at": "2100-01-01T00:00:00Z", "source_commit": "abc", "containers": []}))
|
||||
m = load_module(repo, docs, runtime, state)
|
||||
|
||||
assert len(m.TOOLS) == 9
|
||||
assert len(m.TOOLS) == 10
|
||||
assert m.overview()["source"] == "docs/PLATFORM_OVERVIEW.md"
|
||||
assert m.current_state()["available"] is True
|
||||
assert m.external_services()["services"][0]["id"] == "test"
|
||||
assert m.search_knowledge({"query": "recovery", "max_results": 3})["count"] >= 1
|
||||
assert "Operations" in m.read_source({"path": "docs/OPERATIONS.md"})["content"]
|
||||
try:
|
||||
|
||||
@@ -11,7 +11,9 @@ Operator-Kontext in jeden Prompt zu kopieren.
|
||||
|
||||
Der MCP ist zugleich das kontrollierte Pflegefenster für seine eigene
|
||||
Dokumentation. Er ist **kein** allgemeiner Athena-Administrator und erhält
|
||||
weder Docker-Socket noch Shell, Netzwerkzugang, Git-Schlüssel oder Secrets.
|
||||
weder Docker-Socket noch Shell, Git-Schlüssel oder Secrets. Sein Netzzugang ist
|
||||
auf feste, versionierte Erreichbarkeitsprüfungen aus dem Diensteverzeichnis
|
||||
beschränkt; Modellparameter können keine freie Adresse vorgeben.
|
||||
|
||||
## Werkzeuge
|
||||
|
||||
@@ -19,6 +21,7 @@ weder Docker-Socket noch Shell, Netzwerkzugang, Git-Schlüssel oder Secrets.
|
||||
|---|---|
|
||||
| `athena_get_overview` | kurze Architektur und Quellenhierarchie |
|
||||
| `athena_get_current_state` | begrenzter aktueller Snapshot ohne Nutzdaten |
|
||||
| `athena_get_external_services` | vorhandene externe Dienste plus feste, bounded Erreichbarkeitsprüfung |
|
||||
| `athena_search_knowledge` | Suche in Dokumentation und versionierten Quellen |
|
||||
| `athena_read_source` | begrenzter Ausschnitt einer ausgewählten Textdatei |
|
||||
| `athena_get_change_workflow` | verbindlicher Ablauf je Änderungstyp |
|
||||
@@ -45,6 +48,13 @@ Dateiinhalte außerhalb der versionierten Dokumentation oder Secretwerte. Der
|
||||
Container liest nur die erzeugte JSON-Datei. Ein Snapshot älter als drei
|
||||
Minuten gilt als veraltet.
|
||||
|
||||
Das zusätzliche Diensteverzeichnis unter `config/service-catalog.json` enthält
|
||||
nur bekannte interne Namen, Adressen, Ports, Zuständigkeiten und Zwecke, keine
|
||||
Zugangsdaten. `athena_get_external_services` prüft ausschließlich diese festen
|
||||
Einträge. Es ist kein Portscanner, liest keine Antwortinhalte und akzeptiert
|
||||
keine URL oder Adresse aus dem Modell. Für Details bleibt anschließend das im
|
||||
Katalog genannte Fachwerkzeug zuständig.
|
||||
|
||||
## Dokumentationspflege
|
||||
|
||||
Die Pflege ist absichtlich zweistufig:
|
||||
|
||||
@@ -19,6 +19,12 @@ Host-Snapshot ersetzt einen Docker-Socket. Dokumentationsänderungen laufen nur
|
||||
bleiben getrennte, nachzuweisende Abschlussarbeiten. Details stehen in
|
||||
`PLATFORM_CONTEXT_MCP.md`.
|
||||
|
||||
Das versionierte, secret-freie Diensteverzeichnis
|
||||
`config/service-catalog.json` dokumentiert bereits vorhandene externe
|
||||
Abhängigkeiten. Vor der Planung eines neuen Backends muss es gelesen und der
|
||||
Bestand mit dem dort genannten Fachwerkzeug geprüft werden. Ein nicht
|
||||
erreichbares Werkzeug bedeutet „nicht verifiziert“, niemals „nicht vorhanden“.
|
||||
|
||||
## Hardware
|
||||
|
||||
- Debian 13 `trixie`, Kernel 6.12
|
||||
@@ -58,6 +64,10 @@ Open WebUI ---> Profile Router ---> Profile Controller ---> genau ein llama.cpp-
|
||||
+-- Unraid
|
||||
```
|
||||
|
||||
Deemix läuft bereits als Container auf dem Unraid-HomeServer. Eine künftige
|
||||
Deemix-MCP-Integration auf Athena verwendet dieses Backend über WireGuard und
|
||||
erzeugt nicht ungefragt eine zweite Deemix-Instanz.
|
||||
|
||||
Open WebUI und Router veröffentlichen keinen normalen Host-Port. Der
|
||||
WireGuard-Gateway-Container stellt nur die vorgesehenen VPN-Endpunkte bereit.
|
||||
Anwendungscontainer erreichen Heimnetz und Internet fail-closed über WireGuard;
|
||||
|
||||
@@ -37,6 +37,13 @@ Dokumentations-Schreibweg darf erst nach Vorschau und ausdrücklicher Freigabe
|
||||
verwendet werden. Eine lokale Dokumentationsänderung ist ohne separaten
|
||||
Git-Commit/Push und erneuerten Recovery-Koffer nicht abgeschlossen.
|
||||
|
||||
Vor einem neuen Backend, Relay oder MCP liest du zusätzlich mit
|
||||
`athena_get_external_services` das versionierte Diensteverzeichnis. Prüfe den
|
||||
gefundenen Bestand danach mit dem genannten Fachwerkzeug. Scheitert diese
|
||||
Prüfung, stoppst du und meldest die Lücke. Du darfst aus einem Toolfehler oder
|
||||
fehlenden Zugriff niemals ableiten, dass der Dienst nicht existiert, und als
|
||||
Ersatz ungefragt eine zweite Instanz planen.
|
||||
|
||||
## 2. Auftrag und Einsatzumgebung
|
||||
|
||||
MikeAI stellt lokal Inferenz, multimodale Bildanalyse, Bildgenerierung,
|
||||
@@ -235,6 +242,11 @@ Netzzugriff. Kein MCP-Port wird am Host veröffentlicht.
|
||||
| Unraid | Host-, Docker-, Array-, Netzwerk- und Logdiagnose | read-only Standard |
|
||||
| MUA/Admin | eng definierte Unraid-Verwaltung | bewusst aktivieren |
|
||||
|
||||
Der vorhandene Deemix-Dienst läuft auf Unraid und ist als externe Abhängigkeit
|
||||
im Diensteverzeichnis eingetragen. Für ein Deemix-MCP wird standardmäßig nur
|
||||
ein Relay auf Athena gebaut; ein zweites Deemix-Backend erfordert einen
|
||||
ausdrücklichen Migrations-, Ersatz- oder Testauftrag.
|
||||
|
||||
`Athena Operator` ist die zentrale Arbeitsumgebung für Änderungen an Athena.
|
||||
Nutze ihn zum Lesen der tatsächlichen Quellen, Erstellen und Anwenden von
|
||||
Dateiänderungen, Testen, Deployen von Compose-Diensten und MCPs, Verwalten der
|
||||
|
||||
@@ -142,7 +142,7 @@ services:
|
||||
build:
|
||||
context: .
|
||||
dockerfile: Dockerfile.platform-context
|
||||
image: mike-ai/mcp-platform-context:1.0.0
|
||||
image: mike-ai/mcp-platform-context:1.1.0
|
||||
container_name: mike-ai-mcp-platform-context
|
||||
environment:
|
||||
ATHENA_REPO_ROOT: /knowledge/repo
|
||||
@@ -158,7 +158,9 @@ services:
|
||||
- ${PLATFORM_DOCS_DIR:-/opt/mike-ai/stack/docs}:/workspace/docs:rw
|
||||
- ${PLATFORM_CONTEXT_RUNTIME_DIR:-/var/lib/mike-ai-platform-context}:/runtime:ro
|
||||
- ${PLATFORM_CONTEXT_STATE_DIR:-/data/mike-ai-platform-context}:/state:rw
|
||||
networks: [tools]
|
||||
# The context service has no arbitrary URL input. Its only egress use is a
|
||||
# bounded reachability check of endpoints from config/service-catalog.json.
|
||||
networks: [tools, egress]
|
||||
healthcheck:
|
||||
test: ["CMD", "python", "-c", "import socket; s=socket.create_connection(('127.0.0.1',8000),2); s.close()"]
|
||||
interval: 30s
|
||||
@@ -224,6 +226,10 @@ services:
|
||||
UNRAID_RMCP_DISABLE_HTTP_AUTH: "true"
|
||||
UNRAID_NOAUTH: "true"
|
||||
UNRAID_RMCP_ALLOWED_HOSTS: "mike-ai-mcp-unraid-official:8000,mike-ai-mcp-unraid-official,localhost:8000,127.0.0.1:8000"
|
||||
# Public DNS cannot resolve the Fritzbox-only name. Preserve the hostname
|
||||
# used by the TLS endpoint while binding it to the verified home-LAN IP.
|
||||
extra_hosts:
|
||||
- "homeserver.fritz.box:192.168.1.2"
|
||||
volumes:
|
||||
- ${RUNRAID_BINARY:-/usr/local/bin/runraid}:/usr/local/bin/unraid:ro
|
||||
entrypoint: ["/usr/local/bin/unraid"]
|
||||
|
||||
@@ -15,15 +15,18 @@ import calendar
|
||||
import json
|
||||
import os
|
||||
import re
|
||||
import socket
|
||||
import sys
|
||||
import tempfile
|
||||
import time
|
||||
import uuid
|
||||
import urllib.error
|
||||
import urllib.request
|
||||
from pathlib import Path
|
||||
from typing import Any
|
||||
|
||||
|
||||
SERVER_VERSION = "1.0.0"
|
||||
SERVER_VERSION = "1.1.0"
|
||||
REPO_ROOT = Path(os.environ.get("ATHENA_REPO_ROOT", "/knowledge/repo"))
|
||||
DOCS_ROOT = Path(os.environ.get("ATHENA_DOCS_ROOT", "/workspace/docs"))
|
||||
RUNTIME_FILE = Path(os.environ.get("ATHENA_RUNTIME_FILE", "/runtime/runtime.json"))
|
||||
@@ -64,6 +67,18 @@ TOOLS = [
|
||||
),
|
||||
"inputSchema": {"type": "object", "properties": {}, "additionalProperties": False},
|
||||
},
|
||||
{
|
||||
"name": "athena_get_external_services",
|
||||
"description": (
|
||||
"USE before designing or installing an integration that may already run on Unraid "
|
||||
"or elsewhere in the home network. Returns the versioned, secret-free service "
|
||||
"catalog and performs only fixed bounded reachability checks for those catalogued "
|
||||
"endpoints. It accepts no host, URL or port from the model and is not a scanner. "
|
||||
"A failed check means unavailable or unverified; it never authorizes creating a "
|
||||
"duplicate service. Use the listed specialist MCP for detailed current state."
|
||||
),
|
||||
"inputSchema": {"type": "object", "properties": {}, "additionalProperties": False},
|
||||
},
|
||||
{
|
||||
"name": "athena_search_knowledge",
|
||||
"description": (
|
||||
@@ -293,6 +308,63 @@ def current_state() -> dict[str, Any]:
|
||||
return data
|
||||
|
||||
|
||||
def external_services() -> dict[str, Any]:
|
||||
catalog_path = REPO_ROOT / "config/service-catalog.json"
|
||||
try:
|
||||
catalog = json.loads(catalog_path.read_text(encoding="utf-8"))
|
||||
except (OSError, json.JSONDecodeError) as exc:
|
||||
return {
|
||||
"available": False,
|
||||
"error": str(exc),
|
||||
"instruction": "The service inventory is unavailable. Do not infer that a replacement service is needed.",
|
||||
}
|
||||
|
||||
results = []
|
||||
for service in catalog.get("services", [])[:24]:
|
||||
item = {key: service.get(key) for key in (
|
||||
"id", "name", "location", "host", "address", "port", "protocol",
|
||||
"specialist_tool", "purpose",
|
||||
)}
|
||||
address = str(service.get("address", ""))
|
||||
port = int(service.get("port", 0))
|
||||
started = time.monotonic()
|
||||
reachable = False
|
||||
http_status = None
|
||||
error = None
|
||||
try:
|
||||
with socket.create_connection((address, port), timeout=2):
|
||||
reachable = True
|
||||
if service.get("probe") == "http-head":
|
||||
path = str(service.get("probe_path", "/"))
|
||||
url = f"{service.get('protocol', 'http')}://{address}:{port}{path}"
|
||||
request = urllib.request.Request(url, method="HEAD", headers={"User-Agent": "MikeAI-Service-Catalog/1"})
|
||||
try:
|
||||
with urllib.request.urlopen(request, timeout=3) as response:
|
||||
http_status = response.status
|
||||
except urllib.error.HTTPError as exc:
|
||||
http_status = exc.code
|
||||
except (OSError, ValueError, urllib.error.URLError) as exc:
|
||||
error = type(exc).__name__
|
||||
item["check"] = {
|
||||
"reachable": reachable,
|
||||
"http_status": http_status,
|
||||
"elapsed_ms": round((time.monotonic() - started) * 1000),
|
||||
"error_class": error,
|
||||
}
|
||||
results.append(item)
|
||||
return {
|
||||
"available": True,
|
||||
"source": "config/service-catalog.json",
|
||||
"catalog_version": catalog.get("version"),
|
||||
"updated": catalog.get("updated"),
|
||||
"services": results,
|
||||
"instruction": (
|
||||
"Existing catalog entries are architecture constraints, not disposable suggestions. "
|
||||
"If a check or specialist tool fails, report the gap and ask for direction; do not plan a duplicate backend."
|
||||
),
|
||||
}
|
||||
|
||||
|
||||
def candidate_files() -> list[Path]:
|
||||
files: list[Path] = []
|
||||
for path in REPO_ROOT.rglob("*"):
|
||||
@@ -550,6 +622,8 @@ def call_tool(name: str, arguments: dict[str, Any]) -> str:
|
||||
result = overview()
|
||||
elif name == "athena_get_current_state":
|
||||
result = current_state()
|
||||
elif name == "athena_get_external_services":
|
||||
result = external_services()
|
||||
elif name == "athena_search_knowledge":
|
||||
result = search_knowledge(arguments)
|
||||
elif name == "athena_read_source":
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
"""
|
||||
title: MikeAI Auto Tool Selector
|
||||
author: MikeAI
|
||||
version: 2.0.0
|
||||
version: 2.1.0
|
||||
description: Selects a small, relevant set of MCP servers for each user request.
|
||||
"""
|
||||
|
||||
@@ -80,7 +80,10 @@ class Filter:
|
||||
"unless the user's current message explicitly requests that exact change "
|
||||
"and every required preview, confirmation, backup, and validation rule "
|
||||
"of the tool is satisfied. Do not call unrelated tools merely because "
|
||||
"they are available. If the selected tool cannot verify the claim, say so."
|
||||
"they are available. If the selected tool cannot verify the claim, say so. "
|
||||
"A missing, disabled or failed inventory tool is not evidence that a service "
|
||||
"does not exist. Never compensate by planning or installing a duplicate backend; "
|
||||
"stop and request clarification."
|
||||
)
|
||||
messages = body.setdefault("messages", [])
|
||||
for message in messages:
|
||||
@@ -165,9 +168,13 @@ class Filter:
|
||||
),
|
||||
)
|
||||
|
||||
# A specialist source is more precise than public web search. Platform
|
||||
# wins over a generic Docker mention when Athena is explicitly named.
|
||||
if operator:
|
||||
# Existing infrastructure must be inspected before a new integration
|
||||
# is designed. For a GitHub-backed MCP that targets a service already
|
||||
# running on Unraid, source plus Unraid inventory are the two most
|
||||
# useful bounded capabilities; the Operator follows at implementation.
|
||||
if github and unraid:
|
||||
selected.extend(("github", "unraid"))
|
||||
elif operator:
|
||||
selected.append("operator")
|
||||
elif platform:
|
||||
selected.append("platform")
|
||||
|
||||
Reference in New Issue
Block a user