Always-up, privileged MCP relays that let Claude Code administer home-lab appliances it can't (or shouldn't) run an agent on directly. The model runs on your Mac; each appliance runs only a tiny relay that executes structured requests locally and returns results.
The project is two layers:
| Layer | What it is | Lives in |
|---|---|---|
Core (mage_hands_core) |
Reusable, pip-installable relay framework: token auth, forensic audit, the lease gate, the gated run() tool, and the read path policy. The security machinery lives here once, so every appliance inherits it. |
common/ |
| Appliances | Thin servers that pick an executor (how to run on the target) and register target-specific tools. synology-hands administers a Synology NAS (privileged container + nsenter). |
synology-hands/ |
Heads-up: the relay is always up. Read-only inspection tools always work. Anything that can change the box (
run,read_file, restarts, firewall changes) needs a time-limited lease that Michael grants out-of-band with a YubiKey touch (nas-unlock <appliance>on the Mac); a locked call returns a structured refusal telling Claude to ask for it. The relay is root on the target and is not sandboxed; safety comes from network isolation (loopback bind + tailnet-onlytailscale serve), a bearer token, the lease gate, dry-run/replay gating on raw execution, and a forensic audit log. (The host's own WAN exposure is separate; theinternet_exposuretool surfaces it.)
┌──────────────┐ https://<nas>.<tailnet>.ts.net/mcp ┌─────────────────────────────┐
│ Claude (Mac) │ ─── Tailscale Serve (TLS, ACL'd) ───► │ appliance (e.g. Synology) │
│ MCP client │ ◄── structured JSON ─────────────────│ Tailscale + Serve :443 │
└──────────────┘ Authorization: Bearer <token> │ ↓ proxy 127.0.0.1:8787 │
(Serve injects Tailscale-User-*) │ relay container (always up)│
│ privileged, pid:host, /:/ │
│ FastMCP /mcp → nsenter → │
│ host (docker, smartctl…) │
└─────────────────────────────┘
- The relay runs in a privileged container and drives the host via
nsenter -t 1— so it uses the host's own toolchain (docker, smartctl, syno*), no docker-socket mount. tailscale serveterminates HTTPS on the tailnet (never the public internet) and proxies to the relay bound on loopback only.- Your Mac connects to it as a normal remote MCP server; tools appear as
mcp__<name>__*.
See ARCHITECTURE.md for the request lifecycle, security model, tool tiers, and audit schema; AGENTS.md for a contributor/agent guide; lessons.md for deployment lessons and Synology gotchas.
Already deployed and just want to use it? → docs/getting-started.md
(the tools load in any Claude session; unlock with nas-unlock when a change is needed).
Deploying to a new appliance? → docs/deploy.md (end-to-end runbook).
- An appliance that runs Docker/Container Manager (x86 Synology DSM 7.2+ for
synology-hands). - Tailscale on both the appliance and your Mac, with MagicDNS + HTTPS enabled on the tailnet.
- Claude Code on your Mac.
The relay exposes capabilities in three tiers (see ARCHITECTURE.md):
| Tier | Nature | Examples | Gating |
|---|---|---|---|
| A | inspection (read-only) | system_info, disk_usage, storage_health, list_containers, container_logs, internet_exposure, performance, pending_updates, firewall_status, firewall_rules, firewall_diagnose |
none: always open |
| A, gated | file read | read_file |
lease + allow/deny policy |
| B | controlled mutation | restart_container, restart_service, firewall_enable, firewall_disable, firewall_reload, firewall_set_rules, pending_updates(check=true) |
lease; typed args; firewall changes are lock-out-guarded |
| C | raw root exec | run(command, exec_token) |
lease + dry-run → one-time replay token + catastrophic-pattern denylist |
mage-hands/
├── common/ # mage_hands_core — the reusable relay framework
│ ├── pyproject.toml
│ └── mage_hands_core/
│ ├── server.py # build_server(): FastMCP + auth + audit + lifespan
│ ├── auth.py # StaticTokenVerifier (real 401 at the transport layer)
│ ├── audit.py # forensic JSONL log + identity allowlist + lease gate
│ ├── lease.py # the lease file: read / validate / slide, fail closed
│ ├── exec.py # Runners (Shell/Nsenter) + gated run() (token + timeouts)
│ ├── denylist.py # catastrophic-command regexes for run()
│ ├── policy.py # PathPolicy + policied read_file
│ ├── meminfo.py # memory_stats(): MemAvailable, emulated on DSM's 3.10 kernel
│ └── config.py # env-driven Config
├── synology-hands/ # appliance #1: Synology NAS
│ ├── server.py # Tier A/B tools + lease tiers; registers read_file + run()
│ ├── firewall.py # DSM firewall audit + lock-out-guarded changes
│ ├── Dockerfile compose.yaml .env.example
│ ├── scripts/ # mage-hands-lease · install-host · smoke-test.py · tailscale/plex/watchtower-update · ups-healthcheck
│ └── README.md
├── net-monitor/ # standalone (NOT an MCP relay): always-on internet connectivity logger on kappa
│ ├── compose.yaml monitor.sh summary.sh
│ └── README.md
├── docs/
│ ├── getting-started.md # use a deployed relay from a fresh Claude session
│ ├── deploy.md # deploy/operate a new appliance
│ ├── maintenance.md # update Tailscale, scheduled tasks, troubleshooting
│ └── lease-gate-design.md # the approved lease-gate design
├── ARCHITECTURE.md AGENTS.md CLAUDE.md lessons.md
├── CONTRIBUTING.md # how to contribute + security-first ground rules
├── LICENSE # Apache-2.0
└── README.md
MIT © Michael Timpe.