diff --git a/dist/mua-2026.08.25.r024-x86_64-1.txz b/dist/mua-2026.08.25.r024-x86_64-1.txz new file mode 100644 index 0000000..06fb756 Binary files /dev/null and b/dist/mua-2026.08.25.r024-x86_64-1.txz differ diff --git a/mcp/helpers.php b/mcp/helpers.php index 349ccb9..e56fc1a 100644 --- a/mcp/helpers.php +++ b/mcp/helpers.php @@ -197,7 +197,7 @@ function sanitize_log_output(string $text, int $max_chars = 50000): string { /** * Kompakte Container-Inspect-Ausgabe. */ -function compact_container_inspect(string $raw): string { +function compact_container_inspect(string $raw, string $detail = 'summary'): string { $data = json_decode($raw, true); if (json_last_error() !== JSON_ERROR_NONE || !is_array($data)) { return $raw; @@ -218,17 +218,26 @@ function compact_container_inspect(string $raw): string { 'Image' => $data['Config']['Image'] ?? '', 'NetworkMode' => $data['HostConfig']['NetworkMode'] ?? '', 'Ports' => $data['NetworkSettings']['Ports'] ?? [], - 'Env' => $data['Config']['Env'] ?? [], - 'Mounts' => array_map(function ($m) { + 'RestartCount' => $data['RestartCount'] ?? 0, + 'Created' => $data['Created'] ?? '', + ]; + if ($detail === 'full') { + $compact['Env'] = array_map(function ($entry) { + if (!is_string($entry) || !str_contains($entry, '=')) return $entry; + [$name, $value] = explode('=', $entry, 2); + if (preg_match('/(KEY|TOKEN|SECRET|PASS|AUTH|COOKIE|ARL)/i', $name)) { + return $name . '='; + } + return $name . '=' . $value; + }, $data['Config']['Env'] ?? []); + $compact['Mounts'] = array_map(function ($m) { return [ 'Type' => $m['Type'] ?? '', 'Source' => $m['Source'] ?? '', 'Destination' => $m['Destination'] ?? '', ]; - }, $data['Mounts'] ?? []), - 'RestartCount' => $data['RestartCount'] ?? 0, - 'Created' => $data['Created'] ?? '', - ]; + }, $data['Mounts'] ?? []); + } return json_encode($compact, JSON_UNESCAPED_SLASHES | JSON_UNESCAPED_UNICODE); } diff --git a/mcp/selftest.php b/mcp/selftest.php index b398d00..91e261a 100644 --- a/mcp/selftest.php +++ b/mcp/selftest.php @@ -68,6 +68,24 @@ try { check('container_runtime_summary() funktioniert', false, $e->getMessage()); } +// ── 3b. Kompakte Inspect-Ausgabe ───────────────────────────────────────── +$fixture = json_encode([[ + 'Id' => str_repeat('a', 64), + 'Name' => '/test', + 'State' => ['Status' => 'running', 'Running' => true, 'Pid' => 42, 'ExitCode' => 0], + 'Config' => ['Image' => 'example:latest', 'Env' => ['SECRET=value']], + 'HostConfig' => ['NetworkMode' => 'bridge'], + 'NetworkSettings' => ['Ports' => []], + 'Mounts' => [['Type' => 'bind', 'Source' => '/secret', 'Destination' => '/data']], + 'RestartCount' => 0, + 'Created' => 'now', +]]); +$summaryInspect = json_decode(compact_container_inspect($fixture), true); +$fullInspect = json_decode(compact_container_inspect($fixture, 'full'), true); +check('Inspect summary laesst Env aus', !isset($summaryInspect['Env']) && !isset($summaryInspect['Mounts'])); +check('Inspect full enthaelt Env und Mounts', isset($fullInspect['Env']) && isset($fullInspect['Mounts'])); +check('Inspect full maskiert Secrets', ($fullInspect['Env'][0] ?? '') === 'SECRET='); + // ── 4. Netzwerk-Inventur ───────────────────────────────────────────────── echo "\n[4] Netzwerk-Inventur\n"; try { diff --git a/mcp/server.php b/mcp/server.php index d42c2d8..5de77e8 100644 --- a/mcp/server.php +++ b/mcp/server.php @@ -95,8 +95,12 @@ function call_tool(string $name, array $args): string { case 'unraid_docker_inspect': $container = validate_name($args['container'] ?? null, 'container'); + $detail = $args['detail'] ?? 'summary'; + if (!in_array($detail, ['summary', 'full'], true)) { + throw new InvalidArgumentException('detail must be summary or full'); + } $raw = docker_exec("inspect --type container -- $container"); - return compact_container_inspect($raw); + return compact_container_inspect($raw, $detail); case 'unraid_docker_logs': $container = validate_name($args['container'] ?? null, 'container'); diff --git a/mcp/tools.php b/mcp/tools.php index 6314206..9e6d037 100644 --- a/mcp/tools.php +++ b/mcp/tools.php @@ -27,13 +27,15 @@ function mua_tools(): array { ], [ 'name' => 'unraid_docker_inspect', - 'description' => 'Inspect a single Docker container in detail (state, image, ports, env, mounts).', + 'description' => "Inspect one known Docker container directly. Default summary returns state, image, network mode and ports only; request detail='full' only when environment variables or mounts are required.", 'inputSchema' => [ 'type' => 'object', 'properties' => [ 'container' => ['type' => 'string', 'description' => 'Container name or ID'], + 'detail' => ['type' => 'string', 'enum' => ['summary', 'full'], 'description' => "Use 'summary' by default. 'full' additionally returns redacted environment and mounts."], ], 'required' => ['container'], + 'additionalProperties' => false, ], ], [ diff --git a/package.json b/package.json index f88cec8..44313a3 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "mua", - "version": "2026.08.24.r023", + "version": "2026.08.25.r024", "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 a1cbe48..8440923 100644 --- a/plugin/mua.plg +++ b/plugin/mua.plg @@ -2,13 +2,13 @@ - + - - + + ]> +### 2026.08.25.r024 +- Container-Inspect liefert standardmäßig nur Status, Image, Netzwerkmodus und Ports; Umgebungsvariablennamen und Mounts sind nur noch über `detail=full` enthalten. +- Reduziert große MCP-Ausgaben bei gezielten Containerdiagnosen und hält Secrets weiterhin vollständig aus den Antworten heraus. + ### 2026.08.24.r023 - Begrenzt die Ausgabe der Nur-Lese-Shell bereits serverseitig auf standardmäßig 12.000 Zeichen und erlaubt ein explizites Limit von 1.000 bis 30.000 Zeichen. - Präzisiert die Werkzeugbeschreibung für eine zielgerichtete Diagnosekette statt breiter Konfigurations- und Verzeichnisabfragen. @@ -149,7 +153,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 a2a5e50..8710f39 100644 --- a/plugin/plugin.json +++ b/plugin/plugin.json @@ -1,7 +1,7 @@ { "name": "mua", "author": "Michael", - "version": "2026.08.24.r023", + "version": "2026.08.25.r024", "minver": "7.0.0", "pluginDirectory": "/usr/local/emhttp/plugins/mua", "configDirectory": "/boot/config/plugins/mua", diff --git a/src/helpers.ts b/src/helpers.ts index 8f270d6..4ca440f 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.24.r023"; +export const MUA_VERSION = "2026.08.25.r024"; 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"; @@ -475,7 +475,10 @@ export function sanitizeLogOutput(text: string, maxChars = 50000): string { } // ── Compact Container Inspect ─────────────────────────────────────────── -export async function compactContainerInspect(container: string): Promise { +export async function compactContainerInspect( + container: string, + detail: "summary" | "full" = "summary", +): Promise { const raw = await dockerExec(`inspect --type container -- ${container}`); let data: unknown; try { @@ -485,7 +488,7 @@ export async function compactContainerInspect(container: string): Promise; - const compact = { + const compact: Record = { Id: (d.Id ?? "").slice(0, 12), Name: d.Name ?? "", State: { @@ -497,19 +500,21 @@ export async function compactContainerInspect(container: string): Promise - typeof entry === "string" ? entry.split("=", 1)[0] : "", - ).filter((name: string) => name !== ""), - Mounts: (d.Mounts ?? []).map((m: any) => ({ - Type: m.Type ?? "", - Source: m.Source ?? "", - Destination: m.Destination ?? "", - })), RestartCount: d.RestartCount ?? 0, Created: d.Created ?? "", }; + if (detail === "full") { + // Environment-Werte enthalten sehr häufig API-Keys, Passwörter und + // interne URLs. Für Diagnosezwecke reichen die Variablennamen. + compact.EnvNames = (d.Config?.Env ?? []).map((entry: unknown) => + typeof entry === "string" ? entry.split("=", 1)[0] : "", + ).filter((name: string) => name !== ""); + compact.Mounts = (d.Mounts ?? []).map((m: any) => ({ + Type: m.Type ?? "", + Source: m.Source ?? "", + Destination: m.Destination ?? "", + })); + } return JSON.stringify(compact); } diff --git a/src/tools.ts b/src/tools.ts index 8cf8188..7571b39 100644 --- a/src/tools.ts +++ b/src/tools.ts @@ -107,13 +107,24 @@ export const TOOLS: ToolDef[] = [ { name: "unraid_docker_inspect", description: - "Inspect a single Docker container in detail (state, image, ports, redacted environment variable names, mounts). Secret values are never returned.", + "Inspect one known Docker container directly. Default summary returns state, image, network mode and ports only. Use detail='full' only when redacted environment variable names or mounts are required. Secret values are never returned.", inputSchema: { type: "object", - properties: { container: str("Container name or ID") }, + properties: { + container: str("Container name or ID"), + detail: { + type: "string", + enum: ["summary", "full"], + description: "summary is compact; full additionally returns environment variable names and mounts. Default: summary.", + }, + }, required: ["container"], + additionalProperties: false, }, - handler: (a) => compactContainerInspect(validateName(a["container"], "container")), + handler: (a) => compactContainerInspect( + validateName(a["container"], "container"), + (a["detail"] as "summary" | "full" | undefined), + ), }, { name: "unraid_docker_logs",