feat: add bounded file inventory for media audits

This commit is contained in:
Mikei386
2026-08-24 11:49:12 +02:00
parent d90874ed18
commit 4426969d63
12 changed files with 163 additions and 12 deletions
+12
View File
@@ -57,6 +57,7 @@ export const SAFE_DEFAULT_TOOLS = [
"unraid_notifications_list",
"unraid_shares_list",
"unraid_share_inspect",
"unraid_files_inventory",
];
// ── Config laden ────────────────────────────────────────────────────────
@@ -104,6 +105,17 @@ export function parseConfig(content: string): MUAConfig {
) {
cfg.enabledTools.push("unraid_docker_update_verified_batch");
}
// r020 adds a bounded metadata-only filesystem inventory. It is strictly
// less powerful than the already-enabled read-only shell and avoids many
// repeated ls/find calls, so existing read-only installations may receive
// it without granting a new write capability.
if (
!cfg.allToolsEnabled
&& cfg.enabledTools.includes("unraid_system_shell_readonly")
&& !cfg.enabledTools.includes("unraid_files_inventory")
) {
cfg.enabledTools.push("unraid_files_inventory");
}
return cfg;
}
+1 -1
View File
@@ -14,7 +14,7 @@ import { createHash, randomUUID } from "node:crypto";
// ── Konstanten ──────────────────────────────────────────────────────────
export const MUA_SERVER_NAME = "mua";
export const MUA_VERSION = "2026.08.24.r019";
export const MUA_VERSION = "2026.08.24.r020";
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";
+9
View File
@@ -30,6 +30,14 @@ describe("secure tool configuration", () => {
expect(cfg.enabledTools).toContain("unraid_docker_update");
expect(cfg.enabledTools).toContain("unraid_docker_update_verified_batch");
});
test("an enabled read-only shell also exposes bounded file inventory", () => {
const cfg = parseConfig(
"MUA_API_KEY=test\nMUA_ENABLED_TOOLS=unraid_system_shell_readonly\n",
);
expect(cfg.enabledTools).toContain("unraid_files_inventory");
expect(cfg.enabledTools).not.toContain("unraid_system_shell");
});
});
describe("secret handling", () => {
@@ -53,6 +61,7 @@ describe("risk classification", () => {
expect(getToolRisk("unraid_network_lan_probe")).toBe("active");
expect(getToolRisk("unraid_docker_list")).toBe("read");
expect(getToolRisk("unraid_system_shell_readonly")).toBe("read");
expect(getToolRisk("unraid_files_inventory")).toBe("read");
expect(getToolRisk("unraid_system_health")).toBe("read");
expect(getToolRisk("unraid_ca_search")).toBe("active");
expect(getToolRisk("unraid_ca_install_preview")).toBe("active");
+29
View File
@@ -533,6 +533,35 @@ export const TOOLS: ToolDef[] = [
},
handler: (a) => runStatusHelper("share-inspect", String(a["share"] ?? "")),
},
{
name: "unraid_files_inventory",
description:
"Inventory file and directory names below one exact Unraid share in one bounded read-only call. Use this instead of repeated ls/find calls for media-library audits, missing-episode checks and locating a named collection. It never reads file contents. If name_contains matches a directory, that directory and its descendants are returned; unrelated trees are omitted.",
inputSchema: {
type: "object",
properties: {
share: str("Exact Unraid share name, for example Audiobooks"),
relative_path: str("Optional path below the share; never use /mnt/user or an absolute path"),
name_contains: str("Optional case-insensitive name fragment, for example 'drei'. Matching directories include their descendants"),
max_depth: int("Maximum directory depth below relative_path (1-20, default 10)"),
max_entries: int("Maximum returned entries (1-5000, default 2000)"),
include_files: { type: "boolean", description: "Include files (default true)" },
include_directories: { type: "boolean", description: "Include directories (default true)" },
},
required: ["share"],
additionalProperties: false,
},
handler: (a) => runStatusHelper(
"files-inventory",
String(a["share"] ?? ""),
String(a["relative_path"] ?? ""),
String(a["name_contains"] ?? ""),
String(Math.max(1, Math.min(20, Number(a["max_depth"] ?? 10)))),
String(Math.max(1, Math.min(5000, Number(a["max_entries"] ?? 2000)))),
a["include_files"] === false ? "0" : "1",
a["include_directories"] === false ? "0" : "1",
),
},
{
name: "unraid_system_connection_test",
description: