@owine/unifi-network-mcp 2.14.2 → 2.15.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.
@@ -5,10 +5,106 @@ const zod_1 = require("zod");
5
5
  const responses_js_1 = require("../utils/responses.js");
6
6
  const query_js_1 = require("../utils/query.js");
7
7
  const safety_js_1 = require("../utils/safety.js");
8
+ const full_put_js_1 = require("../utils/full-put.js");
8
9
  const output_schemas_js_1 = require("../utils/output-schemas.js");
10
+ // Request-body fields per management type, from the 11.0.83 POST/PUT schemas.
11
+ const COMMON_FIELDS = ["management", "name", "enabled", "vlanId", "dhcpGuarding"];
12
+ const COMMON_REQUIRED = ["management", "name", "enabled", "vlanId"];
13
+ const NETWORK_VARIANTS = {
14
+ UNMANAGED: { allowed: COMMON_FIELDS, required: COMMON_REQUIRED },
15
+ GATEWAY: {
16
+ allowed: [
17
+ ...COMMON_FIELDS,
18
+ "isolationEnabled",
19
+ "cellularBackupEnabled",
20
+ "zoneId",
21
+ "internetAccessEnabled",
22
+ "mdnsForwardingEnabled",
23
+ "ipv4Configuration",
24
+ "ipv6Configuration",
25
+ ],
26
+ required: [
27
+ ...COMMON_REQUIRED,
28
+ "isolationEnabled",
29
+ "cellularBackupEnabled",
30
+ "internetAccessEnabled",
31
+ "ipv4Configuration",
32
+ ],
33
+ },
34
+ SWITCH: {
35
+ allowed: [
36
+ ...COMMON_FIELDS,
37
+ "isolationEnabled",
38
+ "cellularBackupEnabled",
39
+ "deviceId",
40
+ "ipv4Configuration",
41
+ ],
42
+ required: [
43
+ ...COMMON_REQUIRED,
44
+ "isolationEnabled",
45
+ "cellularBackupEnabled",
46
+ "deviceId",
47
+ "ipv4Configuration",
48
+ ],
49
+ },
50
+ };
51
+ const NETWORK_VARIANTS_DOC = "all types take name, enabled, vlanId, optional dhcpGuarding (omitted or null = disabled). GATEWAY (routed by the UniFi gateway) also requires isolationEnabled, cellularBackupEnabled, internetAccessEnabled, ipv4Configuration (gateway-managed subnet + DHCP), optional zoneId, mdnsForwardingEnabled, ipv6Configuration. SWITCH (routed by an L3 switch) also requires isolationEnabled, cellularBackupEnabled, deviceId (the L3 switch), ipv4Configuration (switch-managed).";
52
+ function fieldsForManagement(management) {
53
+ if (management === "UNMANAGED" || management === "GATEWAY" || management === "SWITCH") {
54
+ return NETWORK_VARIANTS[management];
55
+ }
56
+ throw new Error(`Unsupported network management type: ${String(management)}`);
57
+ }
58
+ function checkNetworkBody(body, updates) {
59
+ const { allowed, required } = fieldsForManagement(body.management);
60
+ const rejected = (0, full_put_js_1.unsupportedFields)(updates, allowed);
61
+ if (rejected.length > 0) {
62
+ throw new Error(`Fields not accepted for management ${String(body.management)}: ${rejected.join(", ")}`);
63
+ }
64
+ const missing = (0, full_put_js_1.missingFields)(body, required);
65
+ if (missing.length > 0) {
66
+ throw new Error(`Missing required fields for management ${String(body.management)}: ${missing.join(", ")}`);
67
+ }
68
+ }
69
+ const config = (description) => zod_1.z.record(zod_1.z.string(), zod_1.z.unknown()).describe(description);
70
+ // Every body field as an optional input. Create tightens the common fields to
71
+ // required; update keeps them optional and fills gaps from the current network.
72
+ const networkFields = {
73
+ name: zod_1.z.string().optional().describe("Network name"),
74
+ management: zod_1.z
75
+ .enum(["UNMANAGED", "GATEWAY", "SWITCH"])
76
+ .optional()
77
+ .describe("Network management type"),
78
+ enabled: zod_1.z.boolean().optional().describe("Enable the network"),
79
+ vlanId: zod_1.z
80
+ .number()
81
+ .int()
82
+ .min(1)
83
+ .max(4009)
84
+ .optional()
85
+ .describe("VLAN ID (1 for the default network, 2-4009 for additional networks)"),
86
+ dhcpGuarding: config("DHCP Guarding settings (trustedDhcpServerIpAddresses). Omitted or null = disabled.")
87
+ .nullable()
88
+ .optional(),
89
+ isolationEnabled: zod_1.z.boolean().optional().describe("Isolate this network from all other networks. GATEWAY/SWITCH."),
90
+ cellularBackupEnabled: zod_1.z.boolean().optional().describe("Allow cellular data when WAN connection(s) are down. GATEWAY/SWITCH."),
91
+ internetAccessEnabled: zod_1.z.boolean().optional().describe("Allow internet access for devices on this network. GATEWAY only."),
92
+ mdnsForwardingEnabled: zod_1.z
93
+ .boolean()
94
+ .nullable()
95
+ .optional()
96
+ .describe("Participate in mDNS forwarding; null = use the site mDNS default. GATEWAY only."),
97
+ zoneId: zod_1.z.string().optional().describe("Firewall zone ID for this network. GATEWAY only."),
98
+ deviceId: zod_1.z.string().optional().describe("ID of the L3 switching capable device that manages this network. SWITCH only."),
99
+ ipv4Configuration: config("IPv4 configuration (hostIpAddress, prefixLength, dhcpConfiguration, …) — copy the shape from unifi_get_network. GATEWAY/SWITCH.")
100
+ .optional(),
101
+ ipv6Configuration: config("IPv6 configuration (interfaceType, clientAddressAssignment, routerAdvertisement, …); omitted or null = IPv6 not configured. GATEWAY only.")
102
+ .nullable()
103
+ .optional(),
104
+ };
9
105
  function registerNetworkTools(server, client, readOnly = false) {
10
106
  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), 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.",
107
+ 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 (only present when enabled), metadata.origin (+ metadata.configurable on the system-defined default network). 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
108
  inputSchema: {
13
109
  siteId: zod_1.z.string().describe("Site ID"),
14
110
  offset: zod_1.z
@@ -27,7 +123,7 @@ function registerNetworkTools(server, client, readOnly = false) {
27
123
  filter: zod_1.z
28
124
  .string()
29
125
  .optional()
30
- .describe("Filter expression"),
126
+ .describe("Filter expression, e.g. vlanId.eq(20)"),
31
127
  },
32
128
  outputSchema: output_schemas_js_1.listNetworksOutputSchema,
33
129
  annotations: safety_js_1.READ_ONLY,
@@ -42,7 +138,7 @@ function registerNetworkTools(server, client, readOnly = false) {
42
138
  }
43
139
  });
44
140
  server.registerTool("unifi_get_network", {
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).",
141
+ 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 — which may contain empty-string entries), and an ipv6Configuration object when IPv6 is configured (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
142
  inputSchema: {
47
143
  siteId: zod_1.z.string().describe("Site ID"),
48
144
  networkId: zod_1.z.string().describe("Network ID"),
@@ -59,7 +155,7 @@ function registerNetworkTools(server, client, readOnly = false) {
59
155
  }
60
156
  });
61
157
  server.registerTool("unifi_get_network_references", {
62
- description: "Get all objects that reference this network (WiFi broadcasts, firewall zones, etc.). Returns: { referenceResources: [...] }. Use before deleting a network to find dependencies that need to be re-pointed or removed.",
158
+ description: "Get all objects that reference this network. Returns: { referenceResources: [{ resourceType (e.g. WIFI), referenceCount, references: [{ referenceId }] }] }. Use before deleting a network to find dependencies that need to be re-pointed or removed. NOTE: on 11.0.83 the controller returns HTTP 500 (api.unexpected-error) for many networks — observed on the default network and on networks used by PPSK keys. On a 500, fall back to unifi_list_wifi + unifi_get_wifi (network.networkId and securityConfiguration.presharedKeys[].network.networkId) and unifi_list_firewall_zones (networkIds).",
63
159
  inputSchema: {
64
160
  siteId: zod_1.z.string().describe("Site ID"),
65
161
  networkId: zod_1.z.string().describe("Network ID"),
@@ -78,9 +174,10 @@ function registerNetworkTools(server, client, readOnly = false) {
78
174
  if (readOnly)
79
175
  return;
80
176
  server.registerTool("unifi_create_network", {
81
- description: "Create a new VLAN/network. management=GATEWAY means routed via UDM/UXG; SWITCH means VLAN-only; UNMANAGED means external. Idempotency: not safe to retry — re-running creates duplicates.",
177
+ description: `Create a new VLAN/network. The body depends on management (per 11.0.83 docs): ${NETWORK_VARIANTS_DOC} Idempotency: not safe to retry — re-running creates duplicates.`,
82
178
  inputSchema: {
83
179
  siteId: zod_1.z.string().describe("Site ID"),
180
+ ...networkFields,
84
181
  name: zod_1.z.string().describe("Network name"),
85
182
  management: zod_1.z
86
183
  .enum(["UNMANAGED", "GATEWAY", "SWITCH"])
@@ -92,10 +189,6 @@ function registerNetworkTools(server, client, readOnly = false) {
92
189
  .min(1)
93
190
  .max(4009)
94
191
  .describe("VLAN ID (1 for default, 2+ for additional)"),
95
- dhcpGuarding: zod_1.z
96
- .record(zod_1.z.string(), zod_1.z.unknown())
97
- .optional()
98
- .describe("DHCP Guarding settings (trustedDhcpServerIpAddresses)"),
99
192
  dryRun: zod_1.z
100
193
  .boolean()
101
194
  .optional()
@@ -103,11 +196,10 @@ function registerNetworkTools(server, client, readOnly = false) {
103
196
  },
104
197
  outputSchema: output_schemas_js_1.networkOutputSchema,
105
198
  annotations: safety_js_1.WRITE_NOT_IDEMPOTENT,
106
- }, async ({ siteId, name, management, enabled, vlanId, dhcpGuarding, dryRun }) => {
199
+ }, async ({ siteId, dryRun, ...fields }) => {
107
200
  try {
108
- const body = { name, management, enabled, vlanId };
109
- if (dhcpGuarding !== undefined)
110
- body.dhcpGuarding = dhcpGuarding;
201
+ const body = (0, full_put_js_1.definedEntries)(fields);
202
+ checkNetworkBody(body, body);
111
203
  if (dryRun)
112
204
  return (0, safety_js_1.formatDryRun)("POST", `/sites/${siteId}/networks`, body);
113
205
  const data = await client.post(`/sites/${siteId}/networks`, body);
@@ -118,40 +210,29 @@ function registerNetworkTools(server, client, readOnly = false) {
118
210
  }
119
211
  });
120
212
  server.registerTool("unifi_update_network", {
121
- description: "Update an existing network",
213
+ description: `Update an existing network. Pass only the fields to change. The API's PUT replaces the whole network — omitting dhcpGuarding disables DHCP guarding, and GATEWAY networks must resend their full ipv4Configuration — so this tool reads the current network, keeps every documented body field for its management type, overlays your changes and sends the full body. Pass null to clear an optional field (e.g. ipv6Configuration: null removes IPv6). Nested objects such as ipv4Configuration are replaced whole, not deep-merged: read them with unifi_get_network and send the complete object. Variants: ${NETWORK_VARIANTS_DOC} dryRun shows the merged body.`,
122
214
  inputSchema: {
123
215
  siteId: zod_1.z.string().describe("Site ID"),
124
216
  networkId: zod_1.z.string().describe("Network ID"),
125
- name: zod_1.z.string().describe("Network name"),
126
- management: zod_1.z
127
- .enum(["UNMANAGED", "GATEWAY", "SWITCH"])
128
- .describe("Network management type"),
129
- enabled: zod_1.z.boolean().describe("Enable the network"),
130
- vlanId: zod_1.z
131
- .number()
132
- .int()
133
- .min(1)
134
- .max(4009)
135
- .describe("VLAN ID (1-4009)"),
136
- dhcpGuarding: zod_1.z
137
- .record(zod_1.z.string(), zod_1.z.unknown())
138
- .optional()
139
- .describe("DHCP Guarding settings (trustedDhcpServerIpAddresses)"),
217
+ ...networkFields,
140
218
  dryRun: zod_1.z
141
219
  .boolean()
142
220
  .optional()
143
- .describe("Preview this action without executing it"),
221
+ .describe("Preview the merged PUT body without executing it (still reads the current network)"),
144
222
  },
145
223
  outputSchema: output_schemas_js_1.networkOutputSchema,
146
224
  annotations: safety_js_1.WRITE,
147
- }, async ({ siteId, networkId, name, management, enabled, vlanId, dhcpGuarding, dryRun }) => {
225
+ }, async ({ siteId, networkId, dryRun, ...fields }) => {
148
226
  try {
149
- const body = { name, management, enabled, vlanId };
150
- if (dhcpGuarding !== undefined)
151
- body.dhcpGuarding = dhcpGuarding;
227
+ const path = `/sites/${siteId}/networks/${networkId}`;
228
+ const current = (0, full_put_js_1.asObject)(await client.get(path), "the current network");
229
+ const updates = (0, full_put_js_1.definedEntries)(fields);
230
+ const { allowed } = fieldsForManagement(updates.management ?? current.management);
231
+ const body = (0, full_put_js_1.mergeForPut)(current, updates, allowed);
232
+ checkNetworkBody(body, updates);
152
233
  if (dryRun)
153
- return (0, safety_js_1.formatDryRun)("PUT", `/sites/${siteId}/networks/${networkId}`, body);
154
- const data = await client.put(`/sites/${siteId}/networks/${networkId}`, body);
234
+ return (0, safety_js_1.formatDryRun)("PUT", path, body);
235
+ const data = await client.put(path, body);
155
236
  return (0, responses_js_1.formatSuccess)(data, { structured: true });
156
237
  }
157
238
  catch (err) {
@@ -26,7 +26,7 @@ function registerSiteTools(server, client) {
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. Filterable: id, internalReference, name — eq/ne/in/notIn only (like is rejected). Example: name.eq('Default')"),
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.6.106 — 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 11.0.83 — 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.6.106 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 11.0.83 docs and no tunnels to sample live; the older 10.4.57 spec lists id, type (IPSEC/OPENVPN/WIREGUARD), name, metadata.origin/source — 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
@@ -57,7 +57,7 @@ function registerSupportingTools(server, client) {
57
57
  filter: zod_1.z
58
58
  .string()
59
59
  .optional()
60
- .describe("Filter expression"),
60
+ .describe("Filter expression, e.g. metadata.source.isNull()"),
61
61
  },
62
62
  outputSchema: output_schemas_js_1.listVpnTunnelsOutputSchema,
63
63
  annotations: safety_js_1.READ_ONLY,
@@ -72,7 +72,7 @@ function registerSupportingTools(server, client) {
72
72
  }
73
73
  });
74
74
  server.registerTool("unifi_list_vpn_servers", {
75
- description: "List VPN servers (roaming/client-access VPNs: WireGuard, OpenVPN, L2TP, Teleport) at a site. Returns: id, type (e.g. WIREGUARD, UID), name, enabled, metadata.origin.",
75
+ description: "List VPN servers (roaming/client-access VPNs) at a site. Returns: id, type (L2TP, OPENVPN, PPTP, UID, WIREGUARD), name, enabled, metadata.origin.",
76
76
  inputSchema: {
77
77
  siteId: zod_1.z.string().describe("Site ID"),
78
78
  offset: zod_1.z
@@ -91,7 +91,7 @@ function registerSupportingTools(server, client) {
91
91
  filter: zod_1.z
92
92
  .string()
93
93
  .optional()
94
- .describe("Filter expression"),
94
+ .describe("Filter expression, e.g. type.eq('WIREGUARD')"),
95
95
  },
96
96
  outputSchema: output_schemas_js_1.listVpnServersOutputSchema,
97
97
  annotations: safety_js_1.READ_ONLY,
@@ -125,7 +125,7 @@ function registerSupportingTools(server, client) {
125
125
  filter: zod_1.z
126
126
  .string()
127
127
  .optional()
128
- .describe("Filter expression"),
128
+ .describe("Filter expression, e.g. name.eq('Default')"),
129
129
  },
130
130
  outputSchema: output_schemas_js_1.listRadiusProfilesOutputSchema,
131
131
  annotations: safety_js_1.READ_ONLY,
@@ -140,7 +140,7 @@ function registerSupportingTools(server, client) {
140
140
  }
141
141
  });
142
142
  server.registerTool("unifi_list_device_tags", {
143
- description: "List device tags at a site. Tags group APs/switches for selective WiFi broadcast (via broadcastingDeviceFilter on a WiFi network). Returns: id, name, deviceIds[].",
143
+ description: "List device tags at a site. Tags group APs/switches for selective WiFi broadcast (via broadcastingDeviceFilter on a WiFi network). Returns: id, name, deviceIds[], metadata.",
144
144
  inputSchema: {
145
145
  siteId: zod_1.z.string().describe("Site ID"),
146
146
  offset: zod_1.z
@@ -192,7 +192,7 @@ function registerSupportingTools(server, client) {
192
192
  filter: zod_1.z
193
193
  .string()
194
194
  .optional()
195
- .describe("Filter expression"),
195
+ .describe("Filter expression, e.g. name.like('Stream*') or id.eq(3). NOTE: totalCount ignores the filter here — page by count"),
196
196
  },
197
197
  outputSchema: output_schemas_js_1.listDpiCategoriesOutputSchema,
198
198
  annotations: safety_js_1.READ_ONLY,
@@ -225,7 +225,7 @@ function registerSupportingTools(server, client) {
225
225
  filter: zod_1.z
226
226
  .string()
227
227
  .optional()
228
- .describe("Filter expression"),
228
+ .describe("Filter expression, e.g. name.like('Net*') or id.eq(3). NOTE: totalCount ignores the filter here — page by count"),
229
229
  },
230
230
  outputSchema: output_schemas_js_1.listDpiApplicationsOutputSchema,
231
231
  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, 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.",
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 11.0.83 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
@@ -27,7 +27,7 @@ function registerSwitchingTools(server, client) {
27
27
  filter: zod_1.z
28
28
  .string()
29
29
  .optional()
30
- .describe("Filter expression"),
30
+ .describe("Filter expression, e.g. name.like('Core*') or metadata.origin.eq('USER_DEFINED'). Device filters (members.deviceId, units.deviceId) are rejected on 11.0.83"),
31
31
  },
32
32
  outputSchema: output_schemas_js_1.listSwitchStacksOutputSchema,
33
33
  annotations: safety_js_1.READ_ONLY,
@@ -59,7 +59,7 @@ function registerSwitchingTools(server, client) {
59
59
  }
60
60
  });
61
61
  server.registerTool("unifi_list_mc_lag_domains", {
62
- description: "List MC-LAG (Multi-Chassis Link Aggregation) Domains — pairs of switches presenting as one for LAG redundancy. Returns: id, name, peers[], lags[] (LAGs spanning the domain), metadata.origin.",
62
+ description: "List MC-LAG (Multi-Chassis Link Aggregation) Domains — pairs of switches presenting as one for LAG redundancy. Returns: id, name, peers[] (per the older 10.4.57 spec: {deviceId, linkPortIdxs[], role TOP|BOTTOM}; not live-verified), lags[] (LAGs spanning the domain), metadata.origin.",
63
63
  inputSchema: {
64
64
  siteId: zod_1.z.string().describe("Site ID"),
65
65
  offset: zod_1.z
@@ -78,7 +78,7 @@ function registerSwitchingTools(server, client) {
78
78
  filter: zod_1.z
79
79
  .string()
80
80
  .optional()
81
- .describe("Filter expression"),
81
+ .describe("Filter expression, e.g. peers.deviceId.contains('<deviceId>')"),
82
82
  },
83
83
  outputSchema: output_schemas_js_1.listMcLagDomainsOutputSchema,
84
84
  annotations: safety_js_1.READ_ONLY,
@@ -110,7 +110,7 @@ function registerSwitchingTools(server, client) {
110
110
  }
111
111
  });
112
112
  server.registerTool("unifi_list_lags", {
113
- description: "List LAGs (Link Aggregation Groups — bonded switch ports) at a site. Returns: id, type (LOCAL/SWITCH_STACK/MULTI_CHASSIS), members[], metadata.origin.",
113
+ description: "List LAGs (Link Aggregation Groups — bonded switch ports) at a site. Returns: id, type (LOCAL/SWITCH_STACK/MULTI_CHASSIS), members[] ({deviceId, portIdxs[]}), switchStackId (SWITCH_STACK) or mcLagDomainId (MULTI_CHASSIS), metadata.origin.",
114
114
  inputSchema: {
115
115
  siteId: zod_1.z.string().describe("Site ID"),
116
116
  offset: zod_1.z
@@ -129,7 +129,7 @@ function registerSwitchingTools(server, client) {
129
129
  filter: zod_1.z
130
130
  .string()
131
131
  .optional()
132
- .describe("Filter expression"),
132
+ .describe("Filter expression, e.g. type.eq('LOCAL'), members.deviceId.contains('<deviceId>'), switchStackId.isNull()"),
133
133
  },
134
134
  outputSchema: output_schemas_js_1.listLagsOutputSchema,
135
135
  annotations: safety_js_1.READ_ONLY,
@@ -6,9 +6,13 @@ const responses_js_1 = require("../utils/responses.js");
6
6
  const query_js_1 = require("../utils/query.js");
7
7
  const safety_js_1 = require("../utils/safety.js");
8
8
  const output_schemas_js_1 = require("../utils/output-schemas.js");
9
+ const matchingItems = zod_1.z
10
+ .array(zod_1.z.object({ type: zod_1.z.string().describe("Item type, e.g. PORT_NUMBER, IP_ADDRESS, SUBNET") }).passthrough())
11
+ .min(1)
12
+ .describe("Non-empty array of tagged item objects, e.g. [{type:'PORT_NUMBER', value:443}]");
9
13
  function registerTrafficMatchingTools(server, client, readOnly = false) {
10
14
  server.registerTool("unifi_list_traffic_matching_lists", {
11
- description: "List traffic matching lists at a site — named collections of ports or IPs reused in firewall/ACL rules. Returns: id, type (PORTS/IPV4_ADDRESSES/IPV6_ADDRESSES), name, items[]. Use for: finding the matching-list ID to reference from a firewall policy.",
15
+ description: "List traffic matching lists at a site — named collections of ports or IPs reused in firewall/ACL rules. Returns: id, type (PORTS/IPV4_ADDRESSES/IPV6_ADDRESSES), name, items[] (tagged objects such as {type:'PORT_NUMBER', value:22} or {type:'SUBNET', value:'<cidr>'}). Use for: finding the matching-list ID to reference from a firewall policy.",
12
16
  inputSchema: {
13
17
  siteId: zod_1.z.string().describe("Site ID"),
14
18
  offset: zod_1.z
@@ -27,7 +31,7 @@ function registerTrafficMatchingTools(server, client, readOnly = false) {
27
31
  filter: zod_1.z
28
32
  .string()
29
33
  .optional()
30
- .describe("Filter expression"),
34
+ .describe("Filter expression. Only id and name are filterable (type is rejected with HTTP 400). Example: name.like('Web*'). NOTE: totalCount ignores the filter here — page by count"),
31
35
  },
32
36
  outputSchema: output_schemas_js_1.listTrafficMatchingListsOutputSchema,
33
37
  annotations: safety_js_1.READ_ONLY,
@@ -61,17 +65,14 @@ function registerTrafficMatchingTools(server, client, readOnly = false) {
61
65
  if (readOnly)
62
66
  return;
63
67
  server.registerTool("unifi_create_traffic_matching_list", {
64
- description: "Create a new traffic matching list",
68
+ description: "Create a new traffic matching list. Items are tagged objects, not strings (live-verified 11.0.83): PORTS lists take {type:'PORT_NUMBER', value:443} (the older 10.4.57 spec also lists {type:'PORT_NUMBER_RANGE', start, stop}); IPV4_ADDRESSES lists take {type:'IP_ADDRESS', value:'192.0.2.1'} or {type:'SUBNET', value:'192.0.2.0/24'} (spec also: IP_ADDRESS_RANGE {start, stop}); IPV6_ADDRESSES lists take IP_ADDRESS or SUBNET. Idempotency: not safe to retry — re-running creates a duplicate list.",
65
69
  inputSchema: {
66
70
  siteId: zod_1.z.string().describe("Site ID"),
67
71
  type: zod_1.z
68
72
  .enum(["PORTS", "IPV4_ADDRESSES", "IPV6_ADDRESSES"])
69
73
  .describe("List type"),
70
74
  name: zod_1.z.string().min(1).describe("List name"),
71
- items: zod_1.z
72
- .array(zod_1.z.unknown())
73
- .min(1)
74
- .describe("List items (ports or IP addresses)"),
75
+ items: matchingItems,
75
76
  dryRun: zod_1.z
76
77
  .boolean()
77
78
  .optional()
@@ -92,7 +93,7 @@ function registerTrafficMatchingTools(server, client, readOnly = false) {
92
93
  }
93
94
  });
94
95
  server.registerTool("unifi_update_traffic_matching_list", {
95
- description: "Update a traffic matching list",
96
+ description: "Replace a traffic matching list (full PUT — type, name and the complete items array are all required; read the current list with unifi_get_traffic_matching_list first). Items are tagged objects, not strings (live-verified 11.0.83): PORTS lists take {type:'PORT_NUMBER', value:443} (the older 10.4.57 spec also lists {type:'PORT_NUMBER_RANGE', start, stop}); IPV4_ADDRESSES lists take {type:'IP_ADDRESS', value:'192.0.2.1'} or {type:'SUBNET', value:'192.0.2.0/24'} (spec also: IP_ADDRESS_RANGE {start, stop}); IPV6_ADDRESSES lists take IP_ADDRESS or SUBNET.",
96
97
  inputSchema: {
97
98
  siteId: zod_1.z.string().describe("Site ID"),
98
99
  trafficMatchingListId: zod_1.z.string().describe("Traffic matching list ID"),
@@ -100,7 +101,7 @@ function registerTrafficMatchingTools(server, client, readOnly = false) {
100
101
  .enum(["PORTS", "IPV4_ADDRESSES", "IPV6_ADDRESSES"])
101
102
  .describe("List type"),
102
103
  name: zod_1.z.string().min(1).describe("List name"),
103
- items: zod_1.z.array(zod_1.z.unknown()).min(1).describe("List items"),
104
+ items: matchingItems,
104
105
  dryRun: zod_1.z
105
106
  .boolean()
106
107
  .optional()