@owine/unifi-network-mcp 2.11.4 → 2.13.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.
package/README.md CHANGED
@@ -81,7 +81,7 @@ This server provides layered safety controls for responsible operation:
81
81
 
82
82
  58 of the 74 tools (all 41 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.
83
83
 
84
- The schemas live in `src/utils/output-schemas.ts` and are verified against UniFi Network API 10.5.43. 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.
84
+ The schemas live in `src/utils/output-schemas.ts` and are verified against UniFi Network API 10.6.106. 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.
85
85
 
86
86
  ## Tools (74 total)
87
87
 
package/dist/tools/acl.js CHANGED
@@ -8,7 +8,7 @@ const safety_js_1 = require("../utils/safety.js");
8
8
  const output_schemas_js_1 = require("../utils/output-schemas.js");
9
9
  function registerAclTools(server, client, readOnly = false) {
10
10
  server.registerTool("unifi_list_acl_rules", {
11
- description: "List ACL rules (switch/AP-level access control lists, distinct from zone-based firewall policies) at a site. Returns: id, type (IPV4/MAC), name, enabled, action (ALLOW/BLOCK), description, protocolFilter, source/destination matchers. ACLs apply earlier in the path than firewall policies.",
11
+ description: "List ACL rules (switch/AP-level access control lists, distinct from zone-based firewall policies) at a site. Returns: id, type (IPV4/MAC), name, enabled, action (ALLOW/BLOCK), description, protocolFilter, sourceFilter/destinationFilter matchers, enforcingDeviceFilter (which devices apply the rule), index (evaluation order — read-only; change it with unifi_reorder_acl_rules), metadata.origin. ACLs apply earlier in the path than firewall policies.",
12
12
  inputSchema: {
13
13
  siteId: zod_1.z.string().describe("Site ID"),
14
14
  offset: zod_1.z
@@ -20,7 +20,7 @@ function registerAclTools(server, client, readOnly = false) {
20
20
  limit: zod_1.z
21
21
  .number()
22
22
  .int()
23
- .min(1)
23
+ .min(0)
24
24
  .max(200)
25
25
  .optional()
26
26
  .describe("Number of records to return (default: 25, max: 200)"),
@@ -8,7 +8,7 @@ const safety_js_1 = require("../utils/safety.js");
8
8
  const output_schemas_js_1 = require("../utils/output-schemas.js");
9
9
  function registerClientTools(server, client, readOnly = false) {
10
10
  server.registerTool("unifi_list_clients", {
11
- description: "List currently connected clients at a site. Returns per client: id, name, type (WIRED/WIRELESS/VPN/TELEPORT), macAddress, ipAddress, connectedAt, uplinkDeviceId (the switch/AP they're attached to), access.type. NOTE: verified against 10.5.43 — the Integration API client schema is minimal and identical across types; it does NOT expose signal strength, channel, or per-port binding. Use for: who's online right now. Disconnected/historical clients are NOT in the Integration API.",
11
+ description: "List currently connected clients at a site. Returns per client: id, name, type (WIRED/WIRELESS/VPN/TELEPORT), macAddress, ipAddress (frequently absent — do not rely on it being present), connectedAt, uplinkDeviceId (the switch/AP they're attached to), access.type. NOTE: verified against 10.6.106 — the Integration API client schema is minimal and identical across types; it does NOT expose signal strength, channel, or per-port binding. Use for: who's online right now. Disconnected/historical clients are NOT in the Integration API.",
12
12
  inputSchema: {
13
13
  siteId: zod_1.z.string().describe("Site ID"),
14
14
  offset: zod_1.z
@@ -20,7 +20,7 @@ function registerClientTools(server, client, readOnly = false) {
20
20
  limit: zod_1.z
21
21
  .number()
22
22
  .int()
23
- .min(1)
23
+ .min(0)
24
24
  .max(200)
25
25
  .optional()
26
26
  .describe("Number of records to return (default: 25, max: 200)"),
@@ -58,7 +58,7 @@ function registerClientTools(server, client, readOnly = false) {
58
58
  if (readOnly)
59
59
  return;
60
60
  server.registerTool("unifi_authorize_guest", {
61
- description: "Authorize a guest client through a captive-portal hotspot. Optional limits override hotspot defaults. Idempotency: not safe to retry — re-authorizing extends the session.",
61
+ description: "Authorize a guest client through a captive-portal hotspot. Optional limits override hotspot defaults. Returns (per 10.6.106 docs, not live-verified): action, grantedAuthorization and revokedAuthorization objects (authorizedAt, authorizationMethod, expiresAt, dataUsageLimitMBytes, rxRateLimitKbps, txRateLimitKbps, usage). Idempotency: not safe to retry — re-authorizing extends the session.",
62
62
  inputSchema: {
63
63
  siteId: zod_1.z.string().describe("Site ID"),
64
64
  clientId: zod_1.z.string().describe("Client ID"),
@@ -116,7 +116,7 @@ function registerClientTools(server, client, readOnly = false) {
116
116
  }
117
117
  });
118
118
  server.registerTool("unifi_unauthorize_guest", {
119
- description: "Revoke guest authorization; the client returns to the captive portal on next request. Idempotent.",
119
+ description: "Revoke guest authorization; the client returns to the captive portal on next request. Returns (per 10.6.106 docs, not live-verified): action plus a revokedAuthorization object. Idempotent.",
120
120
  inputSchema: {
121
121
  siteId: zod_1.z.string().describe("Site ID"),
122
122
  clientId: zod_1.z.string().describe("Client ID"),
@@ -22,7 +22,7 @@ function registerDeviceTools(server, client, readOnly = false) {
22
22
  limit: zod_1.z
23
23
  .number()
24
24
  .int()
25
- .min(1)
25
+ .min(0)
26
26
  .max(200)
27
27
  .optional()
28
28
  .describe("Number of records to return (default: 25, max: 200)"),
@@ -41,7 +41,7 @@ function registerDeviceTools(server, client, readOnly = false) {
41
41
  }
42
42
  });
43
43
  server.registerTool("unifi_get_device", {
44
- description: "Get full configuration for a device. Returns (in addition to list fields): supported, firmwareUpdatable, provisionedAt, configurationId, uplink.deviceId, features (object keyed by capability: switching {lags[]} / accessPoint {}), interfaces.ports[] for switches ({idx, state, connector, maxSpeedMbps, speedMbps, poe:{standard, type, enabled, state}}), interfaces.radios[] for APs ({wlanStandard, frequencyGHz, channelWidthMHz, channel}). NOTE: in the LIST endpoint, features/interfaces are capability-tag string arrays instead. Use for: switch port layout/PoE state, AP radio config, uplink topology. For live throughput/CPU/memory, use unifi_get_device_statistics.",
44
+ description: "Get full configuration for a device. Returns (in addition to list fields): supported, firmwareUpdatable, adoptedAt, provisionedAt, configurationId, uplink.deviceId, features (object keyed by capability: switching {lags[]} / accessPoint {}), interfaces.ports[] for switches ({idx, state, connector, maxSpeedMbps, speedMbps, poe:{standard, type, enabled, state}}), interfaces.radios[] for APs ({wlanStandard, frequencyGHz, channelWidthMHz, channel}). NOTE: in the LIST endpoint, features/interfaces are capability-tag string arrays instead. Use for: switch port layout/PoE state, AP radio config, uplink topology. For live throughput/CPU/memory, use unifi_get_device_statistics.",
45
45
  inputSchema: {
46
46
  siteId: zod_1.z.string().describe("Site ID"),
47
47
  deviceId: zod_1.z.string().describe("Device ID"),
@@ -58,7 +58,7 @@ function registerDeviceTools(server, client, readOnly = false) {
58
58
  }
59
59
  });
60
60
  server.registerTool("unifi_get_device_statistics", {
61
- description: "Get latest live statistics for a device. Returns: uptimeSec, lastHeartbeatAt, nextHeartbeatAt, loadAverage1/5/15Min, cpuUtilizationPct, memoryUtilizationPct, uplink (txRateBps, rxRateBps), interfaces.radios[] for APs ({frequencyGHz, txRetriesPct}). NOTE: verified against 10.5.43 — the Integration API does NOT expose per-switch-port byte/error/PoE-power counters here; port-level live stats are unavailable. Use for: device health and AP radio metrics. For config (channel, power, port assignment), use unifi_get_device.",
61
+ description: "Get latest live statistics for a device. Returns: uptimeSec, lastHeartbeatAt, nextHeartbeatAt, loadAverage1/5/15Min, cpuUtilizationPct, memoryUtilizationPct, uplink (txRateBps, rxRateBps), interfaces.radios[] for APs ({frequencyGHz, txRetriesPct}). NOTE: verified against 10.6.106 — the Integration API does NOT expose per-switch-port byte/error/PoE-power counters here; port-level live stats are unavailable. Use for: device health and AP radio metrics. For config (channel, power, port assignment), use unifi_get_device.",
62
62
  inputSchema: {
63
63
  siteId: zod_1.z.string().describe("Site ID"),
64
64
  deviceId: zod_1.z.string().describe("Device ID"),
@@ -75,7 +75,7 @@ function registerDeviceTools(server, client, readOnly = false) {
75
75
  }
76
76
  });
77
77
  server.registerTool("unifi_list_pending_devices", {
78
- description: "List devices pending adoption across all sites (global endpoint, not site-scoped). Returns: basic device info per pending device (macAddress, model, ipAddress, firmwareVersion, etc. — exact per-row schema is not rendered in the 10.5.43 docs). Use for: discovering new devices on the network before calling unifi_adopt_device.",
78
+ description: "List devices pending adoption across all sites (global endpoint, not site-scoped). Returns: basic device info per pending device (macAddress, model, ipAddress, firmwareVersion, etc. — exact per-row schema is not rendered in the 10.6.106 docs). Use for: discovering new devices on the network before calling unifi_adopt_device.",
79
79
  inputSchema: {
80
80
  offset: zod_1.z
81
81
  .number()
@@ -86,7 +86,7 @@ function registerDeviceTools(server, client, readOnly = false) {
86
86
  limit: zod_1.z
87
87
  .number()
88
88
  .int()
89
- .min(1)
89
+ .min(0)
90
90
  .max(200)
91
91
  .optional()
92
92
  .describe("Number of records to return (default: 25, max: 200)"),
@@ -111,7 +111,9 @@ function registerDeviceTools(server, client, readOnly = false) {
111
111
  inputSchema: {
112
112
  siteId: zod_1.z.string().describe("Site ID"),
113
113
  macAddress: zod_1.z.string().describe("MAC address of the device"),
114
- ignoreDeviceLimit: zod_1.z.boolean().optional().describe("Ignore device limit when adopting (default: false)"),
114
+ // Required by the API; kept optional here and defaulted to false below
115
+ // so callers need not pass it explicitly.
116
+ ignoreDeviceLimit: zod_1.z.boolean().optional().describe("Ignore device limit when adopting (required by the API; sent as false when omitted)"),
115
117
  dryRun: zod_1.z.boolean().optional().describe("Preview this action without executing it"),
116
118
  },
117
119
  outputSchema: adoptDeviceOutputSchema,
@@ -8,7 +8,7 @@ const safety_js_1 = require("../utils/safety.js");
8
8
  const output_schemas_js_1 = require("../utils/output-schemas.js");
9
9
  function registerDnsPolicyTools(server, client, readOnly = false) {
10
10
  server.registerTool("unifi_list_dns_policies", {
11
- description: "List DNS policies (local DNS records and forward rules served by the gateway) at a site. Returns: id, type (A_RECORD, AAAA_RECORD, CNAME_RECORD, MX_RECORD, TXT_RECORD, SRV_RECORD, FORWARD_DOMAIN), enabled, domain, ipv4Address, ttlSeconds.",
11
+ description: "List DNS policies (local DNS records and forward rules served by the gateway) at a site. Returns: id, type (A_RECORD, AAAA_RECORD, CNAME_RECORD, MX_RECORD, TXT_RECORD, SRV_RECORD, FORWARD_DOMAIN), enabled, domain, ipv4Address, ttlSeconds, metadata.origin.",
12
12
  inputSchema: {
13
13
  siteId: zod_1.z.string().describe("Site ID"),
14
14
  offset: zod_1.z
@@ -20,7 +20,7 @@ function registerDnsPolicyTools(server, client, readOnly = false) {
20
20
  limit: zod_1.z
21
21
  .number()
22
22
  .int()
23
- .min(1)
23
+ .min(0)
24
24
  .max(200)
25
25
  .optional()
26
26
  .describe("Number of records to return (default: 25, max: 200)"),
@@ -20,7 +20,7 @@ function registerFirewallTools(server, client, readOnly = false) {
20
20
  limit: zod_1.z
21
21
  .number()
22
22
  .int()
23
- .min(1)
23
+ .min(0)
24
24
  .max(200)
25
25
  .optional()
26
26
  .describe("Number of records to return (default: 25, max: 200)"),
@@ -71,7 +71,7 @@ function registerFirewallTools(server, client, readOnly = false) {
71
71
  limit: zod_1.z
72
72
  .number()
73
73
  .int()
74
- .min(1)
74
+ .min(0)
75
75
  .max(200)
76
76
  .optional()
77
77
  .describe("Number of records to return (default: 25, max: 200)"),
@@ -270,7 +270,7 @@ function registerFirewallTools(server, client, readOnly = false) {
270
270
  }
271
271
  });
272
272
  server.registerTool("unifi_patch_firewall_policy", {
273
- description: "Partially update a firewall policy without resending all fields. Common use: toggle loggingEnabled or enabled. Idempotent for fields supplied.",
273
+ description: "Partially update a firewall policy without resending all fields. loggingEnabled is the only field documented as patchable in API 10.6.106; other fields may be rejected — use unifi_update_firewall_policy for a full update. Idempotent for fields supplied.",
274
274
  inputSchema: {
275
275
  siteId: zod_1.z.string().describe("Site ID"),
276
276
  firewallPolicyId: zod_1.z.string().describe("Firewall policy ID"),
@@ -21,7 +21,7 @@ function registerHotspotTools(server, client, readOnly = false) {
21
21
  limit: zod_1.z
22
22
  .number()
23
23
  .int()
24
- .min(1)
24
+ .min(0)
25
25
  .max(1000)
26
26
  .optional()
27
27
  .default(100)
@@ -143,7 +143,7 @@ function registerHotspotTools(server, client, readOnly = false) {
143
143
  }
144
144
  });
145
145
  server.registerTool("unifi_delete_voucher", {
146
- description: "DESTRUCTIVE: Delete a hotspot voucher",
146
+ description: "DESTRUCTIVE: Delete a hotspot voucher. Returns: vouchersDeleted (count of vouchers removed).",
147
147
  inputSchema: {
148
148
  siteId: zod_1.z.string().describe("Site ID"),
149
149
  voucherId: zod_1.z.string().describe("Voucher ID"),
@@ -173,12 +173,12 @@ function registerHotspotTools(server, client, readOnly = false) {
173
173
  }
174
174
  });
175
175
  server.registerTool("unifi_bulk_delete_vouchers", {
176
- description: "DESTRUCTIVE: Bulk delete hotspot vouchers based on filter criteria",
176
+ description: "DESTRUCTIVE: Bulk delete hotspot vouchers based on filter criteria. Returns: vouchersDeleted (count of vouchers removed).",
177
177
  inputSchema: {
178
178
  siteId: zod_1.z.string().describe("Site ID"),
179
179
  filter: zod_1.z
180
180
  .string()
181
- .describe("Required filter expression (e.g., 'expired.eq(true)', 'name.like(guest*)')"),
181
+ .describe("Required filter expression (e.g., 'expired.eq(true)', name.like('guest*'))"),
182
182
  confirm: zod_1.z
183
183
  .boolean()
184
184
  .optional()
@@ -8,7 +8,7 @@ const safety_js_1 = require("../utils/safety.js");
8
8
  const output_schemas_js_1 = require("../utils/output-schemas.js");
9
9
  function registerNetworkTools(server, client, readOnly = false) {
10
10
  server.registerTool("unifi_list_networks", {
11
- description: "List all networks (VLANs/LAN segments) at a site. Returns: id, name, management (UNMANAGED/GATEWAY/SWITCH), enabled, vlanId, default (true for the default network), dhcpGuarding, metadata.origin. NOTE: the list view is sparse — for subnet/DHCP/NTP detail (ipv4Configuration), call unifi_get_network on a specific id. Use for: VLAN inventory; pair with unifi_get_network_references to find what consumes a network.",
11
+ description: "List all networks (VLANs/LAN segments) at a site. Returns: id, name, management (UNMANAGED/GATEWAY/SWITCH), enabled, vlanId, default (true for the default network), zoneId (the firewall zone the network sits in), dhcpGuarding, metadata.origin. NOTE: the list view is sparse — for subnet/DHCP/NTP detail (ipv4Configuration/ipv6Configuration), call unifi_get_network on a specific id. Use for: VLAN inventory; pair with unifi_get_network_references to find what consumes a network.",
12
12
  inputSchema: {
13
13
  siteId: zod_1.z.string().describe("Site ID"),
14
14
  offset: zod_1.z
@@ -20,7 +20,7 @@ function registerNetworkTools(server, client, readOnly = false) {
20
20
  limit: zod_1.z
21
21
  .number()
22
22
  .int()
23
- .min(1)
23
+ .min(0)
24
24
  .max(200)
25
25
  .optional()
26
26
  .describe("Number of records to return (default: 25, max: 200)"),
@@ -42,7 +42,7 @@ function registerNetworkTools(server, client, readOnly = false) {
42
42
  }
43
43
  });
44
44
  server.registerTool("unifi_get_network", {
45
- description: "Get a network/VLAN by ID. Returns the list fields PLUS (live-verified) zoneId, isolationEnabled, internetAccessEnabled, mdnsForwardingEnabled, cellularBackupEnabled, and a full ipv4Configuration object (hostIpAddress, prefixLength, dhcpConfiguration with ipAddressRange/leaseTimeSeconds/domainName/ntpServerIpAddresses). NOTE: subnet/DHCP detail appears here at get-by-id but NOT in unifi_list_networks (sparse list view).",
45
+ description: "Get a network/VLAN by ID. Returns the list fields PLUS (live-verified) isolationEnabled, internetAccessEnabled, mdnsForwardingEnabled, cellularBackupEnabled, a full ipv4Configuration object (autoScaleEnabled, hostIpAddress, prefixLength, dhcpConfiguration with mode/ipAddressRange/leaseTimeSeconds/domainName/pingConflictDetectionEnabled/ntpServerIpAddresses), and an ipv6Configuration object (interfaceType, clientAddressAssignment, routerAdvertisement, hostIpAddress, prefixLength). NOTE: subnet/DHCP detail appears here at get-by-id but NOT in unifi_list_networks (sparse list view).",
46
46
  inputSchema: {
47
47
  siteId: zod_1.z.string().describe("Site ID"),
48
48
  networkId: zod_1.z.string().describe("Network ID"),
@@ -19,14 +19,14 @@ function registerSiteTools(server, client) {
19
19
  limit: zod_1.z
20
20
  .number()
21
21
  .int()
22
- .min(1)
22
+ .min(0)
23
23
  .max(200)
24
24
  .optional()
25
25
  .describe("Number of records to return (default: 25, max: 200)"),
26
26
  filter: zod_1.z
27
27
  .string()
28
28
  .optional()
29
- .describe("Filter expression (e.g., 'name.like(office*)')"),
29
+ .describe("Filter expression (e.g., name.like('office*'))"),
30
30
  },
31
31
  outputSchema: output_schemas_js_1.listSitesOutputSchema,
32
32
  annotations: safety_js_1.READ_ONLY,
@@ -8,7 +8,7 @@ const safety_js_1 = require("../utils/safety.js");
8
8
  const output_schemas_js_1 = require("../utils/output-schemas.js");
9
9
  function registerSupportingTools(server, client) {
10
10
  server.registerTool("unifi_list_wans", {
11
- description: "List WAN interface definitions at a site. Returns: id, name only (verified against 10.5.43 — the Integration API exposes no live link status or throughput rates here). Use for: WAN inventory, multi-WAN topology.",
11
+ description: "List WAN interface definitions at a site. Returns: id, name only (verified against 10.6.106 — the Integration API exposes no live link status or throughput rates here). Use for: WAN inventory, multi-WAN topology.",
12
12
  inputSchema: {
13
13
  siteId: zod_1.z.string().describe("Site ID"),
14
14
  offset: zod_1.z
@@ -20,7 +20,7 @@ function registerSupportingTools(server, client) {
20
20
  limit: zod_1.z
21
21
  .number()
22
22
  .int()
23
- .min(1)
23
+ .min(0)
24
24
  .max(200)
25
25
  .optional()
26
26
  .describe("Number of records to return (default: 25, max: 200)"),
@@ -38,7 +38,7 @@ function registerSupportingTools(server, client) {
38
38
  }
39
39
  });
40
40
  server.registerTool("unifi_list_vpn_tunnels", {
41
- description: "List site-to-site VPN tunnels (IPsec, WireGuard, OpenVPN site-to-site) at a site. Returns: tunnel definitions per row (per-row schema not rendered in 10.5.43 docs — call to inspect). For roaming client VPN servers, see unifi_list_vpn_servers.",
41
+ description: "List site-to-site VPN tunnels (IPsec, WireGuard, OpenVPN site-to-site) at a site. Returns: tunnel definitions per row (per-row schema not rendered in 10.6.106 docs — call to inspect). For roaming client VPN servers, see unifi_list_vpn_servers.",
42
42
  inputSchema: {
43
43
  siteId: zod_1.z.string().describe("Site ID"),
44
44
  offset: zod_1.z
@@ -50,7 +50,7 @@ function registerSupportingTools(server, client) {
50
50
  limit: zod_1.z
51
51
  .number()
52
52
  .int()
53
- .min(1)
53
+ .min(0)
54
54
  .max(200)
55
55
  .optional()
56
56
  .describe("Number of records to return (default: 25, max: 200)"),
@@ -84,7 +84,7 @@ function registerSupportingTools(server, client) {
84
84
  limit: zod_1.z
85
85
  .number()
86
86
  .int()
87
- .min(1)
87
+ .min(0)
88
88
  .max(200)
89
89
  .optional()
90
90
  .describe("Number of records to return (default: 25, max: 200)"),
@@ -118,7 +118,7 @@ function registerSupportingTools(server, client) {
118
118
  limit: zod_1.z
119
119
  .number()
120
120
  .int()
121
- .min(1)
121
+ .min(0)
122
122
  .max(200)
123
123
  .optional()
124
124
  .describe("Number of records to return (default: 25, max: 200)"),
@@ -152,7 +152,7 @@ function registerSupportingTools(server, client) {
152
152
  limit: zod_1.z
153
153
  .number()
154
154
  .int()
155
- .min(1)
155
+ .min(0)
156
156
  .max(200)
157
157
  .optional()
158
158
  .describe("Number of records to return (default: 25, max: 200)"),
@@ -185,7 +185,7 @@ function registerSupportingTools(server, client) {
185
185
  limit: zod_1.z
186
186
  .number()
187
187
  .int()
188
- .min(1)
188
+ .min(0)
189
189
  .max(200)
190
190
  .optional()
191
191
  .describe("Number of records to return (default: 25, max: 200)"),
@@ -218,7 +218,7 @@ function registerSupportingTools(server, client) {
218
218
  limit: zod_1.z
219
219
  .number()
220
220
  .int()
221
- .min(1)
221
+ .min(0)
222
222
  .max(200)
223
223
  .optional()
224
224
  .describe("Number of records to return (default: 25, max: 200)"),
@@ -251,14 +251,14 @@ function registerSupportingTools(server, client) {
251
251
  limit: zod_1.z
252
252
  .number()
253
253
  .int()
254
- .min(1)
254
+ .min(0)
255
255
  .max(200)
256
256
  .optional()
257
257
  .describe("Number of records to return (default: 25, max: 200)"),
258
258
  filter: zod_1.z
259
259
  .string()
260
260
  .optional()
261
- .describe("Filter expression (e.g., 'name.like(United*)')"),
261
+ .describe("Filter expression (e.g., name.like('United*'))"),
262
262
  },
263
263
  outputSchema: output_schemas_js_1.listCountriesOutputSchema,
264
264
  annotations: safety_js_1.READ_ONLY,
@@ -8,7 +8,7 @@ const safety_js_1 = require("../utils/safety.js");
8
8
  const output_schemas_js_1 = require("../utils/output-schemas.js");
9
9
  function registerSwitchingTools(server, client) {
10
10
  server.registerTool("unifi_list_switch_stacks", {
11
- description: "List Switch Stacks (multiple physical switches managed as one logical unit) at a site. Returns: id, name, members[], lags[] (LAGs spanning the stack), metadata.origin. Use for: identifying stacked switches; individual member configs/stats still come from unifi_get_device.",
11
+ description: "List Switch Stacks (multiple physical switches managed as one logical unit) at a site. Returns: id, deviceId (the stack's primary device), name, units[] (the stacked switches — named `members` on consoles older than 10.6.106), lags[] (LAGs spanning the stack), metadata.origin. Per-unit schema is not rendered in the 10.6.106 docs — call unifi_get_switch_stack to inspect. Use for: identifying stacked switches; individual member configs/stats still come from unifi_get_device.",
12
12
  inputSchema: {
13
13
  siteId: zod_1.z.string().describe("Site ID"),
14
14
  offset: zod_1.z
@@ -20,7 +20,7 @@ function registerSwitchingTools(server, client) {
20
20
  limit: zod_1.z
21
21
  .number()
22
22
  .int()
23
- .min(1)
23
+ .min(0)
24
24
  .max(200)
25
25
  .optional()
26
26
  .describe("Number of records to return (default: 25, max: 200)"),
@@ -42,7 +42,7 @@ function registerSwitchingTools(server, client) {
42
42
  }
43
43
  });
44
44
  server.registerTool("unifi_get_switch_stack", {
45
- description: "Get full details of a Switch Stack including all members and stacking topology. Returns the same fields as the list response but for a single stack.",
45
+ description: "Get full details of a Switch Stack including all units and stacking topology. Returns the same fields as the list response but for a single stack: id, deviceId, name, units[] (named `members` before 10.6.106), lags[], metadata.origin.",
46
46
  inputSchema: {
47
47
  siteId: zod_1.z.string().describe("Site ID"),
48
48
  switchStackId: zod_1.z.string().describe("Switch Stack ID"),
@@ -71,7 +71,7 @@ function registerSwitchingTools(server, client) {
71
71
  limit: zod_1.z
72
72
  .number()
73
73
  .int()
74
- .min(1)
74
+ .min(0)
75
75
  .max(200)
76
76
  .optional()
77
77
  .describe("Number of records to return (default: 25, max: 200)"),
@@ -122,7 +122,7 @@ function registerSwitchingTools(server, client) {
122
122
  limit: zod_1.z
123
123
  .number()
124
124
  .int()
125
- .min(1)
125
+ .min(0)
126
126
  .max(200)
127
127
  .optional()
128
128
  .describe("Number of records to return (default: 25, max: 200)"),
@@ -6,7 +6,7 @@ const safety_js_1 = require("../utils/safety.js");
6
6
  const output_schemas_js_1 = require("../utils/output-schemas.js");
7
7
  function registerSystemTools(server, client) {
8
8
  server.registerTool("unifi_get_info", {
9
- description: "Get UniFi Network application info. Returns: applicationVersion. NOTE: verified against 10.5.43 on a UniFi OS console — the Integration API returns ONLY applicationVersion here; there is no isUniFiOSConsole or other field. Use for: version checks before calling version-gated tools.",
9
+ description: "Get UniFi Network application info. Returns: applicationVersion. NOTE: verified against 10.6.106 on a UniFi OS console — the Integration API returns ONLY applicationVersion here; there is no isUniFiOSConsole or other field. Use for: version checks before calling version-gated tools.",
10
10
  inputSchema: {},
11
11
  outputSchema: output_schemas_js_1.getInfoOutputSchema,
12
12
  annotations: safety_js_1.READ_ONLY,
@@ -20,7 +20,7 @@ function registerTrafficMatchingTools(server, client, readOnly = false) {
20
20
  limit: zod_1.z
21
21
  .number()
22
22
  .int()
23
- .min(1)
23
+ .min(0)
24
24
  .max(200)
25
25
  .optional()
26
26
  .describe("Number of records to return (default: 25, max: 200)"),
@@ -20,7 +20,7 @@ function registerWifiTools(server, client, readOnly = false) {
20
20
  limit: zod_1.z
21
21
  .number()
22
22
  .int()
23
- .min(1)
23
+ .min(0)
24
24
  .max(200)
25
25
  .optional()
26
26
  .describe("Number of records to return (default: 25, max: 200)"),
@@ -68,8 +68,8 @@ function registerWifiTools(server, client, readOnly = false) {
68
68
  enabled: zod_1.z.boolean().describe("Enable the WiFi network"),
69
69
  type: zod_1.z.enum(["STANDARD", "IOT_OPTIMIZED"]).describe("WiFi type"),
70
70
  broadcastingFrequenciesGHz: zod_1.z
71
- .array(zod_1.z.string())
72
- .describe("Frequencies: 2.4, 5, 6"),
71
+ .array(zod_1.z.enum(["2.4", "5", "6"]))
72
+ .describe("Frequencies to broadcast on. Accepted as strings and sent to the API as numbers (2.4, 5, 6)."),
73
73
  securityConfiguration: zod_1.z
74
74
  .record(zod_1.z.string(), zod_1.z.unknown())
75
75
  .describe("Security configuration object"),
@@ -209,7 +209,7 @@ function registerWifiTools(server, client, readOnly = false) {
209
209
  }
210
210
  });
211
211
  server.registerTool("unifi_update_wifi", {
212
- description: "Update an existing WiFi network",
212
+ description: "Update an existing WiFi network. NOTE: this is a full PUT — API 10.6.106 documents 14 of these fields as required (type, name, enabled, securityConfiguration, multicastToUnicastConversionEnabled, clientIsolationEnabled, hideName, uapsdEnabled, channel2gLockedTo6, dtimPeriod2gLockedTo3, broadcastingFrequenciesGHz, arpProxyEnabled, bssTransitionEnabled, advertiseDeviceName). They are optional here for convenience, but a sparse update may be rejected by the API — read the current values with unifi_get_wifi and resend the full set if you get a 400.",
213
213
  inputSchema: {
214
214
  siteId: zod_1.z.string().describe("Site ID"),
215
215
  wifiBroadcastId: zod_1.z.string().describe("WiFi Broadcast ID"),
@@ -220,9 +220,9 @@ function registerWifiTools(server, client, readOnly = false) {
220
220
  .optional()
221
221
  .describe("WiFi type"),
222
222
  broadcastingFrequenciesGHz: zod_1.z
223
- .array(zod_1.z.string())
223
+ .array(zod_1.z.enum(["2.4", "5", "6"]))
224
224
  .optional()
225
- .describe("Frequencies: 2.4, 5, 6"),
225
+ .describe("Frequencies to broadcast on. Accepted as strings and sent to the API as numbers (2.4, 5, 6)."),
226
226
  securityConfiguration: zod_1.z
227
227
  .record(zod_1.z.string(), zod_1.z.unknown())
228
228
  .optional()
@@ -3,7 +3,7 @@
3
3
  * field optional, every nested object uses .passthrough() to allow
4
4
  * firmware/hardware-specific fields to flow through unchanged).
5
5
  *
6
- * Verified against UniFi Network API 10.5.43. Where the docs collapse
6
+ * Verified against UniFi Network API 10.6.106. Where the docs collapse
7
7
  * nested arrays/objects (e.g. interfaces.ports[], radios[]), the schema
8
8
  * uses passthrough records so the contract doesn't lock to fields we
9
9
  * haven't verified.
@@ -37,6 +37,7 @@ export declare const networkOutputSchema: {
37
37
  cellularBackupEnabled: z.ZodOptional<z.ZodBoolean>;
38
38
  dhcpGuarding: z.ZodOptional<z.ZodUnknown>;
39
39
  ipv4Configuration: z.ZodOptional<z.ZodUnknown>;
40
+ ipv6Configuration: z.ZodOptional<z.ZodUnknown>;
40
41
  metadata: z.ZodOptional<z.ZodObject<{
41
42
  origin: z.ZodOptional<z.ZodString>;
42
43
  }, z.core.$loose>>;
@@ -60,6 +61,7 @@ export declare const listNetworksOutputSchema: {
60
61
  cellularBackupEnabled: z.ZodOptional<z.ZodBoolean>;
61
62
  dhcpGuarding: z.ZodOptional<z.ZodUnknown>;
62
63
  ipv4Configuration: z.ZodOptional<z.ZodUnknown>;
64
+ ipv6Configuration: z.ZodOptional<z.ZodUnknown>;
63
65
  metadata: z.ZodOptional<z.ZodObject<{
64
66
  origin: z.ZodOptional<z.ZodString>;
65
67
  }, z.core.$loose>>;
@@ -202,6 +204,7 @@ export declare const aclRuleOutputSchema: {
202
204
  sourceFilter: z.ZodOptional<z.ZodUnknown>;
203
205
  destinationFilter: z.ZodOptional<z.ZodUnknown>;
204
206
  enforcingDeviceFilter: z.ZodOptional<z.ZodUnknown>;
207
+ index: z.ZodOptional<z.ZodNumber>;
205
208
  metadata: z.ZodOptional<z.ZodObject<{
206
209
  origin: z.ZodOptional<z.ZodString>;
207
210
  }, z.core.$loose>>;
@@ -222,6 +225,7 @@ export declare const listAclRulesOutputSchema: {
222
225
  sourceFilter: z.ZodOptional<z.ZodUnknown>;
223
226
  destinationFilter: z.ZodOptional<z.ZodUnknown>;
224
227
  enforcingDeviceFilter: z.ZodOptional<z.ZodUnknown>;
228
+ index: z.ZodOptional<z.ZodNumber>;
225
229
  metadata: z.ZodOptional<z.ZodObject<{
226
230
  origin: z.ZodOptional<z.ZodString>;
227
231
  }, z.core.$loose>>;
@@ -233,6 +237,8 @@ export declare const aclRuleOrderingOutputSchema: {
233
237
  export declare const switchStackOutputSchema: {
234
238
  id: z.ZodString;
235
239
  name: z.ZodOptional<z.ZodString>;
240
+ deviceId: z.ZodOptional<z.ZodString>;
241
+ units: z.ZodOptional<z.ZodArray<z.ZodUnknown>>;
236
242
  members: z.ZodOptional<z.ZodArray<z.ZodUnknown>>;
237
243
  lags: z.ZodOptional<z.ZodArray<z.ZodUnknown>>;
238
244
  metadata: z.ZodOptional<z.ZodObject<{
@@ -247,6 +253,8 @@ export declare const listSwitchStacksOutputSchema: {
247
253
  readonly data: z.ZodArray<z.ZodObject<{
248
254
  id: z.ZodString;
249
255
  name: z.ZodOptional<z.ZodString>;
256
+ deviceId: z.ZodOptional<z.ZodString>;
257
+ units: z.ZodOptional<z.ZodArray<z.ZodUnknown>>;
250
258
  members: z.ZodOptional<z.ZodArray<z.ZodUnknown>>;
251
259
  lags: z.ZodOptional<z.ZodArray<z.ZodUnknown>>;
252
260
  metadata: z.ZodOptional<z.ZodObject<{
@@ -6,7 +6,7 @@ exports.listPendingDevicesOutputSchema = exports.listCountriesOutputSchema = exp
6
6
  * field optional, every nested object uses .passthrough() to allow
7
7
  * firmware/hardware-specific fields to flow through unchanged).
8
8
  *
9
- * Verified against UniFi Network API 10.5.43. Where the docs collapse
9
+ * Verified against UniFi Network API 10.6.106. Where the docs collapse
10
10
  * nested arrays/objects (e.g. interfaces.ports[], radios[]), the schema
11
11
  * uses passthrough records so the contract doesn't lock to fields we
12
12
  * haven't verified.
@@ -160,7 +160,7 @@ const WifiBroadcast = zod_1.z
160
160
  .passthrough();
161
161
  const ApplicationInfo = zod_1.z
162
162
  .object({
163
- // Live-verified (10.5.43): only applicationVersion is returned.
163
+ // Live-verified (10.6.106): only applicationVersion is returned.
164
164
  applicationVersion: zod_1.z.string().optional(),
165
165
  })
166
166
  .passthrough();
@@ -185,8 +185,9 @@ const Network = zod_1.z
185
185
  mdnsForwardingEnabled: zod_1.z.boolean().optional(),
186
186
  cellularBackupEnabled: zod_1.z.boolean().optional(),
187
187
  dhcpGuarding: zod_1.z.unknown().optional(),
188
- // get-by-id only (absent in list view); nested shape left loose.
188
+ // get-by-id only (absent in list view); nested shapes left loose.
189
189
  ipv4Configuration: zod_1.z.unknown().optional(),
190
+ ipv6Configuration: zod_1.z.unknown().optional(),
190
191
  metadata: Metadata.optional(),
191
192
  })
192
193
  .passthrough();
@@ -263,6 +264,9 @@ const AclRule = zod_1.z
263
264
  sourceFilter: zod_1.z.unknown().optional(),
264
265
  destinationFilter: zod_1.z.unknown().optional(),
265
266
  enforcingDeviceFilter: zod_1.z.unknown().optional(),
267
+ // Read-only in the response. Deprecated as a *request* field — ordering
268
+ // is set via the dedicated reorder endpoint, so no write tool accepts it.
269
+ index: zod_1.z.number().int().optional(),
266
270
  metadata: Metadata.optional(),
267
271
  })
268
272
  .passthrough();
@@ -275,6 +279,12 @@ const SwitchStack = zod_1.z
275
279
  .object({
276
280
  id: zod_1.z.string(),
277
281
  name: zod_1.z.string().optional(),
282
+ // The stack's primary device. Added in 10.6.106.
283
+ deviceId: zod_1.z.string().optional(),
284
+ // Renamed from `members` in 10.6.106 (was `members` through 10.4.57).
285
+ // `members` stays declared so responses from older consoles still
286
+ // describe themselves; both are optional, so either shape validates.
287
+ units: zod_1.z.array(zod_1.z.unknown()).optional(),
278
288
  members: zod_1.z.array(zod_1.z.unknown()).optional(),
279
289
  lags: zod_1.z.array(zod_1.z.unknown()).optional(),
280
290
  metadata: Metadata.optional(),
@@ -324,7 +334,7 @@ const TrafficMatchingList = zod_1.z
324
334
  metadata: Metadata.optional(),
325
335
  })
326
336
  .passthrough();
327
- // Verified against the live Integration API (10.5.43). The WAN and
337
+ // Verified against the live Integration API (10.6.106). The WAN and
328
338
  // site-to-site-tunnel list rows are intentionally minimal in the API.
329
339
  const Wan = zod_1.z
330
340
  .object({
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@owine/unifi-network-mcp",
3
- "version": "2.11.4",
3
+ "version": "2.13.0",
4
4
  "description": "MCP server for the UniFi Network API",
5
5
  "main": "dist/index.js",
6
6
  "bin": {
@@ -22,7 +22,7 @@
22
22
  "engines": {
23
23
  "node": "^22.13.0 || ^24.0.0"
24
24
  },
25
- "packageManager": "pnpm@11.24.0",
25
+ "packageManager": "pnpm@12.4.1",
26
26
  "repository": {
27
27
  "type": "git",
28
28
  "url": "git+https://github.com/owine/unifi-network-mcp.git"
@@ -38,17 +38,17 @@
38
38
  "license": "MIT",
39
39
  "dependencies": {
40
40
  "@modelcontextprotocol/sdk": "1.30.0",
41
- "zod": "4.5.4"
41
+ "zod": "4.6.2"
42
42
  },
43
43
  "devDependencies": {
44
44
  "@eslint/js": "10.0.1",
45
- "@types/node": "24.13.3",
45
+ "@types/node": "24.13.4",
46
46
  "@typescript/native": "npm:typescript@7.0.2",
47
- "@vitest/coverage-v8": "4.1.11",
48
- "eslint": "10.9.1",
47
+ "@vitest/coverage-v8": "5.0.0",
48
+ "eslint": "10.10.0",
49
49
  "lefthook": "2.1.12",
50
50
  "typescript": "npm:@typescript/typescript6@6.0.2",
51
- "typescript-eslint": "8.68.0",
52
- "vitest": "4.1.11"
51
+ "typescript-eslint": "8.70.0",
52
+ "vitest": "5.0.0"
53
53
  }
54
54
  }