CLI, MCP server, and Rust client library for managing UGREEN NAS (UGOS) devices.
UGOS ships a web UI but no CLI and no API documentation. Reaching past it with virsh/qemu makes UGOS lose track of its own VMs, so this project drives the same (undocumented) API the web UI uses — reverse-engineered from its bundles and verified against real hardware.
Covers virtual machines, Docker, files, downloads, system monitoring, logs and users.
⚠️ Work in Progress — A large subset of the UGOS API, not all of it. See the implementation status for what is covered and what is deliberately left out.
| Crate | Description |
|---|---|
ugos-client |
API client library — auth, types, API calls |
ugos-cli |
Command-line interface |
ugos-mcp |
MCP server for AI-assisted NAS management |
# Set credentials (or use --host, --user, --password flags)
export UGOS_HOST=192.168.1.10
export UGOS_USER=admin # UGOS_USERNAME works too
export UGOS_PASSWORD=<password>
# List VMs
ugos-cli vm list
# Show VM details
ugos-cli vm show CachyOS
# Power management
ugos-cli vm start CachyOS
ugos-cli vm stop CachyOS
ugos-cli vm stop --force CachyOS
# Snapshots
ugos-cli vm snapshot list CachyOS
ugos-cli vm snapshot create CachyOS # UGOS names it after the creation time
# Create a VM
ugos-cli vm create debian --cores 4 --memory 8g --disk 50g \
--iso /volume1/iso/debian.iso
# Print the request body instead of creating anything (works offline)
ugos-cli vm create debian --cores 4 --memory 8g --disk 50g --dry-run
# Upload an ISO, from a local file or a URL
ugos-cli image upload ~/Downloads/debian.iso
ugos-cli image upload https://example.org/debian.iso --name debian-13
# Host load and all VMs in one call
ugos-cli overview
# System log, filterable — the KVM audit log is `vm log`
ugos-cli log --module login --size 10
ugos-cli vm log list
# User accounts
ugos-cli user list
ugos-cli user me
# Browse the NAS
ugos-cli fs ls /volume1/download
ugos-cli fs volumes
ugos-cli fs put ./backup.tar.gz /volume1/backups
ugos-cli fs get /volume1/backups/backup.tar.gz
# Filesystem snapshots of shares and home folders (not VM snapshots)
ugos-cli snapshot folders
ugos-cli snapshot list backup
ugos-cli snapshot create backup --desc "before the migration"
ugos-cli snapshot clone backup 3 backup-recovered
# Let the NAS fetch a file straight from the internet
ugos-cli download add https://example.org/big.iso
ugos-cli download list
# NAS hardware, firmware and live readings
ugos-cli system info
ugos-cli system stat
ugos-cli system processes --limit 10
# How much space KVM uses, per volume and per VM
ugos-cli storage df
# Docker containers
ugos-cli docker ps
ugos-cli docker show nginx
ugos-cli docker create nginx --image nginx:latest --publish 8080:80
ugos-cli docker start nginx
ugos-cli docker logs nginx
# Docker images and compose projects
ugos-cli docker images
ugos-cli docker pull redis:7
ugos-cli docker project-ls
ugos-cli docker project-create myapp --file ./compose.yaml
# Passthrough hardware
ugos-cli usb list
ugos-cli passthrough list
# Remote console
ugos-cli vnc list
ugos-cli vnc generate CachyOS
# Other resources
ugos-cli network list
ugos-cli storage list
ugos-cli image list
ugos-cli info
# JSON output, for scripting
ugos-cli -o json vm listEvery command group has more than is shown here; ugos-cli <group> --help
lists the rest.
use ugos_client::{UgosClient, Credentials};
use ugos_client::api::kvm::KvmApi;
#[tokio::main]
async fn main() -> Result<(), Box<dyn std::error::Error>> {
let creds = Credentials {
username: "admin".into(),
password: "<password>".into(),
};
let client = UgosClient::connect("192.168.1.10", 9443, creds).await?;
let vms = client.vm_list().await?;
for vm in &vms {
println!("{}: {}", vm.vir_display_name, vm.status);
}
Ok(())
}Configure in your MCP client (e.g. Kiro, Claude Desktop):
{
"mcpServers": {
"ugos": {
"command": "ugos-mcp",
"env": {
"UGOS_HOST": "192.168.1.10",
"UGOS_USER": "admin",
"UGOS_PASSWORD": "<password>"
}
}
}
}Multiple NAS targets are supported:
{
"mcpServers": {
"ugos": {
"command": "ugos-mcp",
"env": {
"UGOS_HOST_1": "192.168.1.10",
"UGOS_USER_1": "admin",
"UGOS_PASSWORD_1": "<password>",
"UGOS_NAME_1": "nas1",
"UGOS_HOST_2": "192.168.1.11",
"UGOS_USER_2": "admin",
"UGOS_PASSWORD_2": "<password>",
"UGOS_NAME_2": "nas2"
}
}
}
}vm create and vm update map directly onto the fields of the
CreateVirtualMachine and UpdateVirtualMachine request bodies and take the
same flags. Device flags are repeatable and take either a short form or a
key=value,... list; sizes accept k, m, g and t suffixes, and a bare
number means MiB. UGOS itself counts in KiB — the CLI converts, so --disk 40g
really is 40 GiB.
# Two disks on different buses, two NICs, two ISOs, USB passthrough
ugos-cli vm create win11 \
--os windows --os-version win11 \
--cores 8 --memory 16g \
--disk size=60g,bus=ide \
--disk size=500g,bus=sata \
--iso /volume1/iso/win11.iso \
--iso /volume1/iso/virtio-drivers.iso \
--nic name=vnet-bridge0,type=e1000e \
--nic name=vnet-nat,type=e1000e,mac=52:54:00:12:34:56 \
--usb vendor-id=0x8087,product-id=0x0033,bus-id=1,device-id=4 \
--graphics qxl --keyboard de --autostart| Flag | Short form | Keys |
|---|---|---|
--disk |
40g |
size, bus, dev, path, order |
--iso |
/path.iso |
path, dev, order |
--nic |
vnet-bridge0 |
name, type, mac |
--usb |
— | vendor-id, product-id, bus-id, device-id, vendor-name, product-name |
--share |
— | passed through verbatim |
On create, device names (vda, sda, hda, …) are derived from the bus
type and boot order is numbered disks-first, unless dev= or order= says
otherwise. On update, a newly added disk is left unnamed on purpose, because
UGOS insists on naming it (see below). --usb and --share also accept a raw
JSON object when the generated body needs to look different.
vm update starts from the VM's current configuration, so only what a flag
names is changed and everything else is sent back verbatim. The VM has to be
shut off — UGOS answers 3002, Fail to edit virtual machine for a running one
— and its UUID is always the one of the VM named on the command line.
Besides the flags above, which replace a whole device list, update edits
lists incrementally. --set-* picks the entry to change with a match=
selector (disk by dev, ISO by dev or path, NIC by name or MAC):
# Grow a disk, keeping its backing file
ugos-cli vm update CachyOS --set-disk match=vda,size=200g
# Attach a second disk and an install ISO, drop a NIC
ugos-cli vm update CachyOS \
--add-disk 500g \
--add-iso /volume1/iso/rescue.iso \
--rm-nic vnet-nat
# Plain resource changes
ugos-cli vm update CachyOS --cores 8 --memory 32g --autostart
# Preview instead of sending
ugos-cli vm update CachyOS --set-nic match=vnet-bridge0,type=e1000 --dry-run| Operation | Flags |
|---|---|
| Replace the whole list | --disk, --iso, --nic, --usb, --share |
| Append one entry | --add-disk, --add-iso, --add-nic |
| Edit one entry | --set-disk, --set-iso, --set-nic |
| Remove one entry | --rm-disk, --rm-iso, --rm-nic |
Edits run in the order remove, set, add, so a --rm-disk vdb --add-disk 100g
pair really does replace that disk rather than cancelling out — though for a
pure resize --set-disk is the better tool, since it keeps the existing
backing file instead of starting from an empty one. A selector that matches
nothing is an error rather than a silent no-op.
Anything the flags do not cover can come from a full JSON spec, which the flags
then override. For create, --dry-run prints the request body instead of
sending it and needs no NAS connection, so specs can be built and inspected
offline; for update it still reads the VM's current configuration first.
# Clone the configuration of an existing VM under a new name
ugos-cli -o json vm show CachyOS > spec.json
ugos-cli vm create CachyOS-2 --spec-file spec.json --memory 32g
# Import an OVA (parse, then create from the parsed spec)
ugos-cli -o json ova parse /volume1/ova/appliance.ova > spec.json
ugos-cli vm create appliance --spec-file spec.json
# Inspect what would be sent
ugos-cli vm create test --spec-file - --dry-run < spec.jsonA repeatable flag replaces the corresponding list from the spec file rather
than appending to it. The UUID in a spec file is never used: create always
generates a fresh one, and update always keeps the one of the VM it was
pointed at.
create and update were verified end to end against a real NAS (UGOS app
build 656, 2026-08-18): a VM is created and starts; CPU, memory and disk size
are changed and it starts again; a second disk is added and removed; it is
renamed, force-stopped and deleted. Five findings from those runs are baked
into the client:
- Sizes are KiB, not bytes — for disks just as for memory. Sending bytes asks for 1024 times the intended size; UGOS accepts the create but the domain then fails to start.
keyboardLanguagemust be a real QEMU keymap. The web UI sendsen-us; a plainenis accepted byCreateVirtualMachineand then makes every start fail with3037, Failed to start.storageUUIDis mandatory onCreateVirtualMachine— without it the answer is3000, Fail to create virtual machine. The client resolves it fromstorage_name, so--storage volume1is enough, but a--dry-runbody shows it empty because resolving needs the NAS.- UGOS assigns the VM UUID itself and ignores
virtualMachineNamein the body, sovm createreports the UUID it finds in the listing afterwards. - A newly added disk must carry neither
devnorpath. UGOS assigns both and answers3002, Fail to edit virtual machinewhen the body names them itself, sovm createnames its disks andvm updateleaves new ones unnamed. Growing an existing disk with--set-diskkeeps its backing file.
One difference to the web UI remains: it picks defaults per OS type that
this CLI does not — ide plus an e1000e NIC for Windows and other,
virtio for Linux. vm create always defaults to virtio, so a Windows
guest without virtio drivers needs
--disk size=60g,bus=ide --nic name=vnet-bridge0,type=e1000e.
Where UGOS answers with a bare code, the client asks the validators the web UI
uses and turns the answer into something readable: a taken VM or network name,
a VM asking for more memory than the host has, or a network still attached to
VMs. Note that CreateVirtualMachine does not check memory itself — such a VM
is created and only fails to start.
Still inferred. USB passthrough and shared-directory bodies have never been sent to a NAS, and a second NIC was accepted but not exercised on a running guest. Use
--dry-runto inspect a body before sending it.
brew install metaneutrons/tap/ugos-cliTake the .deb for your architecture from the
latest release and
install it with apt, which resolves dependencies for a local file too:
sudo apt install ./ugos-cli_<version>_amd64.debAn APT repository under deb.metaneutrons.cc is being prepared. It does not
serve yet, so do not add it as a source.
There is no AUR package yet. Use the Linux archive from the releases page.
Every release carries binaries for Linux, macOS and Windows, each on x86_64 and
aarch64, plus .deb packages for amd64 and arm64. See the
releases page.
See docs/packaging.md for how these are built and verified.
cargo install --git https://github.com/metaneutrons/ugos-cli ugos-cli
cargo install --git https://github.com/metaneutrons/ugos-cli ugos-mcpThe crates are not published on crates.io. Depend on the repository directly:
[dependencies]
ugos-client = { git = "https://github.com/metaneutrons/ugos-cli", tag = "v0.9.0" }UGOS publishes no API documentation, so everything here was reconstructed from the web UI's JavaScript bundles and verified against real hardware. The notes are kept because the reasoning is easy to lose and expensive to redo.
| Document | Covers |
|---|---|
| api-overview.md | Every module found, and which are documented |
| api-auth.md | Login flow, RSA key exchange, session tokens |
| api-tls.md | The version 1 certificate and how it is pinned |
| api-encryption.md | The AES-GCM request wrapping and when UGOS uses it |
| api-kvm.md | Virtual machines, snapshots, networks, storage, images |
| api-docker.md | Containers, images, compose projects |
| api-files.md | File manager, including upload and download |
| api-downloadcenter.md | Queuing downloads for the NAS to fetch |
| api-system.md | Machine info, monitoring, processes, services |
| api-snapshot.md | Filesystem snapshots of shares and homes |
| packaging.md | Release channels, signing and reproducibility |
| api-backup.md | Why no backup commands exist |
| Resource | Operations |
|---|---|
| VM | list, show, start, stop, force-stop, reboot, force-reboot, delete, create, update |
| Snapshot | list, create, delete, revert, describe |
| Network | list, show, create, update, delete |
| Storage | list (with VM count), usage, add, delete, df (usage per VM) |
| Image | list, upload (file or URL), register, delete, usage |
USB (usb) |
list |
PCI passthrough (passthrough) |
list devices |
| VNC | list links, generate noVNC link |
| OVA | export, parse |
| Log | system log across all modules, with filters |
VM log (vm log) |
KVM audit log, list operators |
| User | list accounts, show own account |
Host (info, overview) |
CPU cores and memory; load plus every VM at once |
| System | hardware and firmware info, live CPU/memory/disk/network/fan readings, processes, services |
| Download | queue a URL for the NAS to fetch, list, check, status, remove |
Files (fs) |
list a directory, list volumes, upload, download, create, rename, delete |
Filesystem snapshots (snapshot) |
list folders, list, create, edit, delete, clone |
Docker container (docker) |
list, show, create, start, stop, restart, kill, remove, update, clone, batch-operate, logs |
| Docker image | list, search, download, delete, export, load (URL/path) |
| Docker registry | list/add/delete/switch mirror, HTTP proxy get/set |
| Docker overview | engine status, resource usage |
Docker compose (docker project-*) |
list, show, create, start, stop, restart, remove |
| Auth | RSA key exchange, PKCS1v1.5 encryption, session tokens, auto re-auth |
| Transport | certificate pinning on first use, shared by CLI and MCP server |
Docker container create/update were reverse-engineered against the real
CreateContainer request body (live-captured 2026-08-06, see
docs/api-docker.md) — this caught two bugs that had never actually
been exercised against a live NAS: port_mapping was built with wrong field
names (hostPort/protocol instead of the real nasPort/portType), and
subnet_settings/gpu_ids were entirely missing from ContainerDetail.
Both are fixed now; the CLI's docker container create --port flag has been
tested end-to-end against a real NAS (nginx container, port mapping,
env vars).
Docker compose project management (create/list/show/stop/remove) was
reverse-engineered against the live CreateProject/GetProjectListV3/
StopProject/DownProject endpoints (live-captured 2026-08-07, see
docs/api-docker.md) and tested end-to-end on a live NAS (a two-service
nginx+redis project, created, verified running, stopped, and removed). The
start/restart endpoints (StartProject/RestartProject) are implemented
by analogy with StopProject and the container-level start/restart pattern
but have not been live-verified — confirm before relying on them for
anything critical.
| Resource | Notes |
|---|---|
| Image rename | RenameImage answers successful and renames nothing; field names unknown |
| OVA import (one step) | ova parse reads an OVA into a VM spec; creating the VM from it is still a manual second step |
| Backup | There is no backup API. The only three backup_restore paths appear solely in the UI's encryption whitelist, nothing calls them, and the group has no status or listing endpoint — see docs/api-backup.md |
| Snapshot restore | snapshot/snapshot/restore rolls a folder back; destructive and untestable without risking real data. snapshot clone covers recovery without it — see docs/api-snapshot.md |
| Log deletion | DeleteLogs is destructive and was not probed |
| Non-KVM modules | Photo, video, music, etc. |
UGOS uses a multi-step auth flow:
- RSA key exchange —
POST /verify/checkreturns an RSA public key - Password encryption — PKCS1v1.5 padding (not OAEP)
- Login —
POST /verify/loginreturns a session token + cookies - Authenticated requests — cookies +
?token=query parameter
The client handles this automatically, including transparent re-authentication when tokens expire (UGOS error code 1024).
UGOS serves a self-signed X.509 version 1 certificate, which no ordinary
certificate parser will accept. Rather than trusting whatever is presented,
the client pins the certificate on first contact and refuses anything else
afterwards, the way SSH treats host keys. Fingerprints are stored per
host:port in known_hosts.json in the user's config directory.
| Flag | Effect |
|---|---|
| (none) | Pin on first contact, require a match afterwards |
--tls-trust-new |
Record a changed certificate, for renewals or reinstalls |
--tls-insecure |
Skip the check entirely |
The first connection is only as trustworthy as the network it happens on, so the fingerprint is printed for comparison against the device. The handshake signature is verified against the pinned certificate's own key, which is what makes the pin meaningful — see docs/api-tls.md for why that needs handling by hand, and why requests are not all encrypted.
| Model | UGOS Version | Notes |
|---|---|---|
| DXP480T Plus | 1.18.1.0098 | Everything in this README is verified against this machine |
| DXP480T Plus | 1.14.1.0107 | Earlier verification |
| DXP4800 Plus | 1.14.x | Reported working, not verified here |
UGOS moves fields around between builds, so a mismatch is worth reporting.
virID vanished from the VM listing in one such build and broke vm list
until every field but the name was made optional.
- Rust 1.98+ (edition 2024)
- Network access to the NAS over HTTPS (port 9443)
- The KVM app for
vm,network,storage,image,usb,vncandova; the Docker app fordocker. The remaining groups need neither.
This project is licensed under the GNU General Public License v3.0,
the ugos-client library included. A library would ordinarily be the place for
the weaker copyleft of the LGPL; here it is not, because the library serves this
project's own programs and every release so far has been GPL.