ugnas-mcp / docs UGREEN NAS · UGOS · SSH/SFTP v0.1.1

ugnas-mcp_

A generic Model Context Protocol server for UGREEN NAS running UGOS, spoken to over SSH. Nothing hard-coded — no fixed IPs, subnets, usernames, or share names. Point it at yours.

npx -y ugnas-mcp
Tools
39
Runtime
Node ≥ 20
Language
TypeScript · ESM
Transports
stdio · HTTP
License
MIT

Scope

01 / 06

Five tool groups, one safety model

Organized the way a NAS owner thinks about daily operations. Every mutating operation plans first; read operations run directly.

GroupCovers
systemConnectivity & system. SSH check, OS/hardware report (CPU, memory, thermals, disks), and storage-pool health across mdadm RAID, LVM, btrfs, and block devices.
dockerContainers, images, networks, compose. Full lifecycle: list, inspect, logs, start/stop/restart, remove; image pull/remove; network create/remove/connect/disconnect; compose up/down/restart.
filesShares & data. Browse, measure, search; SFTP upload/download (chroot-aware); rsync-style sync with a built-in cp fallback; ffmpeg thumbnails rendered on the NAS.
backupArchives & schedules. tar.gz creation, listing, verification (checksum + entry count), restore, and managed recurring cron jobs that preserve your existing crontab entries.
networkHost networking. Interface inventory with IPv4/IPv6 scopes, both routing tables, and listening TCP/UDP sockets with owning processes.

Safety

02 / 06

Dry-run first, then confirm

Every mutating tool is a two-phase machine. Nothing destructive happens on the first call — ever.

1
caller

Call a mutating tool with its arguments — files.remove with path: "media/old". Nothing runs yet.

2
ugnas-mcp

Returns a dry-run plan: the exact command it would run — rm -rf '/volume1/media/old' — plus a summary, warnings, and a confirmation token.

3
you
Decline

Do nothing. The plan expires in 10 minutes; nothing ever touched the NAS.

Approve

Re-call the same tool with the same arguments, plus confirmToken.

4
ugnas-mcp

Validates the token (single-use, action-bound, unexpired), cross-checks the arguments against the plan, then executes the exact commands captured at plan time — verbatim, on the NAS.

Fig. 1 — the confirmation gate. Read-only tools skip it entirely.

Injection-proof by construction. Every user value — paths, container names, image tags — passes through POSIX single-quote escaping before it touches a shell. Backup job names are restricted to [A-Za-z0-9._-] and cron expressions validated as exactly five fields, because both end up inside shell scripts and crontab lines.

Root paths are refused outright. files.remove will not plan a deletion of /, a bare share root (/volume1), top-level system directories, or UGOS-managed directories like @docker — it rejects before planning.

Pinned by fingerprint. Optionally pin the SSH host key (hostKeySha256); comparisons are padding-insensitive so the canonical ssh-keygen -lf output works as-is.

Structure

03 / 06

A small, layered codebase

One module per tool group. A narrow executor interface so the test suite runs against a fake — no NAS required.

src/
├── server.ts              entrypoint: stdio + optional stateless HTTP (/mcp, /health)
├── config.ts              zod-validated config; UGNAS_* env overrides win over the file
├── mcp/
│   ├── createServer.ts     McpServer wrapper
│   └── toolset.ts          all 39 tool registrations
├── safety/plan.ts         ConfirmationTokenService — the dry-run/confirm core
├── ssh/
│   ├── sshExecutor.ts      CommandExecutor interface + ssh2-backed SshExecutor
│   └── shellQuote.ts       shq() — every user value passes through it
└── services/              one class per tool group, each takes ServiceContext
    ├── systemService       status / info / pools
    ├── dockerService       containers / images / networks / compose
    ├── fileService         list / usage / find / thumbnail / mkdir / copy / move
    │                       / remove / upload / download / sync
    ├── backupService       create / list / verify / restore / schedule
    ├── networkService      interfaces / routes / ports
    └── util.ts             planOrExecute, path resolution, output parsers

One round-trip for many reads. Read-heavy tools batch commands behind ###NAME### markers and parse the sections from a single SSH call — system.info gathers OS, CPU, load, memory, uptime, thermals, disks, and the Docker version in one shot.

Chroot-aware SFTP. UGOS chroots SFTP to the share root: exec path /volume1/docker/x becomes SFTP path /docker/x. The sftpRoot config owns the mapping; thumbnails travel via base64-over-exec because /tmp sits outside the chroot.

BusyBox-tolerant. GNU-only flags (ls --time-style, find -printf) are probed once and cached; on BusyBox the file tools fall back to ls -1p and find -exec stat and keep working.

Index

04 / 06

The complete ledger — 39 tools

read  runs directly   gated  plans first   dstr  gated, destructive, warned

system.statusSSH connectivity, hostname, kernel, uptime, Docker availability.read
system.infoOS release, CPU cores, load, memory, thermals, disks, Docker version.read
system.poolsmdadm arrays + degradation, LVM, btrfs, block devices, filesystems.read
docker.containers.listContainers with id, name, image, state, status.read
docker.containers.inspectFull docker inspect JSON for one container.read
docker.containers.logsRecent log lines (tail, default 200).read
docker.containers.startStart a stopped container.gated
docker.containers.stopStop a running container.gated
docker.containers.restartRestart a container.gated
docker.containers.removeDelete a container (optional force / anonymous volumes).dstr
docker.images.listLocally stored images.read
docker.images.pullPull an image from its registry.gated
docker.images.removeRemove a local image.gated
docker.networks.listDocker networks.read
docker.networks.inspectNetwork details: subnets, attached containers.read
docker.networks.createCreate a network (driver, subnet optional).gated
docker.networks.removeRemove a network (fails if containers attached).gated
docker.networks.connectAttach a container to a network.gated
docker.networks.disconnectDetach a container from a network.gated
docker.composeup -d / down / restart for a stack directory.gated
files.listDirectory listing (name, type, size, owner, modified).read
files.usageDisk usage (du), optionally per subdirectory.read
files.findSearch by name glob and/or extension; path, size, mtime.read
files.thumbnailffmpeg preview of an image/video frame → local file (write gated).gated
files.mkdirCreate a directory (parents included).gated
files.copyCopy within the NAS (or NAS-to-NAS).gated
files.moveMove or rename a path.gated
files.removeDelete a file or directory (recursive by default; root paths refused).dstr
files.uploadSFTP upload, local machine → NAS.gated
files.downloadSFTP download, NAS → local machine.gated
files.syncrsync sync with mirror/dry-run; cp -a fallback when rsync is missing.gated
backup.createArchive a directory into a .tar.gz on the NAS.gated
backup.listArchives (*.tar.gz, *.tgz, *.tar, *.zip) in a directory.read
backup.verifyArchive exists, checksum, entry count, sample entries.read
backup.restoreExtract an archive into a destination (overwrites).dstr
backup.scheduleManaged cron job for recurring backups.gated
network.interfacesInterfaces with IPv4/IPv6 addresses and scopes.read
network.routesIPv4 and IPv6 routing tables.read
network.portsListening TCP/UDP sockets and owning processes.read
39 / 39 shown ledger is generated from the wiring contract

Setup

05 / 06

Configuration & wiring

The server reads a JSON config from ~/.ugnas-mcp/config.json (override with UGNAS_MCP_CONFIG). Environment variables win over the file — useful for CI and secret injection: UGNAS_HOST, UGNAS_PORT, UGNAS_USER, UGNAS_KEY_PATH or UGNAS_AUTH_TYPE=password + UGNAS_PASSWORD, UGNAS_DEFAULT_SHARE, UGNAS_HOST_KEY_SHA256.

# ~/.ugnas-mcp/config.json
{
  "name": "home-nas",
  "host": "192.168.168.110",
  "port": 22,
  "username": "admin",
  "auth": { "type": "key", "privateKeyPath": "~/.ssh/id_ed25519" },
  "defaultShare": "/volume1",
  "commandTimeoutMs": 60000
}

Client wiring (stdio)

# official package — no clone, no build
npx ugnas-mcp                # or: npm i -g ugnas-mcp && ugnas-mcp

# client config
{
  "mcpServers": {
    "ugnas": {
      "command": "npx",
      "args": ["-y", "ugnas-mcp"],
      "env": { "UGNAS_MCP_CONFIG": "/absolute/path/to/config.json" }
    }
  }
}

Optional HTTP transport

stdio by default. Set UGNAS_HTTP_PORT to also expose a stateless streamable HTTP transport at /mcp (1 MiB body cap) plus /health. Loopback only, unless UGNAS_HTTP_ALLOW_REMOTE=1 — the endpoint carries no authentication, so put a proxy with auth in front if you need it off-box.

UGNAS_HTTP_HOST=127.0.0.1 UGNAS_HTTP_PORT=4110 ugnas-mcp

POST 127.0.0.1:4110/mcp      # JSON-RPC, stateless
GET  127.0.0.1:4110/health   # liveness + target host

On the NAS

Required. SSH enabled (UGOS: Control Panel → Terminal & SNMP); the SSH user in the docker group; crontab access for scheduled backups; standard tools (tar, df, du, ip, ss — all present on UGOS).

Optional. rsync (only files.sync; the cp -a fallback covers its absence) and ffmpeg (only files.thumbnail).

Development

06 / 06

Developing the server

Everything needed to work on the server — commands, tests, architecture, and the release pipeline — lives in DEVELOPMENT.md. CI (GitHub Actions) runs the full chain — typecheck, tests, build, both smoke tests — on every push and pull request.