- New helper runShell(): executes /bin/sh -c, returns {exit_code, stdout, stderr} as JSON
- New tool unraid_system_shell: direct shell access on Unraid host (root)
- params: command (required), timeout_seconds (1-300, default 60)
- output size-limited via sanitizeLogOutput
- Version bump r005 -> r006
335 lines
12 KiB
TypeScript
335 lines
12 KiB
TypeScript
/**
|
|
* MUA — Mikes Unraid Agent
|
|
* tools.ts — MCP Tool-Definitionen (22 Tools)
|
|
*
|
|
* Portiert von mcp/tools.php. Schema: unraid_<kategorie>_<aktion>
|
|
* Kategorien: docker (14), network (6), system (2).
|
|
*/
|
|
|
|
import {
|
|
containerRuntimeSummary,
|
|
compactContainerInspect,
|
|
dockerExec,
|
|
sanitizeLogOutput,
|
|
analyzeContainerLogs,
|
|
runPhpHelper,
|
|
compactNetworkInventory,
|
|
hostState,
|
|
auditAllTcpEndpoints,
|
|
probeDualstack,
|
|
connectionTest,
|
|
validateName,
|
|
runShell,
|
|
} from "./helpers";
|
|
|
|
export interface ToolDef {
|
|
name: string;
|
|
description: string;
|
|
inputSchema: object;
|
|
handler: (args: Record<string, unknown>) => Promise<string>;
|
|
}
|
|
|
|
const str = (desc: string) => ({ type: "string", description: desc });
|
|
const int = (desc: string) => ({ type: "integer", description: desc });
|
|
const num = (desc: string) => ({ type: "number", description: desc });
|
|
const bool = (desc: string) => ({ type: "boolean", description: desc });
|
|
const empty = { type: "object", properties: {}, additionalProperties: false } as const;
|
|
|
|
export const TOOLS: ToolDef[] = [
|
|
// ── Docker (14) ───────────────────────────────────────────────────────
|
|
{
|
|
name: "unraid_docker_list",
|
|
description:
|
|
"List all Docker containers with runtime stats (CPU, memory, network I/O). Returns a compact JSON summary.",
|
|
inputSchema: empty,
|
|
handler: () => containerRuntimeSummary(),
|
|
},
|
|
{
|
|
name: "unraid_docker_inspect",
|
|
description:
|
|
"Inspect a single Docker container in detail (state, image, ports, env, mounts).",
|
|
inputSchema: {
|
|
type: "object",
|
|
properties: { container: str("Container name or ID") },
|
|
required: ["container"],
|
|
},
|
|
handler: (a) => compactContainerInspect(validateName(a["container"], "container")),
|
|
},
|
|
{
|
|
name: "unraid_docker_logs",
|
|
description: "Get recent logs from a Docker container (with timestamps).",
|
|
inputSchema: {
|
|
type: "object",
|
|
properties: {
|
|
container: str("Container name or ID"),
|
|
tail: int("Number of lines (1-2000, default 200)"),
|
|
},
|
|
required: ["container"],
|
|
},
|
|
handler: async (a) => {
|
|
const container = validateName(a["container"], "container");
|
|
const tail = Number(a["tail"] ?? 200);
|
|
if (tail < 1 || tail > 2000) throw new Error("tail must be between 1 and 2000");
|
|
const raw = await dockerExec(`logs --timestamps --tail ${tail} ${container}`, 60);
|
|
return sanitizeLogOutput(raw);
|
|
},
|
|
},
|
|
{
|
|
name: "unraid_docker_analyze_logs",
|
|
description:
|
|
"Analyze container logs server-side for errors/warnings. Returns pattern counts and sample matches.",
|
|
inputSchema: {
|
|
type: "object",
|
|
properties: {
|
|
severity: {
|
|
type: "string",
|
|
enum: ["error", "warn", "info"],
|
|
description: "Log severity to scan for",
|
|
},
|
|
container: str("Container name (optional, scans all running containers if omitted)"),
|
|
since: str("Time filter (default 24h)"),
|
|
scan_tail: int("Max lines to scan (default 1000)"),
|
|
max_results: int("Max sample matches (default 50)"),
|
|
},
|
|
required: ["severity"],
|
|
},
|
|
handler: (a) => {
|
|
const severity = (a["severity"] as string) ?? "error";
|
|
const container = a["container"] as string | null | undefined;
|
|
if (container !== undefined && container !== null && typeof container !== "string") {
|
|
throw new Error("container must be a string");
|
|
}
|
|
const since = (a["since"] as string) ?? "24h";
|
|
const scanTail = Number(a["scan_tail"] ?? 1000);
|
|
const maxResults = Number(a["max_results"] ?? 50);
|
|
return analyzeContainerLogs(severity, container ?? null, since, scanTail, maxResults);
|
|
},
|
|
},
|
|
{
|
|
name: "unraid_docker_processes",
|
|
description: "List processes running inside a Docker container (docker top).",
|
|
inputSchema: {
|
|
type: "object",
|
|
properties: { container: str("Container name or ID") },
|
|
required: ["container"],
|
|
},
|
|
handler: (a) =>
|
|
dockerExec(
|
|
`top ${validateName(a["container"], "container")} -eo pid,ppid,user,stat,lstart,etime,args`,
|
|
),
|
|
},
|
|
{
|
|
name: "unraid_docker_stats",
|
|
description: "Get live CPU/memory/network/block I/O stats for all running containers.",
|
|
inputSchema: empty,
|
|
handler: () => dockerExec(`stats --no-stream --format '{{json .}}'`),
|
|
},
|
|
{
|
|
name: "unraid_docker_info",
|
|
description:
|
|
"Get Docker daemon information (version, storage driver, container counts, etc.).",
|
|
inputSchema: empty,
|
|
handler: () => dockerExec(`info --format '{{json .}}'`),
|
|
},
|
|
{
|
|
name: "unraid_docker_start",
|
|
description: "Start a Docker container.",
|
|
inputSchema: {
|
|
type: "object",
|
|
properties: { container: str("Container name or ID") },
|
|
required: ["container"],
|
|
},
|
|
handler: (a) => dockerExec(`start ${validateName(a["container"], "container")}`),
|
|
},
|
|
{
|
|
name: "unraid_docker_stop",
|
|
description: "Stop a Docker container.",
|
|
inputSchema: {
|
|
type: "object",
|
|
properties: { container: str("Container name or ID") },
|
|
required: ["container"],
|
|
},
|
|
handler: (a) => dockerExec(`stop ${validateName(a["container"], "container")}`),
|
|
},
|
|
{
|
|
name: "unraid_docker_restart",
|
|
description: "Restart a Docker container.",
|
|
inputSchema: {
|
|
type: "object",
|
|
properties: { container: str("Container name or ID") },
|
|
required: ["container"],
|
|
},
|
|
handler: (a) => dockerExec(`restart ${validateName(a["container"], "container")}`),
|
|
},
|
|
{
|
|
name: "unraid_docker_create",
|
|
description: "Create a Docker container from a template (pulls image, creates container).",
|
|
inputSchema: {
|
|
type: "object",
|
|
properties: {
|
|
template_name: str('Template name (e.g. "linuxserver/sonarr")'),
|
|
},
|
|
required: ["template_name"],
|
|
},
|
|
handler: (a) => runPhpHelper("create", validateName(a["template_name"], "template_name")),
|
|
},
|
|
{
|
|
name: "unraid_docker_modify",
|
|
description:
|
|
"Modify a container template (port, env, volume, network, privileged) and rebuild.",
|
|
inputSchema: {
|
|
type: "object",
|
|
properties: {
|
|
container: str("Container/template name"),
|
|
field: {
|
|
type: "string",
|
|
enum: ["port", "env", "volume", "network", "privileged"],
|
|
},
|
|
value: str("New value (format depends on field)"),
|
|
},
|
|
required: ["container", "field", "value"],
|
|
},
|
|
handler: (a) => {
|
|
const container = validateName(a["container"], "container");
|
|
const field = (a["field"] as string) ?? "";
|
|
const value = (a["value"] as string) ?? "";
|
|
if (!["port", "env", "volume", "network", "privileged"].includes(field)) {
|
|
throw new Error("field must be one of: port, env, volume, network, privileged");
|
|
}
|
|
return runPhpHelper("modify", container, field, value);
|
|
},
|
|
},
|
|
{
|
|
name: "unraid_docker_update",
|
|
description: "Update a container (pull latest image, rebuild).",
|
|
inputSchema: {
|
|
type: "object",
|
|
properties: { container: str("Container/template name") },
|
|
required: ["container"],
|
|
},
|
|
handler: (a) => runPhpHelper("update", validateName(a["container"], "container")),
|
|
},
|
|
{
|
|
name: "unraid_docker_rebuild",
|
|
description:
|
|
"Rebuild a container from its template (without pulling new image).",
|
|
inputSchema: {
|
|
type: "object",
|
|
properties: { container: str("Container/template name") },
|
|
required: ["container"],
|
|
},
|
|
handler: (a) => runPhpHelper("rebuild", validateName(a["container"], "container")),
|
|
},
|
|
|
|
// ── Netzwerk (6) ──────────────────────────────────────────────────────
|
|
{
|
|
name: "unraid_network_inventory",
|
|
description: "Compact Docker network inventory (all networks with container counts).",
|
|
inputSchema: empty,
|
|
handler: () => compactNetworkInventory(),
|
|
},
|
|
{
|
|
name: "unraid_network_list",
|
|
description: "List all Docker networks.",
|
|
inputSchema: empty,
|
|
handler: () => dockerExec(`network ls --no-trunc --format '{{json .}}'`),
|
|
},
|
|
{
|
|
name: "unraid_network_inspect",
|
|
description: "Inspect a Docker network in detail.",
|
|
inputSchema: {
|
|
type: "object",
|
|
properties: { network: str("Network name or ID") },
|
|
required: ["network"],
|
|
},
|
|
handler: (a) => dockerExec(`network inspect -- ${validateName(a["network"], "network")}`),
|
|
},
|
|
{
|
|
name: "unraid_network_host_state",
|
|
description:
|
|
"Get host network state (IPv4/IPv6 addresses, routes, listening sockets).",
|
|
inputSchema: empty,
|
|
handler: () => hostState(),
|
|
},
|
|
{
|
|
name: "unraid_network_audit_tcp",
|
|
description:
|
|
"Audit all TCP endpoints: probe IPv4/IPv6 reachability for every published port. Returns classification (dualstack/ipv4-only/ipv6-only/unreachable).",
|
|
inputSchema: {
|
|
type: "object",
|
|
properties: {
|
|
timeout_seconds: num("Probe timeout (0.2-10, default 2)"),
|
|
include_all_endpoints: bool("Include all endpoints (default false, only problems)"),
|
|
},
|
|
},
|
|
handler: (a) => {
|
|
const timeout = Number(a["timeout_seconds"] ?? 2);
|
|
if (timeout < 0.2 || timeout > 10) {
|
|
throw new Error("timeout_seconds must be between 0.2 and 10");
|
|
}
|
|
const includeAll = Boolean(a["include_all_endpoints"] ?? false);
|
|
return auditAllTcpEndpoints(timeout, includeAll);
|
|
},
|
|
},
|
|
{
|
|
name: "unraid_network_lan_probe",
|
|
description:
|
|
"Probe a specific host:port for IPv4 and IPv6 reachability (dualstack test).",
|
|
inputSchema: {
|
|
type: "object",
|
|
properties: {
|
|
host: str("Hostname or IP"),
|
|
port: int("Port (1-65535)"),
|
|
timeout_seconds: num("Timeout (0.2-10, default 3)"),
|
|
},
|
|
required: ["host", "port"],
|
|
},
|
|
handler: (a) => {
|
|
const host = validateName(a["host"], "host");
|
|
const port = Number(a["port"] ?? 0);
|
|
const timeout = Number(a["timeout_seconds"] ?? 3);
|
|
if (port < 1 || port > 65535 || timeout < 0.2 || timeout > 10) {
|
|
throw new Error("Invalid port or timeout");
|
|
}
|
|
return probeDualstack(host, port, timeout);
|
|
},
|
|
},
|
|
|
|
// ── System (2) ────────────────────────────────────────────────────────
|
|
{
|
|
name: "unraid_system_connection_test",
|
|
description:
|
|
"Test connection to the Unraid host (hostname, kernel, Unraid version).",
|
|
inputSchema: empty,
|
|
handler: () => connectionTest(),
|
|
},
|
|
{
|
|
name: "unraid_system_shell",
|
|
description:
|
|
"Execute a shell command on the Unraid host (as root, via /bin/sh -c) and return exit code, stdout, and stderr. Use for direct terminal access: file inspection, system commands, package info, log reading, etc. Commands run with a timeout and output is size-limited.",
|
|
inputSchema: {
|
|
type: "object",
|
|
properties: {
|
|
command: str(
|
|
"Shell command to execute on the Unraid host (run via /bin/sh -c)",
|
|
),
|
|
timeout_seconds: int("Timeout in seconds (1-300, default 60)"),
|
|
},
|
|
required: ["command"],
|
|
},
|
|
handler: (a) => {
|
|
const command = (a["command"] as string) ?? "";
|
|
if (command.trim() === "") throw new Error("command is required");
|
|
const timeout = Number(a["timeout_seconds"] ?? 60);
|
|
if (timeout < 1 || timeout > 300) {
|
|
throw new Error("timeout_seconds must be between 1 and 300");
|
|
}
|
|
return runShell(command, timeout);
|
|
},
|
|
},
|
|
];
|
|
|
|
export function toolByName(name: string): ToolDef | undefined {
|
|
return TOOLS.find((t) => t.name === name);
|
|
}
|