@owine/unifi-network-mcp 2.15.0 → 3.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (56) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +257 -77
  3. package/dist/client.d.ts +26 -1
  4. package/dist/client.js +199 -24
  5. package/dist/config.d.ts +11 -0
  6. package/dist/config.js +102 -3
  7. package/dist/guard.d.ts +46 -0
  8. package/dist/guard.js +180 -0
  9. package/dist/index.js +17 -4
  10. package/dist/instructions.d.ts +9 -0
  11. package/dist/instructions.js +32 -0
  12. package/dist/tls.d.ts +14 -0
  13. package/dist/tls.js +58 -0
  14. package/dist/tools/acl.js +123 -50
  15. package/dist/tools/classic.d.ts +3 -0
  16. package/dist/tools/classic.js +54 -0
  17. package/dist/tools/client-history.js +22 -12
  18. package/dist/tools/clients.js +10 -9
  19. package/dist/tools/devices.js +21 -20
  20. package/dist/tools/dns-policies.js +27 -27
  21. package/dist/tools/exposure.d.ts +3 -0
  22. package/dist/tools/exposure.js +118 -0
  23. package/dist/tools/firewall.d.ts +2 -1
  24. package/dist/tools/firewall.js +200 -75
  25. package/dist/tools/hotspot.js +35 -28
  26. package/dist/tools/index.d.ts +13 -1
  27. package/dist/tools/index.js +67 -4
  28. package/dist/tools/networks.d.ts +2 -1
  29. package/dist/tools/networks.js +44 -31
  30. package/dist/tools/sites.js +2 -2
  31. package/dist/tools/supporting.js +22 -21
  32. package/dist/tools/switching.js +16 -15
  33. package/dist/tools/system.js +1 -1
  34. package/dist/tools/traffic-matching.d.ts +2 -1
  35. package/dist/tools/traffic-matching.js +22 -20
  36. package/dist/tools/wifi.js +76 -30
  37. package/dist/utils/full-put.d.ts +11 -2
  38. package/dist/utils/full-put.js +25 -2
  39. package/dist/utils/output-schemas.d.ts +241 -4
  40. package/dist/utils/output-schemas.js +225 -49
  41. package/dist/utils/params.d.ts +12 -0
  42. package/dist/utils/params.js +21 -0
  43. package/dist/utils/pick.d.ts +6 -0
  44. package/dist/utils/pick.js +11 -0
  45. package/dist/utils/responses.d.ts +2 -0
  46. package/dist/utils/responses.js +28 -2
  47. package/dist/utils/safety.d.ts +35 -1
  48. package/dist/utils/safety.js +53 -3
  49. package/dist/utils/secrets.js +3 -0
  50. package/dist/utils/wan-exposure.d.ts +69 -0
  51. package/dist/utils/wan-exposure.js +245 -0
  52. package/dist/write-log.d.ts +52 -0
  53. package/dist/write-log.js +178 -0
  54. package/dist/write-tools.d.ts +27 -0
  55. package/dist/write-tools.js +50 -0
  56. package/package.json +19 -15
package/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 owine
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md CHANGED
@@ -1,33 +1,58 @@
1
1
  # UniFi Network MCP Server
2
2
 
3
- An MCP (Model Context Protocol) server that exposes the UniFi Network Integration API as tools for Claude Code and other MCP clients. Provides 76 tools for managing sites, devices, clients, networks, WiFi, firewalls, ACLs, switching, DNS policies, hotspot vouchers, VPNs, and more.
3
+ An MCP (Model Context Protocol) server that exposes the UniFi Network Integration API as tools for Claude and other MCP clients: sites, devices, clients, networks, WiFi, firewalls, ACLs, switching, DNS policies, hotspot vouchers, VPNs, and more.
4
4
 
5
5
  ## Prerequisites
6
6
 
7
- - Node.js 22.13+ or 24 (see `engines` in `package.json`)
7
+ - Node.js in the range `engines.node` in `package.json` allows
8
8
  - A UniFi Network console with the Integration API enabled
9
- - An API key generated from your UniFi Network console
9
+ - An API key generated from your UniFi Network console (see [Choosing an API key](#choosing-an-api-key))
10
10
 
11
11
  ## Setup
12
12
 
13
- ### Quick start (npx)
13
+ ### Quick start (Claude Code)
14
14
 
15
15
  Add to Claude Code with a single command — no clone or build needed:
16
16
 
17
17
  ```bash
18
- claude mcp add-json unifi-network '{"command":"npx","args":["-y","@owine/unifi-network-mcp@latest"],"env":{"UNIFI_NETWORK_HOST":"192.168.1.1","UNIFI_NETWORK_API_KEY":"your-api-key","UNIFI_NETWORK_VERIFY_SSL":"false"}}' -s user
18
+ claude mcp add-json unifi-network '{"command":"npx","args":["-y","@owine/unifi-network-mcp@latest"],"env":{"UNIFI_NETWORK_HOST":"192.168.1.1","UNIFI_NETWORK_API_KEY":"your-api-key"}}' -s user
19
19
  ```
20
20
 
21
21
  Use `-s user` for global availability across all projects, or `-s project` for the current project only.
22
22
 
23
+ `@latest` runs the newest release every time the server starts, including a new major version. To take fixes but choose when to make breaking upgrades, pin the major version instead (for example `@owine/unifi-network-mcp@3`).
24
+
25
+ ### Claude Desktop
26
+
27
+ Add the server to `claude_desktop_config.json` (Settings → Developer → Edit Config), then restart Claude Desktop:
28
+
29
+ ```json
30
+ {
31
+ "mcpServers": {
32
+ "unifi-network": {
33
+ "command": "npx",
34
+ "args": ["-y", "@owine/unifi-network-mcp@latest"],
35
+ "env": {
36
+ "UNIFI_NETWORK_HOST": "192.168.1.1",
37
+ "UNIFI_NETWORK_API_KEY": "your-api-key"
38
+ }
39
+ }
40
+ }
41
+ }
42
+ ```
43
+
44
+ ### Manual configuration (Claude Code)
45
+
46
+ Alternatively, add the same `"unifi-network"` entry to your `~/.claude.json` under the top-level `"mcpServers"` key.
47
+
23
48
  ### From source
24
49
 
25
- If you prefer to build locally, this project uses pnpm via Corepack — use `pnpm install`, not `npm install`, which ignores `pnpm-lock.yaml` and resolves different dependency versions:
50
+ If you prefer to build locally, this project uses pnpm — use `pnpm install`, not `npm install`, which ignores `pnpm-lock.yaml` and resolves different dependency versions. Node 25 and later no longer bundle Corepack, so install pnpm itself if you don't have it; it switches to the version `packageManager` in `package.json` pins:
26
51
 
27
52
  ```bash
28
53
  git clone https://github.com/owine/unifi-network-mcp.git
29
54
  cd unifi-network-mcp
30
- corepack enable
55
+ npm install -g pnpm # if pnpm is not installed
31
56
  pnpm install
32
57
  pnpm run build
33
58
  ```
@@ -35,21 +60,92 @@ pnpm run build
35
60
  Then add to Claude Code:
36
61
 
37
62
  ```bash
38
- claude mcp add-json unifi-network '{"command":"node","args":["/path/to/unifi-network-mcp/dist/index.js"],"env":{"UNIFI_NETWORK_HOST":"192.168.1.1","UNIFI_NETWORK_API_KEY":"your-api-key","UNIFI_NETWORK_VERIFY_SSL":"false"}}' -s user
63
+ claude mcp add-json unifi-network '{"command":"node","args":["/path/to/unifi-network-mcp/dist/index.js"],"env":{"UNIFI_NETWORK_HOST":"192.168.1.1","UNIFI_NETWORK_API_KEY":"your-api-key"}}' -s user
39
64
  ```
40
65
 
41
66
  ### Environment Variables
42
67
 
43
68
  | Variable | Required | Default | Description |
44
69
  |---|---|---|---|
45
- | `UNIFI_NETWORK_HOST` | Yes | — | IP or hostname of your UniFi Network console |
70
+ | `UNIFI_NETWORK_HOST` | Yes | — | IP or hostname of your UniFi Network console, without `https://` |
46
71
  | `UNIFI_NETWORK_API_KEY` | Yes | — | API key from Network integration settings |
47
- | `UNIFI_NETWORK_VERIFY_SSL` | No | `true` | Set to `false` to skip TLS certificate verification (needed for self-signed certs) |
72
+ | `UNIFI_NETWORK_CA_CERT` | No | — | Absolute path to a PEM file with the console's certificate, to verify a self-signed console without turning verification off (see below) |
73
+ | `UNIFI_NETWORK_VERIFY_SSL` | No | `true` | Set to `false` to skip TLS certificate verification. Not allowed with `api.ui.com` or together with `UNIFI_NETWORK_CA_CERT` |
48
74
  | `UNIFI_NETWORK_READ_ONLY` | No | `true` | Set to `false` to enable write/mutating tools (read-only by default) |
75
+ | `UNIFI_NETWORK_CONSOLE_ID` | No | — | Cloud Connector console ID; used only when `UNIFI_NETWORK_HOST` is `api.ui.com` (see [Cloud Connector](#cloud-connector)) |
76
+ | `UNIFI_NETWORK_WRITE_TOOLS` | No | all write tools | Comma-separated write tool names and/or groups (`devices`, `clients`, `networks`, `wifi`, `hotspot`, `firewall`, `acl`, `dns-policies`, `traffic-matching`) to load; `none` or empty loads none. Unknown names stop the server. Ignored, with a warning, while read-only (see [Limiting write mode](#limiting-write-mode)) |
77
+ | `UNIFI_NETWORK_ALLOW_WAN_EXPOSURE` | No | `false` | Set to `true` to stop refusing writes that could let traffic in from the WAN; they then only need `confirm: true` |
78
+ | `UNIFI_NETWORK_WAN_ZONE_IDS` | No | — | Comma-separated firewall zone IDs (UUIDs) to treat as the WAN, in addition to the system-defined zone named `External`. Recommended when writes are on |
79
+ | `UNIFI_NETWORK_WRITE_LOG` | No | — | Absolute path of a JSON Lines file that every write tool call is appended to. Created `0600` (parent directories `0700`); the server will not start if it is a symlink or writable by others. Ignored, with a warning, while read-only |
80
+
81
+ UniFi consoles ship with a self-signed certificate, so against a local console the first call usually fails with a certificate error. The safe fix is to trust that one certificate: save it and point `UNIFI_NETWORK_CA_CERT` at the file.
82
+
83
+ ```bash
84
+ openssl s_client -connect 192.168.1.1:443 </dev/null 2>/dev/null | openssl x509 > ~/.config/unifi-console.pem
85
+ ```
86
+
87
+ Set `UNIFI_NETWORK_CA_CERT` to the file's absolute path (for example `/home/you/.config/unifi-console.pem`); MCP hosts do not expand `~` in `env`. The server then accepts that exact certificate even though it is issued to a name like `unifi.local` rather than the IP you connect to, and whether it is self-signed or issued by a private CA; any other certificate must chain to the file or to a publicly trusted CA and match the hostname. Pinning does not lift a certificate's validity dates. Check the fingerprint (`openssl x509 -in ~/.config/unifi-console.pem -noout -fingerprint -sha256`) against the console's if the network between you is not trusted. If the console later generates a new certificate, save it again.
88
+
89
+ Setting `UNIFI_NETWORK_VERIFY_SSL=false` instead turns off certificate checks for this server's connections to the console: anyone who can intercept that traffic can then read the API key. Only do this on a network you trust.
49
90
 
50
- ### Manual Configuration
91
+ Behind an HTTP(S) proxy, set `NODE_USE_ENV_PROXY=1` along with `HTTPS_PROXY` (and `NO_PROXY` for anything to reach directly), as for Node's own `fetch`; the console's certificate is still verified through the proxy.
51
92
 
52
- Alternatively, add to your `~/.claude.json` under the top-level `"mcpServers"` key:
93
+ ### Choosing an API key
94
+
95
+ The key carries the rights of the admin who created it, and this server is driven by a model that reads data other people control (client hostnames, device names). Create the key as a dedicated admin with the least role that covers your use: for the default read-only mode, a **View Only** admin, so nothing can be changed even if a tool were misused. Use a full-access admin only when you set `UNIFI_NETWORK_READ_ONLY=false`, and then also read [Limiting write mode](#limiting-write-mode).
96
+
97
+ ### Cloud Connector
98
+
99
+ To use a Site Manager API key with a console, set `UNIFI_NETWORK_HOST=api.ui.com` and `UNIFI_NETWORK_CONSOLE_ID` to its console ID. The existing `UNIFI_NETWORK_API_KEY` supplies the key. This works for current Integration API tools and the controller history and classic routes when permitted by the console/key. Cloud Connector requires eligible console ownership/access and firmware 5.0.3 or later. TLS verification stays on: the server refuses to start with `UNIFI_NETWORK_VERIFY_SSL=false` and `api.ui.com`.
100
+
101
+ References: [official Cloud Connector API](https://developer.ui.com/site-manager/v1.0.0) and the [Art-of-WiFi controller client](https://github.com/Art-of-WiFi/UniFi-API-client/blob/master/src/Client.php), whose session queries use epoch seconds.
102
+
103
+ ## Upgrading from 2.x
104
+
105
+ 3.0 tightens several defaults. Most setups need no change; check these:
106
+
107
+ - **Cloud Connector with `UNIFI_NETWORK_VERIFY_SSL=false`**: the server now refuses to start with `UNIFI_NETWORK_HOST=api.ui.com` and verification off. Unset `UNIFI_NETWORK_VERIFY_SSL`.
108
+ - **Writes that open access need `confirm: true`**: creating, enabling or switching a firewall policy or ACL rule to ALLOW, disabling a blocking policy or rule, changing what an enabled policy or rule matches, a reorder that moves an ALLOW ahead of a BLOCK, creating or enabling an open SSID or changing its network, captive portal, client isolation or client filtering, changing an SSID's security type, and creating a large batch of vouchers. Without it the tool returns an error naming the change, and the agent can ask you before re-invoking. A dry run of such a change says why in `confirmationRequired`.
109
+ - **IDs must be IDs**: ID inputs accept only letters, digits, `-` and `_` (pass the IDs that list and get tools return).
110
+ - **Smaller default results**: `unifi_list_client_sessions` and `unifi_list_vouchers` return fewer rows by default, and `unifi_list_client_history` now returns at most `limit` clients, most recently seen first, with `totalCount` and `truncated`; pass `limit` for more. Rows from both history tools carry only the fields their tool descriptions list.
111
+ - **Compact text output**: tool results are compact JSON rather than indented; `structuredContent` is unchanged.
112
+
113
+ - **Writes that could let traffic in from the WAN are refused**: with writes enabled, a firewall policy that allows traffic from the External zone, a change to that zone or to which networks are in it, putting a network in a zone such a policy targets, a reorder of its policies, or an edit to a traffic matching list such a policy uses now fails even with `confirm: true` (see [WAN-exposure refusals](#wan-exposure-refusals)). Set `UNIFI_NETWORK_ALLOW_WAN_EXPOSURE=true` to go back to needing only `confirm`.
114
+ - **Only the endpoints the tools use**: every request is checked against an allowlist before it is sent. No tool behaves differently; anything else is refused.
115
+
116
+ One rule is relaxed: a `dryRun: true` preview of a destructive tool no longer needs `confirm`.
117
+
118
+ ## Safety Features
119
+
120
+ This server provides layered safety controls for responsible operation:
121
+
122
+ - **Tool annotations** — Every tool declares `readOnlyHint`, `destructiveHint` and `openWorldHint`, and every write tool `idempotentHint`, so MCP clients (like Claude Code) can make informed confirmation decisions
123
+ - **Server instructions** — On connect the server gives the MCP host a short usage guide for the model (get a `siteId` first, page with `offset`/`limit`, preview writes with `dryRun`, ask before passing `confirm`), matched to read-only or read-write mode
124
+ - **Read-only mode** — Enabled by default. Only read operations (list, get) are registered. Set `UNIFI_NETWORK_READ_ONLY=false` to enable write/mutating tools
125
+ - **Destructive tool warnings** — Tools that delete or irreversibly modify resources have descriptions prefixed with `DESTRUCTIVE:` to clearly signal risk
126
+ - **Confirmation parameter** — Every tool marked `DESTRUCTIVE:` (including `unifi_remove_device` and `unifi_bulk_delete_vouchers`) requires an explicit `confirm: true` parameter for the call to succeed. A `dryRun: true` preview does not need it, since it changes nothing
127
+ - **Confirmation for changes that open access** — Some routine write tools ask for `confirm: true` only when a particular change can let traffic or guests in: a firewall policy or ACL rule that ALLOWs traffic (created, switched on, or switched to ALLOW), disabling a policy or rule that blocks, changing what an enabled policy or rule matches (widening an ALLOW and narrowing a BLOCK both let more through), a reorder that moves an ALLOW ahead of a BLOCK it used to follow, creating or switching on an open SSID or changing an open SSID's network, captive portal, client isolation or client filtering, changing an SSID's security type, and creating a large batch of vouchers (`unifi_create_voucher`'s `confirm` parameter states the threshold). Each is judged on the resource as the change would leave it, so the update, patch and reorder tools read it first. Without it the call returns an error naming the change; a dry run never needs it, and reports the reason in `confirmationRequired`
128
+ - **Dry-run support** — Every write tool accepts an optional `dryRun: true` parameter that returns a preview of the HTTP request (method, path, body) without making any changes
129
+ - **Safe partial updates** — The UniFi PUT endpoints for WiFi broadcasts, networks, firewall policies, ACL rules and DNS policies replace the whole resource, and some fields reset when omitted (a missing `dhcpGuarding` disables DHCP guarding). Their update tools read the current resource, keep every documented body field, and overlay only the top-level fields you pass, so omitted fields keep their current values. A field you pass replaces the current one whole — nested objects such as `ipv4Configuration`, `securityConfiguration` or a policy's `source` are not deep-merged, so start from what the get tool returns. Pass `null` to clear an optional field. Parallel updates to one resource run one at a time. The dry-run preview shows the merged body (the read still happens)
130
+ - **Secret redaction** — WiFi passphrases (including per-VLAN PPSK keys) are returned as `[REDACTED]` unless the caller passes `includeSecrets: true` to `unifi_list_wifi` / `unifi_get_wifi`. Write-tool echoes and dry-run previews are always redacted, and an update that echoes back `[REDACTED]` keeps the stored passphrase
131
+ - **Request allowlist** — Every request the server makes, reads included, is checked before it is sent against a fixed list of the methods and paths the tools use. Anything else, such as a path with `..` or encoded characters, another application on the console, or a write the server is not set up to make, is refused and never reaches the console
132
+ - **Operator limits for write mode** — `UNIFI_NETWORK_WRITE_TOOLS` loads only the write tools you name, writes that could let traffic in from the WAN are refused outright, and `UNIFI_NETWORK_WRITE_LOG` keeps a record of every write call. See [Limiting write mode](#limiting-write-mode)
133
+
134
+ ## Limiting write mode
135
+
136
+ An agent with write access reads data that other people control: client hostnames, device names, SSIDs, notes on vouchers. Any of it can be written to look like an instruction, and a model can be misled by it. `dryRun` and `confirm` help when the model behaves, but they are inputs the model itself supplies. The settings below are yours: they hold whatever the model is told, because the server enforces them before anything reaches the console.
137
+
138
+ ### Turning writes on
139
+
140
+ 1. **Use a dedicated, least-privilege key.** Create a separate admin for the agent, limited to the Network application and to the sites it should manage, and generate the key as that admin. Do not reuse your own key.
141
+ 2. **Enable writes**: set `UNIFI_NETWORK_READ_ONLY=false`.
142
+ 3. **Load only the write tools the job needs**: set `UNIFI_NETWORK_WRITE_TOOLS`, for example `unifi_create_voucher,unifi_delete_voucher` for a front desk, or `wifi` for SSID changes. Other write tools are not registered, so the agent never sees them, and the request allowlist refuses their requests as well.
143
+ 4. **Keep the WAN-exposure refusals on**: leave `UNIFI_NETWORK_ALLOW_WAN_EXPOSURE` unset.
144
+ 5. **Name your WAN zone**: set `UNIFI_NETWORK_WAN_ZONE_IDS` to the ID of each site's External zone. While the server is still read-only, ask the agent to run `unifi_list_firewall_zones` for each site and copy the `id` of the External zone (system-defined, `metadata.origin` `SYSTEM_DEFINED`). Without the list the server recognises that zone by its name, which can be changed.
145
+ 6. **Turn on the write log and watch it**: set `UNIFI_NETWORK_WRITE_LOG` to an absolute path, for example `/home/you/.local/state/unifi-mcp/writes.jsonl`, and look at it after a session (`tail -f` works).
146
+ 7. **Try changes with `dryRun: true` first**: ask the agent to preview a change, check the request, then let it run.
147
+
148
+ A Claude Desktop entry with all of this set:
53
149
 
54
150
  ```json
55
151
  {
@@ -59,45 +155,76 @@ Alternatively, add to your `~/.claude.json` under the top-level `"mcpServers"` k
59
155
  "args": ["-y", "@owine/unifi-network-mcp@latest"],
60
156
  "env": {
61
157
  "UNIFI_NETWORK_HOST": "192.168.1.1",
62
- "UNIFI_NETWORK_API_KEY": "your-api-key",
63
- "UNIFI_NETWORK_VERIFY_SSL": "false"
158
+ "UNIFI_NETWORK_API_KEY": "key-of-the-agent-admin",
159
+ "UNIFI_NETWORK_CA_CERT": "/home/you/.config/unifi-console.pem",
160
+ "UNIFI_NETWORK_READ_ONLY": "false",
161
+ "UNIFI_NETWORK_WRITE_TOOLS": "unifi_create_voucher,unifi_delete_voucher,wifi",
162
+ "UNIFI_NETWORK_WAN_ZONE_IDS": "6f1c2a52-3b6d-4a8e-9a0b-2d5e8f7c1a90",
163
+ "UNIFI_NETWORK_WRITE_LOG": "/home/you/.local/state/unifi-mcp/writes.jsonl"
64
164
  }
65
165
  }
66
166
  }
67
167
  }
68
168
  ```
69
169
 
70
- ## Safety Features
170
+ ### Write tool limits
71
171
 
72
- This server provides layered safety controls for responsible operation:
172
+ `UNIFI_NETWORK_WRITE_TOOLS` takes write tool names from the [Tools](#tools) tables and group names, which match the tool modules: `devices`, `clients`, `networks`, `wifi`, `hotspot`, `firewall`, `acl`, `dns-policies`, `traffic-matching`. Unset, every write tool loads, as before. `none` or an empty value loads none, leaving a read-only tool set while the request allowlist still refuses writes. An unknown name stops the server with a configuration error listing the valid names. Read tools are never affected. The server instructions tell the model which write tools are available.
73
173
 
74
- - **Tool annotations** — Every tool declares `readOnlyHint`, `destructiveHint`, and `idempotentHint` so MCP clients (like Claude Code) can make informed confirmation decisions
75
- - **Read-only mode** — Enabled by default. Only read operations (list, get) are registered. Set `UNIFI_NETWORK_READ_ONLY=false` to enable write/mutating tools
76
- - **Destructive tool warnings** — Tools that delete or irreversibly modify resources have descriptions prefixed with `DESTRUCTIVE:` to clearly signal risk
77
- - **Confirmation parameter** — Every tool marked `DESTRUCTIVE:` (all 10 of them, including `unifi_remove_device` and `unifi_bulk_delete_vouchers`) requires an explicit `confirm: true` parameter for the call to succeed
78
- - **Dry-run support** — All 33 write tools accept an optional `dryRun: true` parameter that returns a preview of the HTTP request (method, path, body) without making any changes
79
- - **Safe partial updates** — The UniFi PUT endpoints for WiFi broadcasts, networks, firewall policies and DNS policies replace the whole resource, and some fields reset when omitted (a missing `dhcpGuarding` disables DHCP guarding). Their update tools read the current resource, keep every documented body field, and overlay only what you pass, so a one-field change never resets the rest. Pass `null` to clear an optional field. The dry-run preview shows the merged body (the read still happens)
80
- - **Secret redaction** — WiFi passphrases (including per-VLAN PPSK keys) are returned as `[REDACTED]` unless the caller passes `includeSecrets: true` to `unifi_list_wifi` / `unifi_get_wifi`. Write-tool echoes and dry-run previews are always redacted, and an update that echoes back `[REDACTED]` keeps the stored passphrase
174
+ ### WAN-exposure refusals
175
+
176
+ With writes enabled, the server refuses these changes, whatever `confirm` says, and sends nothing:
177
+
178
+ - creating, updating or patching a firewall policy so that it allows traffic (any action but `BLOCK` or `REJECT`) from the WAN zone. A policy whose `connectionStateFilter` lists only `ESTABLISHED` and/or `RELATED` is exempt, since it only lets return traffic back in; a missing or empty filter matches every state, so it is not. An update is judged on the full body after merging, and a patch on the policy as the patch would leave it (a patch that only turns logging or flow reporting on or off, or disables the policy, is not checked)
179
+ - changing the WAN zone (its name or networks), deleting it, or moving networks out of it into another zone
180
+ - creating or updating a network so that it is in the WAN zone, or moving a network out of it
181
+ - putting a network in a zone that a policy allowing traffic from the WAN targets (creating or updating a network with that `zoneId`, or adding networks to that zone), since the network becomes reachable from the WAN without any policy changing. This holds even when the policy's destination `trafficFilter` narrows it to particular hosts, as the server cannot tell whether those hosts are on the network being moved
182
+ - reordering the policies for traffic from the WAN zone, which could put an allow ahead of a block
183
+ - updating a traffic matching list that a policy allowing traffic from the WAN uses, since the list decides what that policy lets in
184
+
185
+ Blocking traffic from the WAN, and allowing traffic from inside to the WAN, are not affected. A dry run runs the same check and reports the refusal. ACL rules are not checked: they are enforced by switches between LAN hosts and have no zone, so they cannot let WAN traffic past the gateway firewall. The Integration API has no port-forwarding endpoint.
186
+
187
+ The Integration API marks zones only as system- or user-defined, and zone names can be changed, so a zone counts as the WAN when its ID is in `UNIFI_NETWORK_WAN_ZONE_IDS`, or when it is system-defined and named `External` (in any case). Set the ID list: a renamed External zone is otherwise missed. To check a change the server lists the site's firewall zones (and, for a matching list or a network put in a zone, its policies). If that fails, or the change names a zone that is not in the list, the write is refused rather than allowed unchecked.
188
+
189
+ A refused call returns:
190
+
191
+ ```text
192
+ Refused: the policy would ALLOW traffic from the External zone, which is the WAN. This server does not make changes that could let traffic in from the WAN, and confirm does not override this; if the change is intended, make it in the UniFi console.
193
+ ```
194
+
195
+ If you do want the agent to make such changes, `UNIFI_NETWORK_ALLOW_WAN_EXPOSURE=true` turns the refusals off; those changes then need `confirm: true` like other changes that open access.
196
+
197
+ ### Write log
198
+
199
+ With `UNIFI_NETWORK_WRITE_LOG` set, each write tool call appends one line, including dry runs and refused calls. Read tools are not logged.
200
+
201
+ ```json
202
+ {"ts":"2026-10-19T00:12:46.170Z","tsLocal":"2026-10-18T20:12:46.170-04:00","tool":"unifi_create_firewall_policy","args":{"siteId":"5f3a9c1e-2b7d-4e8f-9a6b-0c1d2e3f4a5b","policy":{"name":"SSH in","enabled":true,"action":{"type":"ALLOW"},"source":{"zoneId":"6f1c2a52-3b6d-4a8e-9a0b-2d5e8f7c1a90"},"destination":{"zoneId":"0b6d9c1e-58a4-4f43-8e0f-1c2d3e4f5a6b"}},"confirm":true},"method":"POST","path":"/integration/v1/sites/5f3a9c1e-2b7d-4e8f-9a6b-0c1d2e3f4a5b/firewall/policies","dryRun":false,"outcome":"refused","reason":"wan_exposure","durationMs":29}
203
+ ```
204
+
205
+ `outcome` is `ok` (with the HTTP `status`), `dry_run`, `refused` (with a `reason`: `request_guard`, `wan_exposure` or `confirmation_required`) or `error` (with the HTTP `status` when the console answered). `path` is the path under the Network application, without host, console ID or query string; `method` and `path` are `null` when the call stopped before choosing a request. Arguments are redacted: WiFi passphrases (including each `presharedKeys[]` entry's, which keeps its network), and any field named, in any case, `passphrase`, `password`, `secret`, `sharedSecret`, `radiusSecret`, their legacy `x_` forms, `apiKey`, `api_key`, `apiToken`, `token`, `accessToken`, `refreshToken`, `authToken`, `privateKey`, `private_key` or `presharedKey`. The list is in `src/write-log.ts`. The API key and the console's response bodies are never written.
206
+
207
+ The file is created with mode `0600`, and missing parent directories with `0700`. The server refuses to start if the path is a symlink, not a regular file, has other hard links, or is writable by group or others. On Windows, where Node reports every writable file as mode `0666` and cannot open without following symlinks, only the symlink and regular-file checks apply: restrict the file with its ACL (for example, keep it in your user profile). If a line cannot be written (a full disk, say), the tool call still completes and the server reports the problem once on stderr.
81
208
 
82
209
  ## Structured Output
83
210
 
84
- 60 of the 76 tools (all 43 read tools, plus the 17 write tools whose API responses return the affected resource) declare an MCP `outputSchema` and return `structuredContent` alongside the usual text content. Clients that understand structured output get typed, machine-readable results instead of parsing JSON out of a text blob.
211
+ Every read tool, and every write tool whose API response returns the affected resource, declares an MCP `outputSchema` and returns `structuredContent` alongside the usual text content. Clients that understand structured output get typed, machine-readable results instead of parsing JSON out of a text blob. A `dryRun: true` call returns its preview, `{"dryRun": true, "wouldExecute": {"method", "path", "body"}}`, the same way. Other write tools (deletes, device and guest actions, reorders) return the API response as text only.
85
212
 
86
- The schemas live in `src/utils/output-schemas.ts` and are verified against UniFi Network API 11.0.83. They deliberately use a **loose strategy**: every non-key field is optional and nested objects use `.passthrough()`, so firmware- and hardware-specific fields flow through unchanged rather than being stripped or triggering a validation error. This keeps the contract stable across console versions and hardware models.
213
+ The schemas live in `src/utils/output-schemas.ts`. They deliberately use a **loose strategy**: every non-key field is optional and objects accept fields the schema does not list, so firmware- and hardware-specific fields flow through unchanged rather than being stripped or triggering a validation error. This keeps the contract stable across console versions and hardware models.
87
214
 
88
- ## Tools (76 total)
215
+ ## Tools
89
216
 
90
- ### System (1)
217
+ ### System
91
218
  | Tool | Description |
92
219
  |---|---|
93
220
  | `unifi_get_info` | Get UniFi Network application info — returns `applicationVersion` only |
94
221
 
95
- ### Sites (1)
222
+ ### Sites
96
223
  | Tool | Description |
97
224
  |---|---|
98
225
  | `unifi_list_sites` | List all sites available to the API key |
99
226
 
100
- ### Devices (8)
227
+ ### Devices
101
228
  | Tool | Description |
102
229
  |---|---|
103
230
  | `unifi_list_devices` | List all adopted devices at a site |
@@ -109,7 +236,7 @@ The schemas live in `src/utils/output-schemas.ts` and are verified against UniFi
109
236
  | `unifi_restart_device` | Restart a device |
110
237
  | `unifi_power_cycle_port` | Power cycle a specific port (PoE restart) |
111
238
 
112
- ### Clients (4)
239
+ ### Clients
113
240
  | Tool | Description |
114
241
  |---|---|
115
242
  | `unifi_list_clients` | List all connected clients (wired, wireless, VPN) at a site |
@@ -117,13 +244,27 @@ The schemas live in `src/utils/output-schemas.ts` and are verified against UniFi
117
244
  | `unifi_authorize_guest` | Authorize a guest client on a hotspot network |
118
245
  | `unifi_unauthorize_guest` | Unauthorize a guest client |
119
246
 
120
- ### Client history (2)
247
+ ### Client history
121
248
  | Tool | Description |
122
249
  |---|---|
123
250
  | `unifi_list_client_history` | Historical/offline client inventory and first/last seen times |
124
251
  | `unifi_list_client_sessions` | Retained connection sessions, duration, byte counters and roaming records |
125
252
 
126
- ### Networks (6)
253
+ ### Classic controller API
254
+ | Tool | Description |
255
+ |---|---|
256
+ | `unifi_list_classic_networks` | Networks as the classic controller stores them, with their classic `_id`s (credentials never returned) |
257
+ | `unifi_list_firewall_groups` | Address and port groups that legacy firewall rules reference |
258
+ | `unifi_list_legacy_firewall_rules` | Legacy (pre-zone-based) firewall rules — may not be enforced on zone-based firewall sites |
259
+
260
+ ### Internet exposure
261
+ | Tool | Description |
262
+ |---|---|
263
+ | `unifi_list_port_forwards` | Port forwards: WAN port, allowed source and LAN target, enabled or not |
264
+ | `unifi_get_upnp_status` | Whether UPnP and NAT-PMP let LAN devices open WAN ports themselves |
265
+ | `unifi_get_device_ssh_settings` | Device SSH: on or off, password logins, listening on every interface, number of keys |
266
+
267
+ ### Networks
127
268
  | Tool | Description |
128
269
  |---|---|
129
270
  | `unifi_list_networks` | List all networks at a site |
@@ -133,7 +274,7 @@ The schemas live in `src/utils/output-schemas.ts` and are verified against UniFi
133
274
  | `unifi_update_network` | Update an existing network — partial changes are merged into the full PUT body |
134
275
  | `unifi_delete_network` | **DESTRUCTIVE:** Delete a network — disconnects all clients |
135
276
 
136
- ### WiFi (5)
277
+ ### WiFi
137
278
  | Tool | Description |
138
279
  |---|---|
139
280
  | `unifi_list_wifi` | List all WiFi broadcasts (SSIDs) at a site (passphrases redacted by default) |
@@ -142,7 +283,7 @@ The schemas live in `src/utils/output-schemas.ts` and are verified against UniFi
142
283
  | `unifi_update_wifi` | Update an existing WiFi network — partial changes are merged into the full PUT body |
143
284
  | `unifi_delete_wifi` | **DESTRUCTIVE:** Delete a WiFi network — disconnects all clients |
144
285
 
145
- ### Hotspot Vouchers (5)
286
+ ### Hotspot Vouchers
146
287
  | Tool | Description |
147
288
  |---|---|
148
289
  | `unifi_list_vouchers` | List all hotspot vouchers at a site |
@@ -151,13 +292,13 @@ The schemas live in `src/utils/output-schemas.ts` and are verified against UniFi
151
292
  | `unifi_delete_voucher` | **DESTRUCTIVE:** Delete a hotspot voucher |
152
293
  | `unifi_bulk_delete_vouchers` | **DESTRUCTIVE:** Bulk delete vouchers matching a filter |
153
294
 
154
- ### Firewall Zones & Policies (13)
295
+ ### Firewall Zones & Policies
155
296
  | Tool | Description |
156
297
  |---|---|
157
298
  | `unifi_list_firewall_zones` | List all firewall zones at a site |
158
299
  | `unifi_get_firewall_zone` | Get a specific firewall zone by ID |
159
300
  | `unifi_create_firewall_zone` | Create a new custom firewall zone |
160
- | `unifi_update_firewall_zone` | Update a firewall zone |
301
+ | `unifi_update_firewall_zone` | Replace a firewall zone's name and network list (send every network ID to keep) |
161
302
  | `unifi_delete_firewall_zone` | **DESTRUCTIVE:** Delete a custom firewall zone |
162
303
  | `unifi_list_firewall_policies` | List all firewall policies at a site |
163
304
  | `unifi_get_firewall_policy` | Get a specific firewall policy by ID |
@@ -168,18 +309,18 @@ The schemas live in `src/utils/output-schemas.ts` and are verified against UniFi
168
309
  | `unifi_get_firewall_policy_ordering` | Get user-defined firewall policy ordering for a zone pair |
169
310
  | `unifi_reorder_firewall_policies` | Reorder user-defined firewall policies for a zone pair |
170
311
 
171
- ### ACL Rules (7)
312
+ ### ACL Rules
172
313
  | Tool | Description |
173
314
  |---|---|
174
315
  | `unifi_list_acl_rules` | List all ACL rules at a site |
175
316
  | `unifi_get_acl_rule` | Get a specific ACL rule by ID |
176
317
  | `unifi_get_acl_rule_ordering` | Get user-defined ACL rule ordering |
177
318
  | `unifi_create_acl_rule` | Create a new ACL rule |
178
- | `unifi_update_acl_rule` | Update an ACL rule |
319
+ | `unifi_update_acl_rule` | Update an ACL rule — partial changes are merged into the full PUT body |
179
320
  | `unifi_delete_acl_rule` | **DESTRUCTIVE:** Delete an ACL rule |
180
321
  | `unifi_reorder_acl_rules` | Reorder user-defined ACL rules |
181
322
 
182
- ### Switching (6)
323
+ ### Switching
183
324
  | Tool | Description |
184
325
  |---|---|
185
326
  | `unifi_list_switch_stacks` | List all Switch Stacks at a site |
@@ -189,7 +330,7 @@ The schemas live in `src/utils/output-schemas.ts` and are verified against UniFi
189
330
  | `unifi_list_lags` | List all LAGs (Link Aggregation Groups) at a site |
190
331
  | `unifi_get_lag` | Get details of a specific LAG |
191
332
 
192
- ### DNS Policies (5)
333
+ ### DNS Policies
193
334
  | Tool | Description |
194
335
  |---|---|
195
336
  | `unifi_list_dns_policies` | List all DNS policies at a site |
@@ -198,16 +339,16 @@ The schemas live in `src/utils/output-schemas.ts` and are verified against UniFi
198
339
  | `unifi_update_dns_policy` | Update a DNS policy — partial changes are merged into the full PUT body |
199
340
  | `unifi_delete_dns_policy` | **DESTRUCTIVE:** Delete a DNS policy |
200
341
 
201
- ### Traffic Matching (5)
342
+ ### Traffic Matching
202
343
  | Tool | Description |
203
344
  |---|---|
204
345
  | `unifi_list_traffic_matching_lists` | List all traffic matching lists (port groups, IP groups) |
205
346
  | `unifi_get_traffic_matching_list` | Get a specific traffic matching list by ID |
206
347
  | `unifi_create_traffic_matching_list` | Create a new traffic matching list |
207
- | `unifi_update_traffic_matching_list` | Update a traffic matching list |
348
+ | `unifi_update_traffic_matching_list` | Replace a traffic matching list (send the full items array) |
208
349
  | `unifi_delete_traffic_matching_list` | **DESTRUCTIVE:** Delete a traffic matching list |
209
350
 
210
- ### Supporting (8)
351
+ ### Supporting
211
352
  | Tool | Description |
212
353
  |---|---|
213
354
  | `unifi_list_wans` | List all WAN interfaces at a site |
@@ -219,57 +360,96 @@ The schemas live in `src/utils/output-schemas.ts` and are verified against UniFi
219
360
  | `unifi_list_dpi_applications` | List all DPI applications for traffic identification |
220
361
  | `unifi_list_countries` | List all countries/regions for geo-based rules |
221
362
 
222
- ## Development
363
+ ## Retained client connections
223
364
 
224
- ```bash
225
- pnpm install # Install dependencies
226
- pnpm run build # Compile TypeScript
227
- pnpm start # Run the server
228
- pnpm run typecheck # Type-check without emitting
229
- pnpm run lint # ESLint
230
- pnpm run lint:fix # ESLint with auto-fix
231
- pnpm test # Run all tests (vitest)
232
- pnpm run test:watch # Run tests in watch mode
233
- pnpm run test:coverage # Run tests with coverage
365
+ The Integration API only lists connected clients. The two history tools use the controller APIs instead: GET `/proxy/network/v2/api/site/{siteReference}/clients/history` and POST `/proxy/network/api/s/{siteReference}/stat/session`. The POST is a statistics query and does not change the network. Both tools remain available in read-only mode.
366
+
367
+ Use `internalReference` from `unifi_list_sites` (often `default`), not its UUID. Session queries require `start` and `end` in Unix **seconds**, with a maximum 31-day window. The controller ignores `_start`/offset on this endpoint. A full result sets `mayBeTruncated=true`; split the time window or filter by MAC and query again. Overlap split boundaries and deduplicate session IDs. A result below the requested limit means the query was not capped, not that all dates were retained.
368
+
369
+ ```json
370
+ {"siteReference":"default","start":1789272000,"end":1789876800}
234
371
  ```
235
372
 
236
- ### Commit conventions
373
+ `unifi_list_client_history` lists offline clients only, most recently seen first, up to `limit`, with `totalCount`. `truncated: true` means less recently seen clients were left out; raise `limit` (narrowing `withinHours` drops those same clients).
237
374
 
238
- This project uses [conventional commits](https://www.conventionalcommits.org/) and [release-please](https://github.com/googleapis/release-please) for automated releases:
375
+ Rows keep the controller's field names (`assoc_time`, `duration`, `rx_bytes`, `tx_bytes`, `roaming_sessions`, …) but carry only the fields each tool's description lists. RX/TX are the controller's counters, not relabeled client download/upload. Inventory is separate from session history. Retention varies; a non-truncated result does not prove all requested dates were retained, and ongoing sessions may not be present. Neither association nor traffic counters establish end-to-end Internet success.
239
376
 
240
- - `feat: ...` — new feature (minor version bump)
241
- - `fix: ...` — bug fix (patch version bump)
242
- - `feat!: ...` or `BREAKING CHANGE:` footer — breaking change (major version bump)
243
- - `chore:`, `docs:`, `ci:`, etc. — no version bump
377
+ These controller routes are not part of the published Integration API contract. Their availability and API-key permissions vary by controller version. Unsupported, unauthorized, HTML login, legacy `meta.rc` error and malformed responses fail explicitly instead of becoming an empty history. History requests are time-limited, reject redirects, and refuse oversized responses. No browser cookies are required where API-key access is supported.
244
378
 
245
- On push to `main`, release-please opens a Release PR that bumps the version and updates `CHANGELOG.md`. Merging that PR publishes to npm automatically.
379
+ ## Classic controller reads
246
380
 
247
- To override the version number, add `Release-As: x.x.x` in the commit body:
381
+ Legacy firewall rules, and the firewall groups and networks they reference, are not in the Integration API. Three read tools list them from the classic controller API instead: GET `/proxy/network/api/s/{siteReference}/rest/networkconf`, `/rest/firewallgroup` and `/rest/firewallrule`. Like the history tools they take the site's `internalReference` from `unifi_list_sites`, and remain available in read-only mode. No classic write is allowlisted.
248
382
 
249
- ```bash
250
- git commit --allow-empty -m "chore: release 2.0.0" -m "Release-As: 2.0.0"
251
- ```
383
+ Each collection is returned whole; the controller does not page it. Legacy rules are sorted by ruleset, then `rule_index`, the order they are evaluated in. Records keep the controller's field names and their classic `_id`s, which differ from Integration API IDs, so an ID from one API cannot be passed to a tool of the other. Legacy rules reference networks by `unifi_list_classic_networks` IDs and groups by `unifi_list_firewall_groups` IDs. On a site that uses the zone-based firewall, legacy rules may not be enforced; its rules are the firewall policies.
252
384
 
253
- ## License
385
+ Classic network records hold WAN, VPN and RADIUS credentials. Rows are therefore built from a fixed list of fields per tool (its output schema, in `src/utils/output-schemas.ts`), never by redacting the record, so a credential under any name is never returned. Errors are reported the same way as for the history routes.
254
386
 
255
- MIT
387
+ ## Internet exposure
256
388
 
257
- ## Retained client connections
389
+ Port forwards, UPnP and device SSH settings are not in the Integration API, so the exposure tools read the classic controller API: GET `/proxy/network/api/s/{siteReference}/rest/portforward` and `/proxy/network/api/s/{siteReference}/get/setting`, directly or through the Cloud Connector. Like the history tools they take the site's `internalReference` from `unifi_list_sites` and remain available in read-only mode. No classic write is allowlisted.
258
390
 
259
- The Integration API only lists connected clients. The two history tools use the controller APIs instead: GET `/proxy/network/v2/api/site/{siteReference}/clients/history` and POST `/proxy/network/api/s/{siteReference}/stat/session`. The POST is a statistics query and does not change the network. Both tools remain available in read-only mode.
391
+ Settings sections hold credentials (SSH passwords, management and cloud-access keys, RADIUS secrets), so every result is built from a fixed list of fields (its output schema, in `src/utils/output-schemas.ts`), never by redacting the record. SSH keys are counted, not returned.
260
392
 
261
- Use `internalReference` from `unifi_list_sites` (often `default`), not its UUID. Session queries require `start` and `end` in Unix **seconds**, with a maximum 31-day window. The controller ignores `_start`/offset on this endpoint. A full result sets `mayBeTruncated=true`; split the time window or filter by MAC and query again. Overlap split boundaries and deduplicate session IDs. A result below the requested limit means the query was not capped, not that all dates were retained.
393
+ A setting this controller version does not have comes back as `status: "unavailable"` with a `detail` saying what was looked for. It never means the feature is off. UPnP is read from a `upnp` settings section where there is one, otherwise from the gateway (`usg`) section's `upnp_*` fields, and `source` says which; Network 11.0.94 has no `upnp` section. `upnp_nat_pmp_enabled` is the stored setting, which does nothing while UPnP is off, and `detail` says so when the two disagree.
262
394
 
263
- ```json
264
- {"siteReference":"default","start":1789272000,"end":1789876800,"limit":1000}
395
+ Remote Access and console (UniFi OS) SSH are UniFi OS settings outside this API, so `unifi_get_device_ssh_settings` reports device SSH only. Which internal networks can reach the console's web UI is decided by the firewall rules or policies.
396
+
397
+ ## Troubleshooting
398
+
399
+ Configuration errors go to the MCP host's server log and the server exits. Tool errors reach the agent as results starting with `Error:`.
400
+
401
+ | Message | Cause and fix |
402
+ |---|---|
403
+ | `Configuration error:` naming `host` or `apiKey` | `UNIFI_NETWORK_HOST` or `UNIFI_NETWORK_API_KEY` is missing from the server's `env`. |
404
+ | `cannot read UNIFI_NETWORK_CA_CERT (…)` | The path does not exist for the server. Use an absolute path; `~` and `$HOME` are not expanded in an MCP config. |
405
+ | `UNIFI_NETWORK_CA_CERT must name a PEM file containing a certificate` | The file has no `BEGIN CERTIFICATE` block; re-run the `openssl` command above. |
406
+ | `…cannot be combined with UNIFI_NETWORK_VERIFY_SSL=false` or `TLS verification cannot be disabled for api.ui.com` | Unset `UNIFI_NETWORK_VERIFY_SSL`. |
407
+ | `Ignoring UNIFI_NETWORK_CONSOLE_ID` (log line) | The console ID only applies with `UNIFI_NETWORK_HOST=api.ui.com`. |
408
+ | `fetch failed (DEPTH_ZERO_SELF_SIGNED_CERT)` or another certificate code | The console's certificate did not verify. Save it to `UNIFI_NETWORK_CA_CERT`; if you already did, the console has a new certificate, so save it again. |
409
+ | `fetch failed (ECONNREFUSED)`, `(ENOTFOUND)` or a connect timeout | Wrong or unreachable host. `UNIFI_NETWORK_HOST` is a bare IP or hostname, without `https://`. |
410
+ | `HTTP 401` / `HTTP 403` | The key is wrong or revoked, or its admin lacks the rights (a View Only key cannot write). |
411
+ | `HTTP 404` | A wrong `siteId` or resource ID, or an endpoint this Network version lacks (check `unifi_get_info`). |
412
+ | `UniFi controller did not respond within …` | The console is slow. For a write, check whether it was applied before retrying. |
413
+ | `This action requires explicit confirmation: …` | Expected: the change needs `confirm: true`, and the agent should ask you first. |
414
+ | `Use an ID returned by a list or get tool` | The agent passed a name or malformed ID. |
415
+ | `Request refused by the request guard: …` | The server would only send a request on its allowlist; this one was not, or belongs to a write tool `UNIFI_NETWORK_WRITE_TOOLS` does not enable. Nothing was sent. If a tool you did not modify hits this, please open an issue. |
416
+ | `Refused: … which is the WAN` | Expected: the change could let traffic in from the WAN, and `confirm` does not override it. Make it in the UniFi console (see [WAN-exposure refusals](#wan-exposure-refusals)). |
417
+ | `Refused: the firewall zones could not be read …` or `… is not one of this site's firewall zones` | The WAN check could not run, so the write was refused. Check the key can read the site's firewall zones, and that the zone ID is right. |
418
+ | `UNIFI_NETWORK_WRITE_TOOLS names unknown write tools or groups: …` | A typo or a read tool's name. The message lists the valid names. |
419
+ | A write tool is missing, or `Tool … not found` | `UNIFI_NETWORK_WRITE_TOOLS` does not name it or its group. |
420
+ | `cannot use UNIFI_NETWORK_WRITE_LOG (…)` | The path is a symlink, not a regular file, hard-linked elsewhere, or writable by others (`chmod 600` it), or its directory cannot be created. |
421
+ | No write tools listed | Read-only is the default. Set `UNIFI_NETWORK_READ_ONLY=false` (only `false` counts; any other value stays read-only) and restart the host. If it is set, check `UNIFI_NETWORK_WRITE_TOOLS` is not `none` or empty. |
422
+ | `Controller history returned HTTP …`, `non-JSON data` or `an empty body` | This console or key cannot use the history routes (see [Retained client connections](#retained-client-connections)). It is not an empty history. |
423
+ | `Classic controller API returned HTTP …`, `non-JSON data` or `malformed JSON` | This console or key cannot read the classic controller API (see [Classic controller reads](#classic-controller-reads)). It is not an empty result. |
424
+ | `Use the site's internalReference …` | History, classic-API and exposure tools take `internalReference` (often `default`) from `unifi_list_sites`, not the UUID. |
425
+
426
+ ## Development
427
+
428
+ ```bash
429
+ pnpm install # Install dependencies
430
+ pnpm run build # Compile TypeScript
431
+ pnpm start # Run the server
432
+ pnpm run typecheck # Type-check src/ and tests
433
+ pnpm run lint # ESLint
434
+ pnpm run lint:fix # ESLint with auto-fix
435
+ pnpm test # Run all tests (vitest)
436
+ pnpm run test:watch # Run tests in watch mode
437
+ pnpm run test:coverage # Run tests with coverage
438
+ pnpm run test:package # Build, pack and smoke-test the published package
265
439
  ```
266
440
 
267
- Results preserve controller field names, including `assoc_time`, `duration`, `rx_bytes`, `tx_bytes` and `roaming_sessions`. RX/TX are the controller's counters, not relabeled client download/upload. Inventory is separate from session history. Retention varies; a non-truncated result does not prove all requested dates were retained, and ongoing sessions may not be present. Neither association nor traffic counters establish end-to-end Internet success. The `is_guest` flag does not establish device ownership.
441
+ ### Commit conventions
268
442
 
269
- These controller routes are not part of the published Integration API contract. Their availability and API-key permissions vary by controller version. Unsupported, unauthorized, HTML login, legacy `meta.rc` error and malformed responses fail explicitly instead of becoming an empty history. History requests have a 25-second timeout, reject redirects, and cap responses at 10 MB. No browser cookies are required where API-key access is supported.
443
+ This project uses [conventional commits](https://www.conventionalcommits.org/) in pull request titles and [release-drafter](https://github.com/release-drafter/release-drafter) for releases. Pull requests are squash-merged, so the title is the commit subject:
270
444
 
271
- ### Cloud Connector
445
+ - `feat: ...` — new feature (minor version bump)
446
+ - `fix: ...` — bug fix (patch version bump)
447
+ - `deps: ...` — dependency update (patch version bump)
448
+ - `feat!: ...` — breaking change (major version bump); the `!` must be in the title, a `BREAKING CHANGE:` footer is not read
449
+ - `chore:`, `docs:`, `ci:`, etc. — no version bump
272
450
 
273
- To use a Site Manager API key with a console, set `UNIFI_NETWORK_HOST=api.ui.com` and `UNIFI_NETWORK_CONSOLE_ID` to its console ID. The existing `UNIFI_NETWORK_API_KEY` supplies the key. This works for current Integration API tools and the controller history routes when permitted by the console/key. Cloud Connector requires eligible console ownership/access and firmware 5.0.3 or later. Keep TLS verification enabled.
451
+ Every push to `main` updates a draft GitHub Release with the next version and its notes. A maintainer publishes the draft, which builds, tests and publishes that tagged commit to npm with provenance. To release a different version, edit the draft's tag and title before publishing. Release notes after 3.0.0 are in [GitHub Releases](https://github.com/owine/unifi-network-mcp/releases); `CHANGELOG.md` covers 3.0.0 and earlier.
274
452
 
275
- References: [official Cloud Connector API](https://developer.ui.com/site-manager/v1.0.0) and the [Art-of-WiFi controller client](https://github.com/Art-of-WiFi/UniFi-API-client/blob/master/src/Client.php), whose session queries use epoch seconds.
453
+ ## License
454
+
455
+ MIT — see [LICENSE](LICENSE).
package/dist/client.d.ts CHANGED
@@ -1,10 +1,24 @@
1
1
  import { Config } from "./config.js";
2
+ /** Run fn so that every request it makes is aborted when signal is. */
3
+ export declare function withCallSignal<T>(signal: AbortSignal | undefined, fn: () => T): T;
4
+ /** The classic controller REST collections the classic read tools list. */
5
+ export declare const CLASSIC_COLLECTIONS: readonly ["networkconf", "firewallgroup", "firewallrule", "portforward"];
6
+ export type ClassicCollection = (typeof CLASSIC_COLLECTIONS)[number];
2
7
  export declare class NetworkClient {
3
- private baseUrl;
4
8
  private headers;
5
9
  private networkUrl;
10
+ private dispatcher;
11
+ private policy;
6
12
  constructor(config: Config);
13
+ /**
14
+ * path is relative to the Integration API, as the tools pass it. The guard
15
+ * sees it as the canonical path under the Network application; the host and
16
+ * any Cloud Connector prefix are added only once it has passed.
17
+ */
7
18
  private request;
19
+ private send;
20
+ /** Check a request against the guard before anything is sent, and note writes for the write log. */
21
+ private guard;
8
22
  get(path: string): Promise<unknown>;
9
23
  post(path: string, body?: unknown): Promise<unknown>;
10
24
  put(path: string, body: unknown): Promise<unknown>;
@@ -18,6 +32,17 @@ export declare class NetworkClient {
18
32
  limit: number;
19
33
  mac?: string;
20
34
  }): Promise<unknown>;
35
+ /** A classic controller REST collection, read whole: the controller does not page these. */
36
+ getClassic(siteReference: string, collection: ClassicCollection): Promise<unknown>;
37
+ /**
38
+ * The site's classic settings (get/setting): one record per section, keyed
39
+ * by its key field. Sections hold credentials (SSH passwords, RADIUS and
40
+ * cloud-access keys), so callers copy out only the fields they need.
41
+ */
42
+ getClassicSettings(siteReference: string): Promise<unknown>;
21
43
  private validateSiteReference;
22
44
  private historyRequest;
45
+ /** The classic controller API, outside /integration/v1. Only GETs of the collections the guard allows. */
46
+ private classicRequest;
47
+ private readController;
23
48
  }