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 and steel thread spec.
For expansion choices, see the preserved feature landscape, official API catalog, 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:
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 devSet 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 |
| Accessible sites and descriptions |
| Device identity, model, version, address and numeric state |
| Currently connected clients and connection identity |
| Exact MAC/IP or name/hostname substring across current and retained clients |
| Exact MAC with separate current and last-known connection details |
| Network name, purpose, VLAN and subnet |
| Subsystem health and available device/client metrics |
| Exact |
| Preview or execute an exact-device rename with |
| Preview or execute LED locate start/stop with |
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.
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.
Malformed responses, denied access, legacy API errors, and connection failures produce sanitized errors. Schema failures do not silently become empty inventory.
Development
"$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 --wheelTests 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.
Latest Blog Posts
- Who's Calling? MCP Hosts Are an Identity Blind Spot (And the Spec Knows It)By Om-Shree-0709 on .mcpAgent IdentityOAuth 2.1
- Your AI Chatbot Just Exposed Your CEO's Salary to an InternBy Om-Shree-0709 on .Agent IdentityMCP SecurityOAuth Delegation
- Why MCP Servers Need Execution Sandboxing (And Why Your Current Stack Isn't Enough)By Om-Shree-0709 on .Agentic AiPrompt InjectionWebAssembly
MCP directory API
We provide all the information about MCP servers via our MCP API.
curl -X GET 'https://glama.ai/api/mcp/v1/servers/duganth/py-unifi-mcp'
If you have feedback or need assistance with the MCP directory API, please join our Discord server