@owine/unifi-network-mcp 2.7.5 → 2.8.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/dist/tools/acl.js CHANGED
@@ -92,6 +92,18 @@ function registerAclTools(server, client, readOnly = false) {
92
92
  .array(zod_1.z.enum(["TCP", "UDP"]))
93
93
  .optional()
94
94
  .describe("Protocols this ACL rule will be applied to (TCP, UDP). When null, applies to all protocols"),
95
+ sourceFilter: zod_1.z
96
+ .unknown()
97
+ .optional()
98
+ .describe("Traffic source filter (opaque object — pass the structure the API expects)"),
99
+ destinationFilter: zod_1.z
100
+ .unknown()
101
+ .optional()
102
+ .describe("Traffic destination filter (opaque object — pass the structure the API expects)"),
103
+ enforcingDeviceFilter: zod_1.z
104
+ .unknown()
105
+ .optional()
106
+ .describe("IDs of the Switch-capable devices used to enforce the ACL rule. When null/omitted, the rule is provisioned to all switches on the site"),
95
107
  dryRun: zod_1.z
96
108
  .boolean()
97
109
  .optional()
@@ -99,13 +111,19 @@ function registerAclTools(server, client, readOnly = false) {
99
111
  },
100
112
  outputSchema: output_schemas_js_1.aclRuleOutputSchema,
101
113
  annotations: safety_js_1.WRITE_NOT_IDEMPOTENT,
102
- }, async ({ siteId, type, name, enabled, action, description, protocolFilter, dryRun }) => {
114
+ }, async ({ siteId, type, name, enabled, action, description, protocolFilter, sourceFilter, destinationFilter, enforcingDeviceFilter, dryRun }) => {
103
115
  try {
104
116
  const body = { type, name, enabled, action };
105
117
  if (description !== undefined)
106
118
  body.description = description;
107
119
  if (protocolFilter !== undefined)
108
120
  body.protocolFilter = protocolFilter;
121
+ if (sourceFilter !== undefined)
122
+ body.sourceFilter = sourceFilter;
123
+ if (destinationFilter !== undefined)
124
+ body.destinationFilter = destinationFilter;
125
+ if (enforcingDeviceFilter !== undefined)
126
+ body.enforcingDeviceFilter = enforcingDeviceFilter;
109
127
  if (dryRun)
110
128
  return (0, safety_js_1.formatDryRun)("POST", `/sites/${siteId}/acl-rules`, body);
111
129
  const data = await client.post(`/sites/${siteId}/acl-rules`, body);
@@ -132,6 +150,18 @@ function registerAclTools(server, client, readOnly = false) {
132
150
  .array(zod_1.z.enum(["TCP", "UDP"]))
133
151
  .optional()
134
152
  .describe("Protocols this ACL rule will be applied to (TCP, UDP). When null, applies to all protocols"),
153
+ sourceFilter: zod_1.z
154
+ .unknown()
155
+ .optional()
156
+ .describe("Traffic source filter (opaque object — pass the structure the API expects)"),
157
+ destinationFilter: zod_1.z
158
+ .unknown()
159
+ .optional()
160
+ .describe("Traffic destination filter (opaque object — pass the structure the API expects)"),
161
+ enforcingDeviceFilter: zod_1.z
162
+ .unknown()
163
+ .optional()
164
+ .describe("IDs of the Switch-capable devices used to enforce the ACL rule. When null/omitted, the rule is provisioned to all switches on the site"),
135
165
  dryRun: zod_1.z
136
166
  .boolean()
137
167
  .optional()
@@ -139,13 +169,19 @@ function registerAclTools(server, client, readOnly = false) {
139
169
  },
140
170
  outputSchema: output_schemas_js_1.aclRuleOutputSchema,
141
171
  annotations: safety_js_1.WRITE,
142
- }, async ({ siteId, aclRuleId, type, name, enabled, action, description, protocolFilter, dryRun }) => {
172
+ }, async ({ siteId, aclRuleId, type, name, enabled, action, description, protocolFilter, sourceFilter, destinationFilter, enforcingDeviceFilter, dryRun }) => {
143
173
  try {
144
174
  const body = { type, name, enabled, action };
145
175
  if (description !== undefined)
146
176
  body.description = description;
147
177
  if (protocolFilter !== undefined)
148
178
  body.protocolFilter = protocolFilter;
179
+ if (sourceFilter !== undefined)
180
+ body.sourceFilter = sourceFilter;
181
+ if (destinationFilter !== undefined)
182
+ body.destinationFilter = destinationFilter;
183
+ if (enforcingDeviceFilter !== undefined)
184
+ body.enforcingDeviceFilter = enforcingDeviceFilter;
149
185
  if (dryRun)
150
186
  return (0, safety_js_1.formatDryRun)("PUT", `/sites/${siteId}/acl-rules/${aclRuleId}`, body);
151
187
  const data = await client.put(`/sites/${siteId}/acl-rules/${aclRuleId}`, body);
@@ -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.4.55 — 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, 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.",
12
12
  inputSchema: {
13
13
  siteId: zod_1.z.string().describe("Site ID"),
14
14
  offset: zod_1.z
@@ -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.4.55 — 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.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.",
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.4.55 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.5.43 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()
@@ -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.4.55 — 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.5.43 — 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
@@ -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.4.55 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.5.43 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
@@ -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.4.55 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.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.",
10
10
  inputSchema: {},
11
11
  outputSchema: output_schemas_js_1.getInfoOutputSchema,
12
12
  annotations: safety_js_1.READ_ONLY,
@@ -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.4.55. Where the docs collapse
6
+ * Verified against UniFi Network API 10.5.43. 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.
@@ -105,7 +105,7 @@ export declare const listVouchersOutputSchema: {
105
105
  readonly totalCount: z.ZodOptional<z.ZodNumber>;
106
106
  };
107
107
  export declare const createVouchersOutputSchema: {
108
- readonly data: z.ZodArray<z.ZodObject<{
108
+ readonly vouchers: z.ZodArray<z.ZodObject<{
109
109
  id: z.ZodString;
110
110
  code: z.ZodOptional<z.ZodString>;
111
111
  name: z.ZodOptional<z.ZodString>;
@@ -120,10 +120,6 @@ export declare const createVouchersOutputSchema: {
120
120
  authorizedGuestCount: z.ZodOptional<z.ZodNumber>;
121
121
  expired: z.ZodOptional<z.ZodBoolean>;
122
122
  }, z.core.$loose>>;
123
- readonly offset: z.ZodOptional<z.ZodNumber>;
124
- readonly limit: z.ZodOptional<z.ZodNumber>;
125
- readonly count: z.ZodOptional<z.ZodNumber>;
126
- readonly totalCount: z.ZodOptional<z.ZodNumber>;
127
123
  };
128
124
  export declare const firewallZoneOutputSchema: {
129
125
  id: z.ZodString;
@@ -205,6 +201,7 @@ export declare const aclRuleOutputSchema: {
205
201
  protocolFilter: z.ZodOptional<z.ZodArray<z.ZodString>>;
206
202
  sourceFilter: z.ZodOptional<z.ZodUnknown>;
207
203
  destinationFilter: z.ZodOptional<z.ZodUnknown>;
204
+ enforcingDeviceFilter: z.ZodOptional<z.ZodUnknown>;
208
205
  metadata: z.ZodOptional<z.ZodObject<{
209
206
  origin: z.ZodOptional<z.ZodString>;
210
207
  }, z.core.$loose>>;
@@ -220,6 +217,7 @@ export declare const listAclRulesOutputSchema: {
220
217
  protocolFilter: z.ZodOptional<z.ZodArray<z.ZodString>>;
221
218
  sourceFilter: z.ZodOptional<z.ZodUnknown>;
222
219
  destinationFilter: z.ZodOptional<z.ZodUnknown>;
220
+ enforcingDeviceFilter: z.ZodOptional<z.ZodUnknown>;
223
221
  metadata: z.ZodOptional<z.ZodObject<{
224
222
  origin: z.ZodOptional<z.ZodString>;
225
223
  }, z.core.$loose>>;
@@ -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.4.55. Where the docs collapse
9
+ * Verified against UniFi Network API 10.5.43. 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.4.55): only applicationVersion is returned.
163
+ // Live-verified (10.5.43): only applicationVersion is returned.
164
164
  applicationVersion: zod_1.z.string().optional(),
165
165
  })
166
166
  .passthrough();
@@ -262,6 +262,7 @@ const AclRule = zod_1.z
262
262
  protocolFilter: zod_1.z.array(zod_1.z.string()).optional(),
263
263
  sourceFilter: zod_1.z.unknown().optional(),
264
264
  destinationFilter: zod_1.z.unknown().optional(),
265
+ enforcingDeviceFilter: zod_1.z.unknown().optional(),
265
266
  metadata: Metadata.optional(),
266
267
  })
267
268
  .passthrough();
@@ -323,7 +324,7 @@ const TrafficMatchingList = zod_1.z
323
324
  metadata: Metadata.optional(),
324
325
  })
325
326
  .passthrough();
326
- // Verified against the live Integration API (10.4.55). The WAN and
327
+ // Verified against the live Integration API (10.5.43). The WAN and
327
328
  // site-to-site-tunnel list rows are intentionally minimal in the API.
328
329
  const Wan = zod_1.z
329
330
  .object({
@@ -407,9 +408,12 @@ exports.listVouchersOutputSchema = {
407
408
  ...PaginationFields,
408
409
  data: zod_1.z.array(Voucher),
409
410
  };
411
+ // NOTE: the Generate Vouchers endpoint does NOT return the standard
412
+ // paginated envelope. Live-verified (10.5.43): it returns
413
+ // `{ "vouchers": [ ...Voucher ] }` with no offset/limit/count/totalCount.
414
+ // An over-tight schema here (e.g. requiring `data`) hard-fails every call.
410
415
  exports.createVouchersOutputSchema = {
411
- ...PaginationFields,
412
- data: zod_1.z.array(Voucher),
416
+ vouchers: zod_1.z.array(Voucher),
413
417
  };
414
418
  exports.firewallZoneOutputSchema = FirewallZone.shape;
415
419
  exports.listFirewallZonesOutputSchema = {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@owine/unifi-network-mcp",
3
- "version": "2.7.5",
3
+ "version": "2.8.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.6.0",
25
+ "packageManager": "pnpm@11.8.0",
26
26
  "repository": {
27
27
  "type": "git",
28
28
  "url": "https://github.com/owine/unifi-network-mcp.git"
@@ -43,11 +43,11 @@
43
43
  "devDependencies": {
44
44
  "@eslint/js": "10.0.1",
45
45
  "@types/node": "24.13.2",
46
- "@vitest/coverage-v8": "4.1.8",
46
+ "@vitest/coverage-v8": "4.1.9",
47
47
  "eslint": "10.5.0",
48
48
  "lefthook": "2.1.9",
49
49
  "typescript": "6.0.3",
50
- "typescript-eslint": "8.61.0",
51
- "vitest": "4.1.8"
50
+ "typescript-eslint": "8.61.1",
51
+ "vitest": "4.1.9"
52
52
  }
53
53
  }