The server is built to be safe to point at a real Unraid box by default. The short version: it can't change anything unless you opt in, it can't leak your API key, and the network transport is locked down.
Monitoring tools are always available. State-changing tools are registered only
when UNRAID_MCP_ALLOW_MUTATIONS=true. Even then, every mutating tool requires an
explicit confirm=true and refuses before making any network call without it — so
an agent can't change anything by accident, and a refusal never touches your server.
There is intentionally no host reboot/shutdown tool: the Unraid GraphQL API doesn't expose those mutations, so neither does this server.
Tools are grouped into three tiers, each behind its own flag. The tiers are
cumulative and the dangerous tier is gated by the mutations flag on top of its own —
enabling UNRAID_MCP_ALLOW_DANGEROUS without UNRAID_MCP_ALLOW_MUTATIONS unlocks
nothing.
| Tier | Flag | Default | Unlocks |
|---|---|---|---|
| Read | (always on) | on | All monitoring/read tools. Never change anything. |
| Mutate | UNRAID_MCP_ALLOW_MUTATIONS |
off | Everyday writes: start/stop array, start/pause/resume/cancel parity, start/stop/restart/update Docker containers (single + batch), start/stop/pause/resume/reboot/force-stop/reset VMs, notification archive/unarchive/unread/delete (single and bulk, plus create_notification — see below). delete_archived_notifications is annotated destructive (irreversible bulk delete) but lives in this tier, not the dangerous one. |
| Dangerous | UNRAID_MCP_ALLOW_DANGEROUS (requires mutations too) |
off | High-blast-radius topology/removal ops (see below). |
Notification lifecycle tools (mutate tier, all require confirm=true):
archive_notifications/unarchive_notifications— bulk archive/unarchive by id.unarchive_all_notifications— bulk unarchive, optionally filtered by severity.delete_archived_notifications— annotateddestructive: permanently deletes every archived notification in one call (irreversible).create_notification— the agent→operator channel: posts a notification into the Unraid WebGUI bell so an agent can leave the operator a persistent message.
Dangerous-tier tools (all annotated destructive, all require confirm=true):
mount_array_disk— bring one array disk online.unmount_array_disk— take one array disk offline; its data becomes inaccessible until remounted.clear_disk_statistics— reset a disk's read/write/error I/O counters (unrecoverable).add_disk_to_array— assign a physical disk to the array (array must be stopped; can overwrite/format the disk once started).remove_disk_from_array— drop a disk from the array config (array must be stopped; data becomes inaccessible).remove_docker_container— permanently delete a container, and optionally (with_image=true) its underlying image.update_all_docker_containers— pull + recreate every container with an available update; fleet-wide, restarts many services at once (each with brief downtime). Updating one specific container stays in the everyday mutate tier (update_docker_container/update_docker_containers).
Splitting these out means you can safely hand an agent everyday container/array control without also handing it the ability to reshape the array or delete containers — those stay locked until you deliberately opt into the dangerous tier.
Use a scoped Unraid API key. A guest/read key is enough for all the read-only
tools — only create a wider-scoped key if you actually enable mutations.
The API key is held as a SecretStr, never logged, and never appears in tool output
or error messages. A redaction filter scrubs it (and any generated bearer token) from
all log lines as defence in depth.
All logs go to stderr; stdout carries only the JSON-RPC protocol. The default
stdio transport has no network surface at all.
When you run streamable-http:
- It binds
127.0.0.1by default. - It requires a bearer token, compared in constant time. Requests with a missing or
duplicated
Authorizationheader are rejected. - Operator-supplied bearer tokens must be at least 32 characters and cannot be
common placeholders such as
change-me. - DNS-rebinding protection (Host/Origin validation) is on automatically for
localhost binds. For a non-localhost bind, set
UNRAID_MCP_ALLOWED_HOSTSto keep it on — the server warns if you don't. - TLS: set
UNRAID_MCP_TLS_CERT+UNRAID_MCP_TLS_KEYto serve HTTPS directly, or terminate TLS at a reverse proxy — see connectivity.md for concrete recipes (LAN, Tailscale, WireGuard, SWAG/NPM/Caddy) and the exactUNRAID_MCP_ALLOWED_HOSTSvalue for each. The server warns loudly if it's serving plaintext on a non-localhost address. Don't expose it to untrusted networks. - One unauthenticated exception:
GET /healthbypasses the bearer gate entirely. It's a static200 {"status":"ok"}— no version/build info, and it never calls the Unraid API — so an unauthenticated caller learns nothing beyond "the process is up" and can't use it to probe the box. Every other path still 401s without a valid token. It also bypasses DNS-rebinding Host-header validation, since that check lives inside the MCP app the health middleware wraps in front of — acceptable given the fixed, no-lookup response.
Requests to the Unraid API verify TLS by default and ignore ambient proxy
environment variables such as HTTP_PROXY, HTTPS_PROXY, and NO_PROXY. That
keeps API traffic from being silently redirected through process-level proxy
settings; route the container or host directly to the Unraid API endpoint.
get_system_info includes the boot flash device's GUID (flash.guid), which is
also the identifier Unraid ties your license to. That's expected for a tool
talking to your own box, but keep it in mind if you ever relay tool output
somewhere less trusted than your own agent session.
Tool results echo strings that originate on the Unraid box — container names, share comments, notification titles/descriptions, Docker container logs, and system log content. A hostile or compromised service there could plant prompt-injection text in them — log output is workload-controlled and gets special mention because it's often long, freeform, and easy to overlook as "just output". MCP clients and agents should treat all tool output as data, never as instructions.
Container logs can also contain secrets that the user's own containers print (API
keys, tokens, connection strings). That's inherent to reading logs and not something
this server can filter — treat get_docker_container_logs output with the same care
as any other secret-bearing log stream.
read_log_file only accepts paths under /var/log — the prefix the Unraid API
serves system logs from — and rejects anything else with a ToolError before
making any network call, pointing the caller back to list_log_files for a valid
path. This is defense-in-depth on top of server-side validation, not a substitute
for it. lines is capped at 500 per call to bound response size; page through
larger files with start_line.
Only typed GraphQL operations are issued. The optional raw-query tool
(UNRAID_MCP_ALLOW_RAW_QUERY=true) parses the document and allows only query
operations — mutations and subscriptions are rejected, including ones hidden behind
comments or leading whitespace.