Add native toolbox MCP tools
This commit is contained in:
@@ -7,7 +7,7 @@ Kompiliertes Bun-Binary (TypeScript), läuft als SysVinit-Service auf Unraid.
|
||||
- **Health-Check:** `http://<unraid-ip>:3002/health` (offen, ohne Auth)
|
||||
- **Config-Server:** `http://127.0.0.1:3013/config` (localhost + separater Admin-Token)
|
||||
- **Auth:** API-Key (Bearer-Token) für `/mcp`
|
||||
- **Tools:** 33 (Docker, Community Applications, Netzwerk und Unraid-System)
|
||||
- **Tools:** 35 (Docker, Community Applications, Netzwerk, Unraid-System und optionale Toolbox)
|
||||
|
||||
---
|
||||
|
||||
@@ -238,11 +238,17 @@ MUA kann einen allgemeinen Werkzeugcontainer dynamisch über das Docker-Label
|
||||
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`.
|
||||
Bei aktiviertem Schalter stellt MUA zwei eigene, dynamisch sichtbare Werkzeuge
|
||||
bereit:
|
||||
|
||||
- `unraid_toolbox_status` erkennt den Container und prüft angefragte Programme
|
||||
wie `ffmpeg`, `ffprobe`, `yt-dlp` oder `mediainfo`.
|
||||
- `unraid_toolbox_exec` führt Befehle direkt darin aus und kann auf ausdrücklichen
|
||||
Auftrag fehlende Pakete dort installieren.
|
||||
|
||||
Der Containername ist nicht fest in MUA eingebaut. Wird der Container ersetzt,
|
||||
genügt dasselbe Label am Nachfolger. Die Toolbox-Werkzeuge erscheinen in keinem
|
||||
MCP-Client, solange der Schalter aus ist oder kein passender Container existiert.
|
||||
|
||||
Beispiel für Docker beziehungsweise DockerMan `ExtraParams`:
|
||||
|
||||
|
||||
BIN
Binary file not shown.
+4
-3
@@ -119,9 +119,10 @@ 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
|
||||
erscheinen `unraid_toolbox_status` und `unraid_toolbox_exec` direkt im nativen
|
||||
Hermes-Werkzeugkatalog. Hermes prüft benötigte Programme zuerst mit dem
|
||||
Statuswerkzeug und führt Medien- oder Downloadbefehle anschließend direkt im
|
||||
Toolbox-Container aus. Im Hermes-Container selbst werden keine Hilfsprogramme
|
||||
nachinstalliert. MUA setzt keinen festen Containernamen voraus.
|
||||
|
||||
## 5. Entfernen
|
||||
|
||||
+1
-1
@@ -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.29.r026';
|
||||
const MUA_VERSION = '2026.08.29.r027';
|
||||
const MUA_SERVER_NAME = 'mua';
|
||||
|
||||
/**
|
||||
|
||||
+1
-1
@@ -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.29.r026';
|
||||
const MUA_VERSION = '2026.08.29.r027';
|
||||
|
||||
// ── Session-Management (in-memory) ───────────────────────────────────────
|
||||
$sessions = []; // session_id => ['created' => time, 'initialized' => bool]
|
||||
|
||||
+1
-1
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "mua",
|
||||
"version": "2026.08.29.r026",
|
||||
"version": "2026.08.29.r027",
|
||||
"description": "Mikes Unraid Agent - MCP over HTTP (Streamable HTTP) for Unraid",
|
||||
"type": "module",
|
||||
"main": "src/index.ts",
|
||||
|
||||
+9
-4
@@ -2,13 +2,13 @@
|
||||
<!DOCTYPE PLUGIN [
|
||||
<!ENTITY name "mua">
|
||||
<!ENTITY author "Michael">
|
||||
<!ENTITY version "2026.08.29.r026">
|
||||
<!ENTITY version "2026.08.29.r027">
|
||||
<!ENTITY launch "Settings/mua">
|
||||
<!ENTITY pluginURL "http://192.168.1.2:4000/michael/MUA-Mikes-Unraid-Agent/raw/branch/main/plugin/mua.plg">
|
||||
<!ENTITY pluginLOC "/boot/config/plugins/&name;">
|
||||
<!ENTITY emhttpLOC "/usr/local/emhttp/plugins/&name;">
|
||||
<!ENTITY txzURL "http://192.168.1.2:4000/michael/MUA-Mikes-Unraid-Agent/raw/branch/main/dist/mua-2026.08.29.r026-x86_64-1.txz">
|
||||
<!ENTITY txzSHA256 "c1c045ce32d41cfec4c985a3f9ff08d8f2150d2103a75146bd17323922d562bb">
|
||||
<!ENTITY txzURL "http://192.168.1.2:4000/michael/MUA-Mikes-Unraid-Agent/raw/branch/main/dist/mua-2026.08.29.r027-x86_64-1.txz">
|
||||
<!ENTITY txzSHA256 "f7fcbc3b3de260f6df0d413ac072d0f3e674bc25425e6a50b8a11b78a4e5007c">
|
||||
]>
|
||||
|
||||
<PLUGIN name="&name;"
|
||||
@@ -23,6 +23,11 @@
|
||||
>
|
||||
|
||||
<CHANGES>
|
||||
### 2026.08.29.r027
|
||||
- Stellt bei aktivierter Toolbox zwei echte MCP-Werkzeuge bereit: `unraid_toolbox_status` und `unraid_toolbox_exec`.
|
||||
- Agenten erkennen und verwenden Medienwerkzeuge direkt im dynamisch gelabelten Container, ohne lokales Docker oder Umwege über die Host-Shell.
|
||||
- Toolbox-Werkzeuge verschwinden automatisch aus `tools/list`, wenn der GUI-Schalter aus ist oder kein gelabelter Container existiert.
|
||||
|
||||
### 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.
|
||||
@@ -162,7 +167,7 @@ Das .txz enthält:
|
||||
install/doinst.sh (läuft nach Installation)
|
||||
===========================================
|
||||
-->
|
||||
<FILE Name="/boot/config/plugins/&name;/mua-2026.08.29.r026-x86_64-1.txz" Run="upgradepkg --install-new" Mode="755" Min="7.0.0">
|
||||
<FILE Name="/boot/config/plugins/&name;/mua-2026.08.29.r027-x86_64-1.txz" Run="upgradepkg --install-new" Mode="755" Min="7.0.0">
|
||||
<URL>&txzURL;</URL>
|
||||
<SHA256>&txzSHA256;</SHA256>
|
||||
</FILE>
|
||||
|
||||
+1
-1
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"name": "mua",
|
||||
"author": "Michael",
|
||||
"version": "2026.08.29.r026",
|
||||
"version": "2026.08.29.r027",
|
||||
"minver": "7.0.0",
|
||||
"pluginDirectory": "/usr/local/emhttp/plugins/mua",
|
||||
"configDirectory": "/boot/config/plugins/mua",
|
||||
|
||||
+2
-2
@@ -169,10 +169,10 @@ $active_count = $all_tools_enabled ? count($all_tools) : count($enabled_tools);
|
||||
<div class="mua-section"><h2>Agent-Werkzeugcontainer</h2><div class="mua-card <?= !$toolbox_available ? '' : ($toolbox_enabled ? 'mua-risk-safe' : 'mua-risk-notice') ?>">
|
||||
<?php if ($toolbox_available): ?>
|
||||
<p class="mua-summary">✅ Gefunden: <?php foreach ($toolbox_containers as $i => $container): ?><?= $i ? ', ' : '' ?><strong><?= htmlspecialchars($container['name'] ?? 'unbekannt') ?></strong> <span class="mua-muted">(<?= htmlspecialchars($container['state'] ?? 'unbekannt') ?>)</span><?php endforeach; ?></p>
|
||||
<form method="post"><input type="hidden" name="csrf_token" value="<?= htmlspecialchars($unraid_csrf) ?>"><input type="hidden" name="mua_csrf" value="<?= htmlspecialchars($csrf) ?>"><input type="hidden" name="action" value="save_toolbox"><label class="mua-switch"><input type="checkbox" name="toolbox_enabled" <?= $toolbox_enabled ? 'checked' : '' ?>><span><strong>Werkzeugcontainer für KI-Agenten bekanntgeben</strong><br><span class="mua-muted">MUA nennt den dynamisch erkannten Container in seinen MCP-Anweisungen und erklärt die Nutzung über <code>docker exec</code>.</span></span></label><button type="submit" class="mua-btn mua-btn-primary">Einstellung speichern</button></form>
|
||||
<form method="post"><input type="hidden" name="csrf_token" value="<?= htmlspecialchars($unraid_csrf) ?>"><input type="hidden" name="mua_csrf" value="<?= htmlspecialchars($csrf) ?>"><input type="hidden" name="action" value="save_toolbox"><label class="mua-switch"><input type="checkbox" name="toolbox_enabled" <?= $toolbox_enabled ? 'checked' : '' ?>><span><strong>Werkzeugcontainer für KI-Agenten bereitstellen</strong><br><span class="mua-muted">Blendet <code>unraid_toolbox_status</code> und <code>unraid_toolbox_exec</code> für MCP-Clients ein. Der Container wird dynamisch über sein Label erkannt.</span></span></label><button type="submit" class="mua-btn mua-btn-primary">Einstellung speichern</button></form>
|
||||
<?php else: ?>
|
||||
<p class="mua-summary mua-muted">Kein Werkzeugcontainer erkannt</p>
|
||||
<label class="mua-switch"><input type="checkbox" disabled><span><strong>Werkzeugcontainer für KI-Agenten bekanntgeben</strong><br><span class="mua-muted">Nicht verfügbar. Erstelle einen Docker-Container und versehe ihn mit dem Label <code>mike.ai.role=toolbox</code>. Danach diese Seite neu laden.</span></span></label><button type="button" class="mua-btn" disabled>Einstellung speichern</button>
|
||||
<label class="mua-switch"><input type="checkbox" disabled><span><strong>Werkzeugcontainer für KI-Agenten bereitstellen</strong><br><span class="mua-muted">Nicht verfügbar. Erstelle einen Docker-Container und versehe ihn mit dem Label <code>mike.ai.role=toolbox</code>. Danach diese Seite neu laden.</span></span></label><button type="button" class="mua-btn" disabled>Einstellung speichern</button>
|
||||
<?php endif; ?>
|
||||
</div></div>
|
||||
|
||||
|
||||
+99
-1
@@ -15,7 +15,7 @@ import { existsSync, mkdirSync, readFileSync, rmSync, statSync, writeFileSync }
|
||||
|
||||
// ── Konstanten ──────────────────────────────────────────────────────────
|
||||
export const MUA_SERVER_NAME = "mua";
|
||||
export const MUA_VERSION = "2026.08.29.r026";
|
||||
export const MUA_VERSION = "2026.08.29.r027";
|
||||
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";
|
||||
@@ -348,6 +348,104 @@ export async function discoverToolboxContainers(): Promise<ToolboxContainer[]> {
|
||||
return rows;
|
||||
}
|
||||
|
||||
function selectRunningToolbox(
|
||||
containers: ToolboxContainer[],
|
||||
requestedName?: string,
|
||||
): ToolboxContainer {
|
||||
const running = containers.filter((container) => container.state === "running");
|
||||
if (requestedName) {
|
||||
const selected = running.find((container) => container.name === requestedName);
|
||||
if (!selected) throw new Error(`Running toolbox container not found: ${requestedName}`);
|
||||
return selected;
|
||||
}
|
||||
if (running.length === 0) throw new Error("No labeled toolbox container is running");
|
||||
if (running.length > 1) {
|
||||
throw new Error(
|
||||
`Multiple toolbox containers are running; specify container: ${running.map((c) => c.name).join(", ")}`,
|
||||
);
|
||||
}
|
||||
return running[0];
|
||||
}
|
||||
|
||||
/** Return labeled toolbox state and optionally probe specific command names. */
|
||||
export async function toolboxStatus(programs: string[] = []): Promise<string> {
|
||||
if (programs.length > 32) throw new Error("programs accepts at most 32 names");
|
||||
for (const program of programs) {
|
||||
if (!/^[A-Za-z0-9._+-]{1,128}$/.test(program)) {
|
||||
throw new Error(`Invalid program name: ${program}`);
|
||||
}
|
||||
}
|
||||
const containers = await discoverToolboxContainers();
|
||||
const running = containers.filter((container) => container.state === "running");
|
||||
const result: Record<string, unknown> = {
|
||||
label: "mike.ai.role=toolbox",
|
||||
available: containers.length > 0,
|
||||
running: running.length > 0,
|
||||
containers,
|
||||
};
|
||||
if (programs.length === 0 || running.length === 0) return JSON.stringify(result);
|
||||
|
||||
if (running.length > 1) {
|
||||
result["note"] = "Multiple toolboxes are running; availability is reported for the first one.";
|
||||
}
|
||||
const selected = running[0];
|
||||
const script =
|
||||
'for p do path=$(command -v "$p" 2>/dev/null || true); ' +
|
||||
'if [ -n "$path" ]; then printf "%s\\t%s\\n" "$p" "$path"; ' +
|
||||
'else printf "%s\\t\\n" "$p"; fi; done';
|
||||
const raw = await runArgv(
|
||||
"toolbox_status",
|
||||
["/usr/bin/docker", "exec", selected.name, "/bin/sh", "-c", script, "toolbox-status", ...programs],
|
||||
30,
|
||||
30_000,
|
||||
8_000,
|
||||
);
|
||||
const paths = new Map(
|
||||
raw.split("\n").map((line) => {
|
||||
const tab = line.indexOf("\t");
|
||||
return tab >= 0 ? [line.slice(0, tab), line.slice(tab + 1)] : [line, ""];
|
||||
}),
|
||||
);
|
||||
result["selected_container"] = selected.name;
|
||||
result["programs"] = programs.map((program) => ({
|
||||
name: program,
|
||||
available: Boolean(paths.get(program)),
|
||||
path: paths.get(program) || null,
|
||||
}));
|
||||
return JSON.stringify(result);
|
||||
}
|
||||
|
||||
/** Execute an explicitly requested shell command inside one labeled toolbox. */
|
||||
export async function toolboxExec(
|
||||
command: string,
|
||||
timeoutSec = 300,
|
||||
requestedName?: string,
|
||||
): Promise<string> {
|
||||
const selected = selectRunningToolbox(await discoverToolboxContainers(), requestedName);
|
||||
try {
|
||||
const proc = spawn(
|
||||
["/usr/bin/docker", "exec", selected.name, "/bin/sh", "-lc", command],
|
||||
{ stdout: "pipe", stderr: "pipe", cwd: "/" },
|
||||
);
|
||||
const timeout = setTimeout(() => proc.kill(), timeoutSec * 1000);
|
||||
const [stdout, stderr, code] = await Promise.all([
|
||||
readStreamLimited(proc.stdout, 100_000),
|
||||
readStreamLimited(proc.stderr, 20_000),
|
||||
proc.exited,
|
||||
]);
|
||||
clearTimeout(timeout);
|
||||
return JSON.stringify({
|
||||
container: selected.name,
|
||||
exit_code: code,
|
||||
stdout: sanitizeLogOutput(stdout.text.trim(), 100_000),
|
||||
stderr: sanitizeLogOutput(stderr.text.trim(), 20_000),
|
||||
truncated: stdout.truncated || stderr.truncated,
|
||||
});
|
||||
} catch (error) {
|
||||
throw new Error(`toolbox execution failed: ${String(error)}`);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Delegiert eine Write-Operation an den PHP-Helper.
|
||||
*/
|
||||
|
||||
+17
-15
@@ -61,13 +61,9 @@ async function toolboxGuidance(): Promise<{
|
||||
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.";
|
||||
instruction = `Agent toolbox available: ${running.map((c) => c.name).join(", ")}. ` +
|
||||
"Use unraid_toolbox_status to check programs and unraid_toolbox_exec to run or install " +
|
||||
"utilities there. Do not install helper programs in Hermes or the MCP client.";
|
||||
} else {
|
||||
instruction =
|
||||
`Agent toolbox configured but currently stopped: ${containers.map((c) => c.name).join(", ")}. ` +
|
||||
@@ -77,6 +73,12 @@ async function toolboxGuidance(): Promise<{
|
||||
return { available, enabled, containers, instruction };
|
||||
}
|
||||
|
||||
const TOOLBOX_TOOL_NAMES = new Set(["unraid_toolbox_status", "unraid_toolbox_exec"]);
|
||||
|
||||
function isToolAvailable(name: string, toolboxEnabled: boolean): boolean {
|
||||
return TOOLBOX_TOOL_NAMES.has(name) ? toolboxEnabled : isToolEnabled(name);
|
||||
}
|
||||
|
||||
// ── JSON-RPC Helpers ────────────────────────────────────────────────────
|
||||
function rpcResult(id: number | string | null, result: unknown) {
|
||||
return { jsonrpc: "2.0", id, result };
|
||||
@@ -164,15 +166,12 @@ async function handleMcpRequest(
|
||||
if (sessionId) touchSession(sessionId);
|
||||
const toolbox = await toolboxGuidance();
|
||||
// Tool-Filter: nur aktive Tools anzeigen
|
||||
const activeTools = TOOLS.filter((t) => isToolEnabled(t.name));
|
||||
const activeTools = TOOLS.filter((t) => isToolAvailable(t.name, toolbox.enabled));
|
||||
return {
|
||||
response: rpcResult(id, {
|
||||
tools: activeTools.map((t) => ({
|
||||
name: t.name,
|
||||
description:
|
||||
t.name === "unraid_system_shell" && toolbox.instruction
|
||||
? `${t.description} ${toolbox.instruction}`
|
||||
: t.description,
|
||||
description: t.description,
|
||||
inputSchema: t.inputSchema,
|
||||
})),
|
||||
}),
|
||||
@@ -196,7 +195,8 @@ async function handleMcpRequest(
|
||||
};
|
||||
}
|
||||
// Tool-Filter: deaktivierte Tools ablehnen
|
||||
if (!isToolEnabled(toolName)) {
|
||||
const toolbox = await toolboxGuidance();
|
||||
if (!isToolAvailable(toolName, toolbox.enabled)) {
|
||||
return {
|
||||
response: rpcResult(id, {
|
||||
content: [{ type: "text", text: `ERROR: Tool disabled: ${toolName}` }],
|
||||
@@ -470,7 +470,7 @@ try {
|
||||
apiKeyMasked: getMaskedApiKey(),
|
||||
enabledTools: getEnabledTools(),
|
||||
allToolsEnabled: getAllToolsEnabled(),
|
||||
allTools: TOOLS.map((t) => ({
|
||||
allTools: TOOLS.filter((t) => !TOOLBOX_TOOL_NAMES.has(t.name)).map((t) => ({
|
||||
name: t.name,
|
||||
description: t.description,
|
||||
risk: getToolRisk(t.name),
|
||||
@@ -498,7 +498,9 @@ try {
|
||||
let generatedApiKey: string | undefined;
|
||||
if (body["generate"] === true) generatedApiKey = generateApiKey();
|
||||
if (Array.isArray(body["enabledTools"])) {
|
||||
const known = new Set(TOOLS.map((t) => t.name));
|
||||
const known = new Set(
|
||||
TOOLS.filter((t) => !TOOLBOX_TOOL_NAMES.has(t.name)).map((t) => t.name),
|
||||
);
|
||||
const tools = (body["enabledTools"] as unknown[])
|
||||
.filter((t): t is string => typeof t === "string" && known.has(t));
|
||||
setEnabledTools(tools, body["allToolsEnabled"] === true);
|
||||
|
||||
@@ -74,6 +74,8 @@ describe("secret handling", () => {
|
||||
describe("risk classification", () => {
|
||||
test("classifies root shell and container changes as critical", () => {
|
||||
expect(getToolRisk("unraid_system_shell")).toBe("critical");
|
||||
expect(getToolRisk("unraid_toolbox_exec")).toBe("critical");
|
||||
expect(getToolRisk("unraid_toolbox_status")).toBe("read");
|
||||
expect(getToolRisk("unraid_system_job_start")).toBe("critical");
|
||||
expect(getToolRisk("unraid_system_job_cleanup")).toBe("critical");
|
||||
expect(getToolRisk("unraid_system_job_status")).toBe("read");
|
||||
@@ -91,6 +93,13 @@ describe("risk classification", () => {
|
||||
});
|
||||
});
|
||||
|
||||
describe("agent toolbox tools", () => {
|
||||
test("publishes explicit status and execution schemas", () => {
|
||||
expect(toolByName("unraid_toolbox_status")).toBeDefined();
|
||||
expect(toolByName("unraid_toolbox_exec")).toBeDefined();
|
||||
});
|
||||
});
|
||||
|
||||
describe("direct Unraid terminal", () => {
|
||||
test("accepts a normal command with the extended timeout", async () => {
|
||||
const tool = toolByName("unraid_system_shell");
|
||||
|
||||
@@ -28,6 +28,8 @@ import {
|
||||
searchCommunityApps,
|
||||
previewCommunityAppInstall,
|
||||
installCommunityApp,
|
||||
toolboxStatus,
|
||||
toolboxExec,
|
||||
} from "./helpers";
|
||||
|
||||
export interface ToolDef {
|
||||
@@ -46,6 +48,7 @@ const CRITICAL_TOOLS = new Set([
|
||||
"unraid_docker_update_verified_batch",
|
||||
"unraid_docker_rebuild",
|
||||
"unraid_system_shell",
|
||||
"unraid_toolbox_exec",
|
||||
"unraid_system_job_start",
|
||||
"unraid_system_job_cleanup",
|
||||
"unraid_ca_install",
|
||||
@@ -92,6 +95,58 @@ function validateShellCommand(value: unknown): string {
|
||||
}
|
||||
|
||||
export const TOOLS: ToolDef[] = [
|
||||
// These two tools are exposed dynamically only when a labeled toolbox is
|
||||
// present and the dedicated WebGUI switch is enabled (see index.ts).
|
||||
{
|
||||
name: "unraid_toolbox_status",
|
||||
description:
|
||||
"Discover the optional labeled agent toolbox on Unraid and check whether command-line programs such as ffmpeg, ffprobe, yt-dlp or mediainfo are installed there. Use this first when a required CLI program may be missing; do not probe the local Hermes or MCP-client container.",
|
||||
inputSchema: {
|
||||
type: "object",
|
||||
properties: {
|
||||
programs: {
|
||||
type: "array",
|
||||
items: { type: "string", minLength: 1, maxLength: 128, pattern: "^[A-Za-z0-9._+-]+$" },
|
||||
maxItems: 32,
|
||||
description: "Optional command names to check inside the running toolbox",
|
||||
},
|
||||
},
|
||||
additionalProperties: false,
|
||||
},
|
||||
handler: (a) => {
|
||||
const raw = a["programs"] ?? [];
|
||||
if (!Array.isArray(raw) || raw.some((item) => typeof item !== "string")) {
|
||||
throw new Error("programs must be an array of strings");
|
||||
}
|
||||
return toolboxStatus(raw as string[]);
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "unraid_toolbox_exec",
|
||||
description:
|
||||
"Run a command directly inside the labeled agent toolbox on Unraid. Use it for media and download utilities and to install a missing package inside that toolbox when the user requests the job. Never install helper programs into Hermes or the MCP client. The toolbox container is selected dynamically by Docker label, not by a hard-coded name.",
|
||||
inputSchema: {
|
||||
type: "object",
|
||||
properties: {
|
||||
command: shellCommand("Command or multi-line shell script to run inside the toolbox"),
|
||||
timeout_seconds: int("Timeout in seconds (1-1800, default 300)"),
|
||||
container: str("Optional toolbox container name; required only if multiple labeled toolboxes are running"),
|
||||
},
|
||||
required: ["command"],
|
||||
additionalProperties: false,
|
||||
},
|
||||
handler: (a) => {
|
||||
const command = validateShellCommand(a["command"]);
|
||||
const timeout = Number(a["timeout_seconds"] ?? 300);
|
||||
if (!Number.isInteger(timeout) || timeout < 1 || timeout > 1800) {
|
||||
throw new Error("timeout_seconds must be between 1 and 1800");
|
||||
}
|
||||
const container = a["container"] === undefined
|
||||
? undefined
|
||||
: validateName(a["container"], "container");
|
||||
return toolboxExec(command, timeout, container);
|
||||
},
|
||||
},
|
||||
// ── Docker (14) ───────────────────────────────────────────────────────
|
||||
{
|
||||
name: "unraid_docker_list",
|
||||
|
||||
Reference in New Issue
Block a user