diff --git a/README.md b/README.md index 9e63425..370b3be 100644 --- a/README.md +++ b/README.md @@ -231,6 +231,25 @@ Zusätzlich kann jedes Werkzeug einzeln nach Risikostufe freigegeben werden: Deaktivierte Tools werden vom MCP-Server gefiltert — sie erscheinen nicht in `tools/list` und können nicht aufgerufen werden (→ `ERROR: Tool disabled`). +### Optionaler Agent-Werkzeugcontainer + +MUA kann einen allgemeinen Werkzeugcontainer dynamisch über das Docker-Label +`mike.ai.role=toolbox` erkennen. Sobald mindestens ein solcher Container +existiert, wird in der WebGUI der Schalter **„Werkzeugcontainer für +KI-Agenten bekanntgeben“** verfügbar. Fehlt das Label, bleibt er ausgegraut. + +Bei aktiviertem Schalter nennt MUA den aktuell erkannten Containernamen und +die Nutzung über `docker exec` in seinen MCP-Anweisungen. Der Name ist nicht +fest in MUA eingebaut. Wird der Container ersetzt, genügt dasselbe Label am +Nachfolger. Die eigentliche Ausführung benötigt das separat freigegebene +Werkzeug `unraid_system_shell`. + +Beispiel für Docker beziehungsweise DockerMan `ExtraParams`: + +```text +--label=mike.ai.role=toolbox +``` + Bei freigegebenem Vollzugriff ist `unraid_system_shell` das direkte Terminal auf dem Unraid-Host. Agenten sollen es für normale Docker-, Datei- und Administrationsaufträge unmittelbar verwenden und weder den MUA-Quellcode @@ -273,6 +292,7 @@ Prozess startet automatisch. Zusätzlich führt der POST-INSTALL-Hook im ```ini MUA_API_KEY=a1b2c3d4... MUA_ENABLED_TOOLS=unraid_docker_list,unraid_docker_inspect,unraid_network_list +MUA_TOOLBOX_ENABLED=false ``` Sonderwerte: `all` aktiviert ausdrücklich alles, `none` deaktiviert alles. diff --git a/dist/mua-2026.08.29.r026-x86_64-1.txz b/dist/mua-2026.08.29.r026-x86_64-1.txz new file mode 100644 index 0000000..fa442ac Binary files /dev/null and b/dist/mua-2026.08.29.r026-x86_64-1.txz differ diff --git a/docs/HERMES.md b/docs/HERMES.md index 844cab1..62d1829 100644 --- a/docs/HERMES.md +++ b/docs/HERMES.md @@ -118,6 +118,12 @@ Hermes-Containerterminal als Zwischenstation verwenden. Normale Befehle und mehrzeilige Skripte laufen synchron; nur Arbeiten über 30 Minuten verwenden die asynchronen Job-Werkzeuge. +Wenn in der MUA-WebGUI der optionale Agent-Werkzeugcontainer aktiviert ist, +liefert MUA dessen dynamisch per Label erkannten Namen bereits in den +MCP-Anweisungen. Hermes verwendet ihn dann über `unraid_system_shell` und +`docker exec`; im Hermes-Container selbst werden keine Hilfsprogramme +nachinstalliert. MUA setzt keinen festen Containernamen voraus. + ## 5. Entfernen Nur die Hermes-Verbindung entfernen, nicht das MUA-Plugin auf Unraid: diff --git a/mcp/helpers.php b/mcp/helpers.php index e56fc1a..4efa8c9 100644 --- a/mcp/helpers.php +++ b/mcp/helpers.php @@ -10,7 +10,7 @@ error_reporting(E_ALL); ini_set('display_errors', 0); // Fehler werden als JSON-RPC-Error zurückgegeben, nicht als HTML -const MUA_VERSION = '2026.08.24.r017'; +const MUA_VERSION = '2026.08.29.r026'; const MUA_SERVER_NAME = 'mua'; /** diff --git a/mcp/server.php b/mcp/server.php index 5de77e8..287534f 100644 --- a/mcp/server.php +++ b/mcp/server.php @@ -21,7 +21,7 @@ const MUA_PORT = 3002; const MUA_BIND = '0.0.0.0'; const MUA_SESSION_TTL = 3600; // Sekunden const MUA_SERVER_NAME = 'mua'; -const MUA_VERSION = '2026.08.24.r017'; +const MUA_VERSION = '2026.08.29.r026'; // ── Session-Management (in-memory) ─────────────────────────────────────── $sessions = []; // session_id => ['created' => time, 'initialized' => bool] diff --git a/package.json b/package.json index c107c79..d87df46 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "mua", - "version": "2026.08.28.r025", + "version": "2026.08.29.r026", "description": "Mikes Unraid Agent - MCP over HTTP (Streamable HTTP) for Unraid", "type": "module", "main": "src/index.ts", diff --git a/plugin/mua.plg b/plugin/mua.plg index 4fc99d1..fd817e1 100644 --- a/plugin/mua.plg +++ b/plugin/mua.plg @@ -2,13 +2,13 @@ - + - - + + ]> +### 2026.08.29.r026 +- Erkennt optionale Agent-Werkzeugcontainer dynamisch über `mike.ai.role=toolbox` statt über einen fest eingebauten Namen. +- Die WebGUI bietet den Toolbox-Schalter nur bei vorhandenem Label an und zeigt andernfalls eine konkrete Einrichtungsanleitung. +- Bei Aktivierung erhalten MCP-Clients den aktuellen Containernamen und die Nutzung über `unraid_system_shell`/`docker exec` als Serverhinweis. + ### 2026.08.28.r025 - Die freie Unraid-Shell ist als direkter Host-Terminalweg beschrieben, damit Agenten sie statt lokaler Proxy-Skripte oder MUA-Quellcodeanalyse verwenden. - Mehrzeilige Skripte bis 256 KiB und synchrone Laufzeiten bis 30 Minuten werden unterstützt; nur noch längere Arbeiten benötigen die vorhandenen asynchronen Job-Werkzeuge. @@ -157,7 +162,7 @@ Das .txz enthält: install/doinst.sh (läuft nach Installation) =========================================== --> - + &txzURL; &txzSHA256; diff --git a/plugin/plugin.json b/plugin/plugin.json index 31b549d..fb47eca 100644 --- a/plugin/plugin.json +++ b/plugin/plugin.json @@ -1,7 +1,7 @@ { "name": "mua", "author": "Michael", - "version": "2026.08.28.r025", + "version": "2026.08.29.r026", "minver": "7.0.0", "pluginDirectory": "/usr/local/emhttp/plugins/mua", "configDirectory": "/boot/config/plugins/mua", diff --git a/scripts/mua.page b/scripts/mua.page index 2ebe968..7b9da8b 100644 --- a/scripts/mua.page +++ b/scripts/mua.page @@ -96,6 +96,7 @@ if ($_SERVER['REQUEST_METHOD'] === 'POST') { if ($action === 'preset_operator') $payload = ['enabledTools' => $operator, 'allToolsEnabled' => false]; if ($action === 'preset_none') $payload = ['enabledTools' => [], 'allToolsEnabled' => false]; if ($action === 'preset_all') $payload = ['enabledTools' => [], 'allToolsEnabled' => true]; + if ($action === 'save_toolbox') $payload = ['toolboxEnabled' => isset($_POST['toolbox_enabled'])]; if ($action === 'save_tools') { $selected = []; foreach (($config['allTools'] ?? []) as $tool) { @@ -128,6 +129,10 @@ if ($_SERVER['REQUEST_METHOD'] === 'POST') { $enabled_tools = $config['enabledTools'] ?? []; $all_tools_enabled = (bool)($config['allToolsEnabled'] ?? false); $all_tools = $config['allTools'] ?? []; +$toolbox = is_array($config['toolbox'] ?? null) ? $config['toolbox'] : []; +$toolbox_available = (bool)($toolbox['available'] ?? false); +$toolbox_enabled = (bool)($toolbox['enabled'] ?? false); +$toolbox_containers = is_array($toolbox['containers'] ?? null) ? $toolbox['containers'] : []; $risk_groups = ['read' => [], 'active' => [], 'write' => [], 'critical' => []]; foreach ($all_tools as $tool) { if (!is_array($tool)) $tool = ['name' => $tool, 'description' => '', 'risk' => 'read']; @@ -145,6 +150,7 @@ $active_count = $all_tools_enabled ? count($all_tools) : count($enabled_tools); ?>

MUA · Mikes Unraid Agent

Sicherer MCP-Zugriff auf Docker, Netzwerk und Unraid-Systemfunktionen
@@ -160,6 +166,16 @@ $active_count = $all_tools_enabled ? count($all_tools) : count($enabled_tools);

MCP-Endpunkt

Streamable HTTP · JSON-RPC 2.0 · Bearer-Authentifizierung erforderlich

+

Agent-Werkzeugcontainer

+ +

✅ Gefunden: $container): ?> ()

+
+ +

Kein Werkzeugcontainer erkannt

+ + +
+

Schnelle Sicherheitsprofile

„Nur Lesen“ ist die empfohlene Dauerstellung. Weitergehende Rechte nur bei konkretem Bedarf freigeben.

">
diff --git a/src/auth.ts b/src/auth.ts index 35559fd..3a96d90 100644 --- a/src/auth.ts +++ b/src/auth.ts @@ -31,6 +31,7 @@ export interface MUAConfig { apiKey: string; enabledTools: string[]; allToolsEnabled: boolean; + toolboxEnabled: boolean; } // Sichere Grundeinstellung für Neuinstallationen. Schreibzugriffe, aktive @@ -68,6 +69,7 @@ export function parseConfig(content: string): MUAConfig { apiKey: "", enabledTools: [...SAFE_DEFAULT_TOOLS], allToolsEnabled: false, + toolboxEnabled: false, }; for (const line of content.split("\n")) { const trimmed = line.trim(); @@ -92,6 +94,8 @@ export function parseConfig(content: string): MUAConfig { .filter((s) => s.length > 0); cfg.allToolsEnabled = false; } + } else if (key === "MUA_TOOLBOX_ENABLED") { + cfg.toolboxEnabled = ["1", "true", "yes", "on"].includes(value.toLowerCase()); } } // r019 replaces repeated single-container update calls with an idempotent @@ -139,7 +143,9 @@ function loadConfig(): MUAConfig { if (envToken && envToken.length > 0) { const envTools = process.env["MUA_ENABLED_TOOLS"] ?? ""; cachedConfig = parseConfig( - `MUA_API_KEY=${envToken}\nMUA_ENABLED_TOOLS=${envTools || "none"}\n`, + `MUA_API_KEY=${envToken}\n` + + `MUA_ENABLED_TOOLS=${envTools || "none"}\n` + + `MUA_TOOLBOX_ENABLED=${process.env["MUA_TOOLBOX_ENABLED"] ?? "false"}\n`, ); return cachedConfig; } @@ -162,6 +168,7 @@ function loadConfig(): MUAConfig { apiKey: newToken, enabledTools: [...SAFE_DEFAULT_TOOLS], allToolsEnabled: false, + toolboxEnabled: false, }; try { @@ -190,6 +197,8 @@ function writeConfigFile(cfg: MUAConfig): void { `MUA_API_KEY=${cfg.apiKey}`, "# Aktive Tools (all = alle, none = keine, oder kommagetrennte Tool-Namen)", `MUA_ENABLED_TOOLS=${toolsValue}`, + "# Optionalen, per Docker-Label erkannten Werkzeugcontainer bekanntgeben", + `MUA_TOOLBOX_ENABLED=${cfg.toolboxEnabled ? "true" : "false"}`, "", ].join("\n"); writeFileSync(CONFIG_FILE, content, { mode: 0o600 }); @@ -242,6 +251,16 @@ export function setEnabledTools(names: string[], allToolsEnabled = false): void saveConfig(cfg); } +export function getToolboxEnabled(): boolean { + return loadConfig().toolboxEnabled; +} + +export function setToolboxEnabled(enabled: boolean): void { + const cfg = loadConfig(); + cfg.toolboxEnabled = enabled; + saveConfig(cfg); +} + /** * Prüft, ob ein Tool aktiv ist. * true = aktiv, false = deaktiviert. diff --git a/src/helpers.ts b/src/helpers.ts index 8925306..5668855 100644 --- a/src/helpers.ts +++ b/src/helpers.ts @@ -15,7 +15,7 @@ import { existsSync, mkdirSync, readFileSync, rmSync, statSync, writeFileSync } // ── Konstanten ────────────────────────────────────────────────────────── export const MUA_SERVER_NAME = "mua"; -export const MUA_VERSION = "2026.08.28.r025"; +export const MUA_VERSION = "2026.08.29.r026"; export const MUA_PROTOCOL_VERSION = "2025-03-26"; export const PHP_HELPER = "/usr/local/bin/unraid-docker-mcp-helper.php"; export const STATUS_HELPER = "/usr/local/bin/unraid-mcp-status-helper.php"; @@ -314,6 +314,40 @@ export async function dockerExec(cmd: string, timeoutSec = 60): Promise return runLocal("docker", `/usr/bin/docker ${cmd} 2>&1`, timeoutSec); } +export interface ToolboxContainer { + name: string; + image: string; + state: string; + status: string; +} + +/** Find self-declared agent toolboxes without relying on a container name. */ +export async function discoverToolboxContainers(): Promise { + const raw = await dockerExec( + "ps -a --filter 'label=mike.ai.role=toolbox' --format '{{json .}}'", + 30, + ); + if (!raw.trim()) return []; + const rows: ToolboxContainer[] = []; + for (const line of raw.split("\n")) { + if (!line.trim().startsWith("{")) continue; + try { + const item = JSON.parse(line) as Record; + const name = String(item["Names"] ?? "").trim(); + if (!name) continue; + rows.push({ + name, + image: String(item["Image"] ?? ""), + state: String(item["State"] ?? "").toLowerCase(), + status: String(item["Status"] ?? ""), + }); + } catch { + // Ignore unrelated Docker diagnostics; absence remains a valid result. + } + } + return rows; +} + /** * Delegiert eine Write-Operation an den PHP-Helper. */ diff --git a/src/index.ts b/src/index.ts index 7375d86..5dd752a 100644 --- a/src/index.ts +++ b/src/index.ts @@ -17,6 +17,7 @@ import { MUA_SERVER_NAME, MUA_VERSION, MUA_PROTOCOL_VERSION, + discoverToolboxContainers, } from "./helpers"; import { TOOLS, toolByName, getToolRisk } from "./tools"; import { @@ -27,6 +28,8 @@ import { getMaskedApiKey, generateApiKey, setEnabledTools, + getToolboxEnabled, + setToolboxEnabled, } from "./auth"; import { randomBytes } from "node:crypto"; import { spawn } from "node:child_process"; @@ -40,6 +43,40 @@ const MAX_CONCURRENT_TOOL_CALLS = Number(process.env["MUA_MAX_CONCURRENT_TOOL_CA const CORS_ORIGIN = process.env["MUA_CORS_ORIGIN"] ?? ""; const AUDIT_LOG = process.env["MUA_AUDIT_LOG"] ?? "/var/log/plugins/mua-audit.log"; +async function toolboxGuidance(): Promise<{ + available: boolean; + enabled: boolean; + containers: Awaited>; + instruction: string; +}> { + let containers: Awaited> = []; + try { + containers = await discoverToolboxContainers(); + } catch { + // Docker can be temporarily unavailable during Unraid startup. + } + const available = containers.length > 0; + const enabled = getToolboxEnabled() && available; + let instruction = ""; + if (enabled) { + const running = containers.filter((c) => c.state === "running"); + if (running.length > 0) { + instruction = isToolEnabled("unraid_system_shell") + ? `Optional agent toolbox available: ${running.map((c) => c.name).join(", ")}. ` + + "When a command-line utility is missing, use unraid_system_shell and run it with " + + "docker exec inside a labeled toolbox container. Install missing utilities there, " + + "not in the MCP client or Hermes container." + : `Agent toolbox detected (${running.map((c) => c.name).join(", ")}), but ` + + "unraid_system_shell is disabled, so the toolbox cannot currently be used through MUA."; + } else { + instruction = + `Agent toolbox configured but currently stopped: ${containers.map((c) => c.name).join(", ")}. ` + + "Do not assume its tools are available until the container is running."; + } + } + return { available, enabled, containers, instruction }; +} + // ── JSON-RPC Helpers ──────────────────────────────────────────────────── function rpcResult(id: number | string | null, result: unknown) { return { jsonrpc: "2.0", id, result }; @@ -104,11 +141,13 @@ async function handleMcpRequest( if (method === "initialize") { const sid = newSessionId(); touchSession(sid); + const toolbox = await toolboxGuidance(); return { response: rpcResult(id, { protocolVersion: MUA_PROTOCOL_VERSION, capabilities: { tools: {} }, serverInfo: { name: MUA_SERVER_NAME, version: MUA_VERSION }, + ...(toolbox.instruction ? { instructions: toolbox.instruction } : {}), }), sessionId: sid, }; @@ -123,13 +162,17 @@ async function handleMcpRequest( // ── tools/list ──────────────────────────────────────────────────────── if (method === "tools/list") { if (sessionId) touchSession(sessionId); + const toolbox = await toolboxGuidance(); // Tool-Filter: nur aktive Tools anzeigen const activeTools = TOOLS.filter((t) => isToolEnabled(t.name)); return { response: rpcResult(id, { tools: activeTools.map((t) => ({ name: t.name, - description: t.description, + description: + t.name === "unraid_system_shell" && toolbox.instruction + ? `${t.description} ${toolbox.instruction}` + : t.description, inputSchema: t.inputSchema, })), }), @@ -421,6 +464,7 @@ try { // GET: Config lesen if (method === "GET") { + const toolbox = await toolboxGuidance(); return Response.json({ apiKeyConfigured: true, apiKeyMasked: getMaskedApiKey(), @@ -431,6 +475,13 @@ try { description: t.description, risk: getToolRisk(t.name), })), + toolbox: { + configured: getToolboxEnabled(), + available: toolbox.available, + enabled: toolbox.enabled, + label: "mike.ai.role=toolbox", + containers: toolbox.containers, + }, }); } @@ -452,13 +503,25 @@ try { .filter((t): t is string => typeof t === "string" && known.has(t)); setEnabledTools(tools, body["allToolsEnabled"] === true); } + if (typeof body["toolboxEnabled"] === "boolean") { + const toolbox = await toolboxGuidance(); + setToolboxEnabled(body["toolboxEnabled"] === true && toolbox.available); + } + const toolbox = await toolboxGuidance(); return Response.json({ ok: true, generatedApiKey, apiKeyMasked: getMaskedApiKey(), enabledTools: getEnabledTools(), allToolsEnabled: getAllToolsEnabled(), + toolbox: { + configured: getToolboxEnabled(), + available: toolbox.available, + enabled: getToolboxEnabled() && toolbox.available, + label: "mike.ai.role=toolbox", + containers: toolbox.containers, + }, }); } diff --git a/src/security.test.ts b/src/security.test.ts index 27dd98e..5270143 100644 --- a/src/security.test.ts +++ b/src/security.test.ts @@ -23,6 +23,16 @@ describe("secure tool configuration", () => { expect(cfg.enabledTools).toEqual([]); }); + test("toolbox integration is opt-in and parsed independently", () => { + const off = parseConfig("MUA_API_KEY=test\nMUA_ENABLED_TOOLS=none\n"); + const on = parseConfig( + "MUA_API_KEY=test\nMUA_ENABLED_TOOLS=none\nMUA_TOOLBOX_ENABLED=true\n", + ); + expect(off.toolboxEnabled).toBe(false); + expect(on.toolboxEnabled).toBe(true); + expect(on.enabledTools).toEqual([]); + }); + test("an enabled legacy update also exposes the safer batch update", () => { const cfg = parseConfig( "MUA_API_KEY=test\nMUA_ENABLED_TOOLS=unraid_docker_update\n",