unas-mcp
MCP server for managing a Ubiquiti UNAS Pro NAS, providing health monitoring, storage and disk inventory, NFS export visibility, read-only file inspection, and gated lifecycle operations via SSH and the UniFi Drive API.
README
unas-mcp
MCP server for managing a Ubiquiti UNAS Pro 8 NAS (6×26 TB RAID-6, ~104 TB
usable) — health monitoring, storage/disk inventory, NFS export visibility,
read-only file inspection, and gated lifecycle operations. 20 tools, deployed
as a container on VM 102 (http://10.0.40.25:8490/mcp).
What it is
The UNAS Pro 8 exposes two independent surfaces, and this server is a thin, safety-gated layer over both:
- SSH (
root@<host>, key-only,src/unas_mcp/ssh.py) —/proc/mdstat,smartctl,df,rpcinfo,exportfs,uptime/last,uname, and a handful of read-only filesystem commands (ls,du). This is the same transportdocs/reference/unas-pro-8-capabilities.md(in thehomelabrepo) documents as voiding Ubiquiti's warranty — used here only where the UI and the Drive API cannot answer the question. - UniFi Drive API (
src/unas_mcp/api.py, unofficial/undocumented — findings indocs/drive-api-findings.md) — physical bay mapping (slotId), vendorhealthScore/riskReasons, and per-RAID-groupcurrentProtectionvsexpectedProtection. SSH's/proc/mdstatandsmartctlare enough for pass/fail health, but they don't carry which physical bay to pull at the rack or the vendor's own risk scoring — that's the entire reason the Drive API transport exists in this server.
Every parser in this project is "tolerant but honest": a field genuinely
absent from a live reply is surfaced as None (or raises, for the small set
of fields health decisions are gated on), and is never silently defaulted
to a value that reads as healthy (0, "ok", False, or a label count
standing in for a verdict actually read). See api.py, parsers/smart.py,
and tools/storage.py's module docstrings for the specific traps this
pattern was built to close.
Tool inventory (20 tools, 5 groups)
| Group | Tool | Tier | What it does |
|---|---|---|---|
| Health (SSH) | unas_health_summary |
read | Rolled-up healthy | unhealthy | unknown verdict across array, SMART, capacity, and NFS — unknown on any transport/parse failure or incomplete reply, never silently healthy |
unas_array_status |
read | Parsed /proc/mdstat — per-array [n/m] state, healthy flag, rebuild/resync/reshape progress |
|
unas_smart_status |
read | One SMART record per drive bay (sda-sdf): verdict + the three failure-predicting counters | |
unas_smart_details |
read | Full SMART health + identity record for one drive bay | |
unas_capacity |
read | Media volume capacity in bytes (total/used/avail) + used_pct, from df -B1 |
|
unas_nfs_state |
read | Whether nfsd/mountd are registered with the local RPC portmapper | |
unas_uptime_boot |
read | uptime + last 5 last -x boot/shutdown records |
|
unas_system_info |
read | uname -a + /etc/os-release |
|
| Lifecycle (SSH) | unas_smart_test_start |
direct | Start a SMART self-test (short/long) on a drive bay |
unas_smart_test_status |
direct | Read a drive bay's SMART self-test log | |
unas_firmware_status |
read | Firmware/OS release identity | |
unas_service_status |
read | Running systemd services | |
unas_reboot |
confirm | Reboot the appliance — ~9.5 min of NFS downtime for every client; requires confirm="yes" |
|
| Storage (Drive API) | unas_pool_status |
read | Every pool's status/type/capacity/usage + per-RAID-group protection state |
unas_disk_inventory |
read | Every physical disk + cache slot: slotId, model, serial, temp, healthScore, riskReasons, error counters |
|
unas_fan_status |
read | Raw fan-control status | |
| NFS (SSH) | unas_nfs_exports |
read | Live exportfs -v table — export paths, client allow-lists, squash options |
unas_fix_permissions |
confirm | Recursive chown 977:988 under a share's .data root (the UNAS's hardcoded NFS all_squash uid/gid) |
|
| Files (SSH) | unas_list_dir |
read | ls -lah on a path under a share |
unas_disk_usage_by_dir |
read | du -h --max-depth=N on a path under a share (bounded depth, 180s timeout) |
Tiers: read = no gate. direct = additive/trivially reversible, no
gate. confirm = requires confirm="yes" (safety.requires_confirmation) —
anything else raises PermissionError and issues no remote command.
Deliberate exclusions
Two categories of tool are absent by design, not merely unimplemented — their absence is the actual protection, not a confirmation prompt sitting in front of them:
- No write file operations. Not move, delete, copy, upload, or mkdir.
The same bytes are already reachable through nine NFS client mounts with
ordinary tools (
mv,rm,cp,rsync, ...) — a second write path to the same ~75 TB of data only doubles the ways to corrupt it. Seetools/files.py's module docstring. (unas_fix_permissionsis the one narrow exception: achown, not a data-touching write, and the only survivor of an investigated-and-cut NFS-allow-list write path — seetools/nfs.py's module docstring for why that one was rejected.) - No pool or disk lifecycle tools at all — no create/destroy/format/
reshape/expand for pools, RAID groups, or disks.
safety.pycalls this theabsenttier: "a confirmation prompt is a speed bump; absence is a wall."is_forbidden_tool_name()insafety.pyis defence in depth on top of that absence (it blocks an obviously-named destructive tool from being registered by accident), but the real guarantee is the exact-match allowlist test (tests/test_health_tools.py:: test_registers_exactly_the_twenty_expected_tool_names) — any new tool, named anything, fails it until a human deliberately updates the expected set.
Environment contract
| Variable | Purpose |
|---|---|
UNAS_HOST |
Hostname/IP of the UNAS Pro 8 |
UNAS_SSH_KEY |
Path to the SSH private key used to reach the UNAS |
UNAS_API_KEY |
UniFi Drive API key (X-API-Key header) for the storage tools — see docs/drive-api-findings.md |
MCP_PORT |
Port the MCP server listens on (default 8490) |
Development
python3.12 -m venv .venv
.venv/bin/pip install -e '.[dev]'
.venv/bin/pytest -v
Docker
docker build -t unas-mcp .
The image includes an openssh-client for the SSH transport.
Deployed
Live on VM 102 (10.0.40.25:8490), registered for Claude Code:
claude mcp add unas --transport http http://10.0.40.25:8490/mcp --scope user
Recommended Servers
playwright-mcp
A Model Context Protocol server that enables LLMs to interact with web pages through structured accessibility snapshots without requiring vision models or screenshots.
Magic Component Platform (MCP)
An AI-powered tool that generates modern UI components from natural language descriptions, integrating with popular IDEs to streamline UI development workflow.
Audiense Insights MCP Server
Enables interaction with Audiense Insights accounts via the Model Context Protocol, facilitating the extraction and analysis of marketing insights and audience data including demographics, behavior, and influencer engagement.
VeyraX MCP
Single MCP tool to connect all your favorite tools: Gmail, Calendar and 40 more.
graphlit-mcp-server
The Model Context Protocol (MCP) Server enables integration between MCP clients and the Graphlit service. Ingest anything from Slack to Gmail to podcast feeds, in addition to web crawling, into a Graphlit project - and then retrieve relevant contents from the MCP client.
Kagi MCP Server
An MCP server that integrates Kagi search capabilities with Claude AI, enabling Claude to perform real-time web searches when answering questions that require up-to-date information.
E2B
Using MCP to run code via e2b.
Neon Database
MCP server for interacting with Neon Management API and databases
Exa Search
A Model Context Protocol (MCP) server lets AI assistants like Claude use the Exa AI Search API for web searches. This setup allows AI models to get real-time web information in a safe and controlled way.
Qdrant Server
This repository is an example of how to create a MCP server for Qdrant, a vector search engine.