py-unifi-mcp
# py-unifi-mcp
A small Python MCP server for local UniFi Network inspection. Built with the
same FastMCP, httpx, and Pydantic structure as py-ynab-mcp.
The initial feature uses a local API key or UniFi OS session authentication and exposes
read-only inventory. See [the charter](docs/ai/charter.md) and
[steel thread spec](docs/ai/archive/steelthread/spec.md).
For expansion choices, see the preserved [feature landscape](docs/research/feature-landscape.md),
[official API catalog](docs/research/official-api-catalog.md), and sanitized live
capability evidence linked there. Health/device detail and rename/locate are the
selected expansion; other groups remain deferred.
Requires Python 3.12+ and a UniFi OS console with local Network access. This
initial version uses fixed legacy Network endpoints. Standalone Network
controllers, cloud accounts and MFA login are not supported. API-key access to
these legacy endpoints depends on the controller version and key permissions;
this is not an Integration API implementation.
## Install and run
From this repository, using an external uv environment:
```sh
uv venv "$WORKSPACE_ROOT/scratch/.venvs/py-unifi-mcp" --python 3.12
uv pip install --python "$WORKSPACE_ROOT/scratch/.venvs/py-unifi-mcp/bin/python" -e . --group dev
```
Set `UNIFI_URL` to an HTTPS origin such as `https://controller.example` (no
trailing slash, path, query, or credentials). Supply either `UNIFI_API_KEY` or both
`UNIFI_USERNAME` and `UNIFI_PASSWORD` through your secret runner's environment
injection. Mixed credentials are rejected. The credential must have access to
the desired sites; prefer read-only privileges unless scoped writes are required.
Do not put passwords in source files or command arguments. Optional `UNIFI_SITE`
defaults to `default`. Optional `UNIFI_CA_FILE` is a PEM CA bundle for a private CA;
otherwise the normal system trust configuration is used. TLS verification is
mandatory. Redirects and environment HTTP proxies are disabled.
Configure your MCP client to execute the absolute path to
`$WORKSPACE_ROOT/scratch/.venvs/py-unifi-mcp/bin/py-unifi-mcp` with these environment
variables. The server uses stdio. API-key mode sends `X-API-Key` on requests, never
logs in, and never falls back to username/password. Session mode keeps cookies
only in memory, logs in lazily on the first inventory request, and can
reauthenticate once when a read encounters an expired session. Transport failures
and login are not retried.
## Tools
| Tool | Inventory |
| --- | --- |
| `unifi_list_sites` | Accessible sites and descriptions |
| `unifi_list_devices` | Device identity, model, version, address and numeric state |
| `unifi_list_clients` | Currently connected clients and connection identity |
| `unifi_find_clients` | Exact MAC/IP or name/hostname substring across current and retained clients |
| `unifi_get_client` | Exact MAC with separate current and last-known connection details |
| `unifi_list_networks` | Network name, purpose, VLAN and subnet |
| `unifi_get_health` | Subsystem health and available device/client metrics |
| `unifi_get_device` | Exact `device_id`: resources, uplink, firmware/update availability |
| `unifi_rename_device` | Preview or execute an exact-device rename with `name` |
| `unifi_locate_device` | Preview or execute LED locate start/stop with `enabled` |
List tools and health accept `offset` (default 0) and `limit` (default 50, maximum 100).
Device, client and network tools also accept a validated optional `site` identifier.
Results contain `items`, `total`, `offset`, `limit`, `returned`, `truncated`,
`has_more`, and `next_offset`. `truncated` means this page omits some items in the
snapshot, including earlier pages. `has_more` indicates a following page.
Pagination bounds MCP output; the controller still returns its full inventory
snapshot, and ordering may change between requests.
Client search requires a nonblank `query`; valid MAC/IP queries match that exact
address, otherwise matching is a case-insensitive name/hostname substring. All
matches are returned, sorted by normalized MAC and paginated. Set
`include_offline=False` to skip retained history. Detail lookup requires `mac`
(colon, hyphen or compact hex form). Both use fixed GET `stat/sta`; retained
history uses `stat/alluser`, and device names use `stat/device`. A failed required
source fails the tool rather than presenting an incomplete search as complete.
Client `current` fields come only from the connected snapshot; `last_known`
contains explicit historical metadata. `not_connected` means absent from the
successful current controller snapshot, not proof of sleep, offline state or
unreachability. Reads are sequential snapshots, not atomic. Retained history is
controller-limited, not a complete lifetime inventory. Last-known IPs are not
DHCP reservations or safe automatic SSH targets. Duplicate client MACs fail
clearly; ambiguous device names are not guessed. Missing telemetry stays null.
DHCP, firewall/port configuration, Wake-on-LAN, Windows and Ollama control remain
outside this feature. See [client lookup spec](docs/ai/archive/client-lookup/spec.md).
Only explicitly modeled fields are returned. WLAN passwords, VPN material and
arbitrary nested settings are discarded. Network addresses and client names are
still private network data visible to the MCP consumer. Treat device names and
other controller text as untrusted data. Unknown numeric device state codes are
preserved without guessing their meanings. Missing telemetry remains null.
Rename and locate default to `dry_run=True`: resolve the exact adopted device ID
in the selected site and show the target and proposed change without mutating it.
Execution requires explicit `dry_run=False`, API-key authentication, and user intent
for that target/change. Session authentication supports reads/previews only.
Names must be nonblank, bounded, and free of control characters. No arbitrary
device settings, restart, or network changes are exposed.
Writes are never retried or replayed. Rename distinguishes controller acknowledgement
from name read-back verification; a failed read-back is unverified, not a reason to
repeat the mutation. Locate acknowledgement cannot verify the physical LED. Set
`enabled=False` to stop locating; a start is not automatically stopped. Transport
or invalid-response failures have an unknown outcome: inspect before acting again.
These legacy writes are based on community source contracts. Live rename read-back
and locate start/stop acknowledgement were tested on the development controller;
support still depends on controller version, key permissions and device model.
See the completed [device operations spec](docs/ai/archive/device-operations/spec.md).
Malformed responses, denied access, legacy API errors, and connection failures
produce sanitized errors. Schema failures do not silently become empty inventory.
## Development
```sh
"$WORKSPACE_ROOT/scratch/.venvs/py-unifi-mcp/bin/ruff" check .
"$WORKSPACE_ROOT/scratch/.venvs/py-unifi-mcp/bin/ruff" format --check .
"$WORKSPACE_ROOT/scratch/.venvs/py-unifi-mcp/bin/mypy" src/
"$WORKSPACE_ROOT/scratch/.venvs/py-unifi-mcp/bin/pytest"
uv build --wheel
```
Tests use synthetic HTTP transports, including an MCP stdio subprocess test;
they do not contact a controller. Live verification uses an external local runner
with in-memory secret injection; controller details and verification evidence
remain outside this repository.
TDQS
Scored across 8 tools
Each tool targets a distinct resource or action: networks, sites, devices, clients, health, device telemetry, device rename, and device locate. There is no meaningful overlap or confusion between tool purposes.
All tools use the unifi_ prefix followed by a clear verb_noun pattern such as list_networks, get_health, rename_device, and locate_device. The naming is uniform and predictable across the entire set.
Eight tools is well-scoped for a UniFi controller MCP server covering inventory lookup, status checks, and common device actions. Each tool serves a clear purpose without unnecessary bloat.
The tool set covers core UniFi operations well: listing sites, networks, devices, clients, checking health, and retrieving device details. Minor gaps exist, such as no network configuration or additional device management actions, but the main workflows are represented.