@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.
- package/README.md +12 -10
- package/dist/tools/acl.js +3 -3
- package/dist/tools/client-history.js +1 -1
- package/dist/tools/clients.js +4 -4
- package/dist/tools/devices.js +9 -9
- package/dist/tools/dns-policies.js +96 -49
- package/dist/tools/firewall.js +53 -13
- package/dist/tools/networks.js +117 -36
- package/dist/tools/sites.js +1 -1
- package/dist/tools/supporting.js +9 -9
- package/dist/tools/switching.js +6 -6
- package/dist/tools/traffic-matching.js +10 -9
- package/dist/tools/wifi.js +164 -293
- package/dist/utils/full-put.d.ts +23 -0
- package/dist/utils/full-put.js +51 -0
- package/dist/utils/output-schemas.d.ts +51 -5
- package/dist/utils/output-schemas.js +26 -6
- package/dist/utils/secrets.d.ts +24 -0
- package/dist/utils/secrets.js +89 -0
- package/package.json +6 -6
package/README.md
CHANGED
|
@@ -76,12 +76,14 @@ This server provides layered safety controls for responsible operation:
|
|
|
76
76
|
- **Destructive tool warnings** — Tools that delete or irreversibly modify resources have descriptions prefixed with `DESTRUCTIVE:` to clearly signal risk
|
|
77
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
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
|
|
79
81
|
|
|
80
82
|
## Structured Output
|
|
81
83
|
|
|
82
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.
|
|
83
85
|
|
|
84
|
-
The schemas live in `src/utils/output-schemas.ts` and are verified against UniFi Network API
|
|
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.
|
|
85
87
|
|
|
86
88
|
## Tools (76 total)
|
|
87
89
|
|
|
@@ -127,17 +129,17 @@ The schemas live in `src/utils/output-schemas.ts` and are verified against UniFi
|
|
|
127
129
|
| `unifi_list_networks` | List all networks at a site |
|
|
128
130
|
| `unifi_get_network` | Get a specific network by ID |
|
|
129
131
|
| `unifi_get_network_references` | Get references to a network (WiFi, firewall zones, etc.) |
|
|
130
|
-
| `unifi_create_network` | Create a new network |
|
|
131
|
-
| `unifi_update_network` | Update an existing network |
|
|
132
|
+
| `unifi_create_network` | Create a new network (UNMANAGED, GATEWAY or SWITCH body) |
|
|
133
|
+
| `unifi_update_network` | Update an existing network — partial changes are merged into the full PUT body |
|
|
132
134
|
| `unifi_delete_network` | **DESTRUCTIVE:** Delete a network — disconnects all clients |
|
|
133
135
|
|
|
134
136
|
### WiFi (5)
|
|
135
137
|
| Tool | Description |
|
|
136
138
|
|---|---|
|
|
137
|
-
| `unifi_list_wifi` | List all WiFi broadcasts (SSIDs) at a site |
|
|
138
|
-
| `unifi_get_wifi` | Get a specific WiFi network by ID |
|
|
139
|
+
| `unifi_list_wifi` | List all WiFi broadcasts (SSIDs) at a site (passphrases redacted by default) |
|
|
140
|
+
| `unifi_get_wifi` | Get a specific WiFi network by ID (passphrases redacted unless `includeSecrets: true`) |
|
|
139
141
|
| `unifi_create_wifi` | Create a new WiFi network (SSID) |
|
|
140
|
-
| `unifi_update_wifi` | Update an existing WiFi network |
|
|
142
|
+
| `unifi_update_wifi` | Update an existing WiFi network — partial changes are merged into the full PUT body |
|
|
141
143
|
| `unifi_delete_wifi` | **DESTRUCTIVE:** Delete a WiFi network — disconnects all clients |
|
|
142
144
|
|
|
143
145
|
### Hotspot Vouchers (5)
|
|
@@ -160,8 +162,8 @@ The schemas live in `src/utils/output-schemas.ts` and are verified against UniFi
|
|
|
160
162
|
| `unifi_list_firewall_policies` | List all firewall policies at a site |
|
|
161
163
|
| `unifi_get_firewall_policy` | Get a specific firewall policy by ID |
|
|
162
164
|
| `unifi_create_firewall_policy` | Create a new firewall policy |
|
|
163
|
-
| `unifi_update_firewall_policy` | Update a firewall policy |
|
|
164
|
-
| `unifi_patch_firewall_policy` | Partially update a firewall policy (
|
|
165
|
+
| `unifi_update_firewall_policy` | Update a firewall policy — partial changes are merged into the full PUT body |
|
|
166
|
+
| `unifi_patch_firewall_policy` | Partially update a firewall policy (toggle logging or Insights flow reporting) |
|
|
165
167
|
| `unifi_delete_firewall_policy` | **DESTRUCTIVE:** Delete a firewall policy |
|
|
166
168
|
| `unifi_get_firewall_policy_ordering` | Get user-defined firewall policy ordering for a zone pair |
|
|
167
169
|
| `unifi_reorder_firewall_policies` | Reorder user-defined firewall policies for a zone pair |
|
|
@@ -192,8 +194,8 @@ The schemas live in `src/utils/output-schemas.ts` and are verified against UniFi
|
|
|
192
194
|
|---|---|
|
|
193
195
|
| `unifi_list_dns_policies` | List all DNS policies at a site |
|
|
194
196
|
| `unifi_get_dns_policy` | Get a specific DNS policy by ID |
|
|
195
|
-
| `unifi_create_dns_policy` | Create a
|
|
196
|
-
| `unifi_update_dns_policy` | Update a DNS policy |
|
|
197
|
+
| `unifi_create_dns_policy` | Create a DNS policy of any type (A, AAAA, CNAME, MX, TXT, SRV, FORWARD_DOMAIN) |
|
|
198
|
+
| `unifi_update_dns_policy` | Update a DNS policy — partial changes are merged into the full PUT body |
|
|
197
199
|
| `unifi_delete_dns_policy` | **DESTRUCTIVE:** Delete a DNS policy |
|
|
198
200
|
|
|
199
201
|
### Traffic Matching (5)
|
package/dist/tools/acl.js
CHANGED
|
@@ -59,7 +59,7 @@ function registerAclTools(server, client, readOnly = false) {
|
|
|
59
59
|
}
|
|
60
60
|
});
|
|
61
61
|
server.registerTool("unifi_get_acl_rule_ordering", {
|
|
62
|
-
description: "Get the evaluation order of user-defined ACL rules. Returns: orderedAclRuleIds[]
|
|
62
|
+
description: "Get the evaluation order of user-defined ACL rules. Returns: orderedAclRuleIds[] — rules are evaluated in list order.",
|
|
63
63
|
inputSchema: {
|
|
64
64
|
siteId: zod_1.z.string().describe("Site ID"),
|
|
65
65
|
},
|
|
@@ -103,7 +103,7 @@ function registerAclTools(server, client, readOnly = false) {
|
|
|
103
103
|
enforcingDeviceFilter: zod_1.z
|
|
104
104
|
.unknown()
|
|
105
105
|
.optional()
|
|
106
|
-
.describe("
|
|
106
|
+
.describe("Device filter object (with a type discriminator) selecting the switch-capable devices that enforce the rule. When null/omitted, the rule is provisioned to all switches on the site"),
|
|
107
107
|
dryRun: zod_1.z
|
|
108
108
|
.boolean()
|
|
109
109
|
.optional()
|
|
@@ -161,7 +161,7 @@ function registerAclTools(server, client, readOnly = false) {
|
|
|
161
161
|
enforcingDeviceFilter: zod_1.z
|
|
162
162
|
.unknown()
|
|
163
163
|
.optional()
|
|
164
|
-
.describe("
|
|
164
|
+
.describe("Device filter object (with a type discriminator) selecting the switch-capable devices that enforce the rule. When null/omitted, the rule is provisioned to all switches on the site"),
|
|
165
165
|
dryRun: zod_1.z
|
|
166
166
|
.boolean()
|
|
167
167
|
.optional()
|
|
@@ -9,7 +9,7 @@ const siteReference = zod_1.z.string().regex(/^[A-Za-z0-9_-]{1,64}$/)
|
|
|
9
9
|
.describe("Site internalReference from unifi_list_sites (often default), not the Integration API UUID");
|
|
10
10
|
function registerClientHistoryTools(server, client) {
|
|
11
11
|
server.registerTool("unifi_list_client_history", {
|
|
12
|
-
description: "List historical/offline client inventory using the controller v2 API. Includes names, MACs, first_seen and last_seen epoch seconds when available. This is inventory, not session history; use unifi_list_client_sessions for past connections. Requires controller history API-key support; errors never mean zero activity. Retention depends on the controller.",
|
|
12
|
+
description: "List historical/offline client inventory using the controller v2 API. Returns OFFLINE clients only — currently connected clients are excluded, so merge with unifi_list_clients for a full inventory. Includes names, MACs, first_seen and last_seen epoch seconds when available. This is inventory, not session history; use unifi_list_client_sessions for past connections. Requires controller history API-key support; errors never mean zero activity. Retention depends on the controller.",
|
|
13
13
|
inputSchema: { siteReference, withinHours: zod_1.z.number().int().min(1).max(8760).default(168).describe("Lookback in hours, up to one year; does not extend controller retention") },
|
|
14
14
|
outputSchema: output_schemas_js_1.listClientHistoryOutputSchema,
|
|
15
15
|
annotations: safety_js_1.READ_ONLY,
|
package/dist/tools/clients.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 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 (
|
|
11
|
+
description: "List currently connected clients at a site. Returns per client: id, name, type (WIRED/WIRELESS/VPN/TELEPORT), macAddress, ipAddress (may be absent), connectedAt, uplinkDeviceId (the switch/AP they're attached to), access.type. NOTE: verified against 11.0.83 (WIRED/WIRELESS; VPN/TELEPORT not live-verified) — the Integration API client schema is minimal and identical across those 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; use unifi_list_client_history and unifi_list_client_sessions for controller-retained records.",
|
|
12
12
|
inputSchema: {
|
|
13
13
|
siteId: zod_1.z.string().describe("Site ID"),
|
|
14
14
|
offset: zod_1.z
|
|
@@ -24,7 +24,7 @@ function registerClientTools(server, client, readOnly = false) {
|
|
|
24
24
|
.max(200)
|
|
25
25
|
.optional()
|
|
26
26
|
.describe("Number of records to return (default: 25, max: 200)"),
|
|
27
|
-
filter: zod_1.z.string().optional().describe("Filter expression"),
|
|
27
|
+
filter: zod_1.z.string().optional().describe("Filter expression. Filterable: id, type, macAddress, ipAddress, connectedAt (timestamp), access.type, access.authorized (boolean). NOT filterable: name, uplinkDeviceId. Example: type.eq('WIRELESS')"),
|
|
28
28
|
},
|
|
29
29
|
outputSchema: output_schemas_js_1.listClientsOutputSchema,
|
|
30
30
|
annotations: safety_js_1.READ_ONLY,
|
|
@@ -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. Returns (per
|
|
61
|
+
description: "Authorize a guest client through a captive-portal hotspot. Optional limits override hotspot defaults. Returns (per 11.0.83 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. Returns (per
|
|
119
|
+
description: "Revoke guest authorization; the client returns to the captive portal on next request. Returns (per docs; the UNAUTHORIZE response sample is not rendered in 11.0.83 and 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"),
|
package/dist/tools/devices.js
CHANGED
|
@@ -26,7 +26,7 @@ function registerDeviceTools(server, client, readOnly = false) {
|
|
|
26
26
|
.max(200)
|
|
27
27
|
.optional()
|
|
28
28
|
.describe("Number of records to return (default: 25, max: 200)"),
|
|
29
|
-
filter: zod_1.z.string().optional().describe("Filter expression"),
|
|
29
|
+
filter: zod_1.z.string().optional().describe("Filter expression. Filterable: name (supports like), model, state, macAddress, ipAddress, firmwareVersion, supported, firmwareUpdatable, features/interfaces (sets: contains/containsAny/containsAll). like is rejected on model. Example: features.contains('accessPoint')"),
|
|
30
30
|
},
|
|
31
31
|
outputSchema: output_schemas_js_1.listDevicesOutputSchema,
|
|
32
32
|
annotations: safety_js_1.READ_ONLY,
|
|
@@ -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):
|
|
44
|
+
description: "Get full configuration for a device. Returns (in addition to list fields): adoptedAt, provisionedAt, configurationId, uplink.deviceId (absent on the ISP modem), features (object keyed by capability: switching {lags[]} / accessPoint {}), interfaces.ports[] for switches, gateways and modems ({idx, state, connector, maxSpeedMbps, speedMbps (may be omitted on DOWN ports), poe:{standard, type, enabled, state} on PoE ports only}), interfaces.radios[] for APs ({wlanStandard, frequencyGHz, channelWidthMHz, channel}). Some devices (e.g. 5G modems) return empty features/interfaces objects. 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
|
|
61
|
+
description: "Get latest live statistics for a device. Returns: uptimeSec, lastHeartbeatAt, nextHeartbeatAt, loadAverage1/5/15Min, cpuUtilizationPct, memoryUtilizationPct, uplink (txRateBps, rxRateBps; absent on e.g. the ISP modem), interfaces.radios[] for APs ({frequencyGHz, txRetriesPct}; interfaces is {} for non-AP devices). NOTE: verified against 11.0.83 — 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
|
|
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 11.0.83 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()
|
|
@@ -90,7 +90,7 @@ function registerDeviceTools(server, client, readOnly = false) {
|
|
|
90
90
|
.max(200)
|
|
91
91
|
.optional()
|
|
92
92
|
.describe("Number of records to return (default: 25, max: 200)"),
|
|
93
|
-
filter: zod_1.z.string().optional().describe("Filter expression"),
|
|
93
|
+
filter: zod_1.z.string().optional().describe("Filter expression, e.g. model.eq('U7 Pro')"),
|
|
94
94
|
},
|
|
95
95
|
outputSchema: output_schemas_js_1.listPendingDevicesOutputSchema,
|
|
96
96
|
annotations: safety_js_1.READ_ONLY,
|
|
@@ -157,13 +157,13 @@ function registerDeviceTools(server, client, readOnly = false) {
|
|
|
157
157
|
}
|
|
158
158
|
});
|
|
159
159
|
server.registerTool("unifi_restart_device", {
|
|
160
|
-
description: "Restart (reboot) a device. The device will be unreachable for ~1–3 minutes.
|
|
160
|
+
description: "Restart (reboot) a device. The device will be unreachable for ~1–3 minutes. Not idempotent: each call triggers a fresh reboot.",
|
|
161
161
|
inputSchema: {
|
|
162
162
|
siteId: zod_1.z.string().describe("Site ID"),
|
|
163
163
|
deviceId: zod_1.z.string().describe("Device ID"),
|
|
164
164
|
dryRun: zod_1.z.boolean().optional().describe("Preview this action without executing it"),
|
|
165
165
|
},
|
|
166
|
-
annotations: safety_js_1.
|
|
166
|
+
annotations: safety_js_1.WRITE_NOT_IDEMPOTENT,
|
|
167
167
|
}, async ({ siteId, deviceId, dryRun }) => {
|
|
168
168
|
const body = { action: "RESTART" };
|
|
169
169
|
if (dryRun)
|
|
@@ -181,10 +181,10 @@ function registerDeviceTools(server, client, readOnly = false) {
|
|
|
181
181
|
inputSchema: {
|
|
182
182
|
siteId: zod_1.z.string().describe("Site ID"),
|
|
183
183
|
deviceId: zod_1.z.string().describe("Device ID"),
|
|
184
|
-
portIdx: zod_1.z.number().int().describe("Port index number"),
|
|
184
|
+
portIdx: zod_1.z.number().int().min(1).describe("Port index number (1-based)"),
|
|
185
185
|
dryRun: zod_1.z.boolean().optional().describe("Preview this action without executing it"),
|
|
186
186
|
},
|
|
187
|
-
annotations: safety_js_1.
|
|
187
|
+
annotations: safety_js_1.WRITE_NOT_IDEMPOTENT,
|
|
188
188
|
}, async ({ siteId, deviceId, portIdx, dryRun }) => {
|
|
189
189
|
const body = { action: "POWER_CYCLE" };
|
|
190
190
|
if (dryRun)
|
|
@@ -5,10 +5,83 @@ 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
|
+
// Type-specific request-body fields, from the 11.0.83 POST/PUT schemas (every
|
|
11
|
+
// type also takes type, enabled, domain). MX/TXT/SRV/FORWARD_DOMAIN have no TTL.
|
|
12
|
+
const DNS_TYPE_FIELDS = {
|
|
13
|
+
A_RECORD: ["ipv4Address", "ttlSeconds"],
|
|
14
|
+
AAAA_RECORD: ["ipv6Address", "ttlSeconds"],
|
|
15
|
+
CNAME_RECORD: ["targetDomain", "ttlSeconds"],
|
|
16
|
+
MX_RECORD: ["mailServerDomain", "priority"],
|
|
17
|
+
TXT_RECORD: ["text"],
|
|
18
|
+
SRV_RECORD: ["serverDomain", "service", "protocol", "port", "priority", "weight"],
|
|
19
|
+
FORWARD_DOMAIN: ["ipAddress"],
|
|
20
|
+
};
|
|
21
|
+
const DNS_TYPES_DOC = "A_RECORD: ipv4Address, ttlSeconds (0-86400). AAAA_RECORD: ipv6Address, ttlSeconds (0-86400). CNAME_RECORD: targetDomain, ttlSeconds (0-604800). MX_RECORD: mailServerDomain, priority. TXT_RECORD: text. SRV_RECORD: serverDomain, service (e.g. _ldap), protocol (e.g. _tcp), port, priority, weight. FORWARD_DOMAIN: ipAddress (upstream DNS server). All type-specific fields are required for their type.";
|
|
22
|
+
const dnsType = zod_1.z
|
|
23
|
+
.enum([
|
|
24
|
+
"A_RECORD",
|
|
25
|
+
"AAAA_RECORD",
|
|
26
|
+
"CNAME_RECORD",
|
|
27
|
+
"MX_RECORD",
|
|
28
|
+
"TXT_RECORD",
|
|
29
|
+
"SRV_RECORD",
|
|
30
|
+
"FORWARD_DOMAIN",
|
|
31
|
+
])
|
|
32
|
+
.describe("DNS record type");
|
|
33
|
+
function fieldsForDnsType(type) {
|
|
34
|
+
if (typeof type !== "string" || !(type in DNS_TYPE_FIELDS)) {
|
|
35
|
+
throw new Error(`Unsupported DNS policy type: ${String(type)}`);
|
|
36
|
+
}
|
|
37
|
+
const specific = DNS_TYPE_FIELDS[type];
|
|
38
|
+
return {
|
|
39
|
+
allowed: ["type", "enabled", "domain", ...specific],
|
|
40
|
+
required: ["type", "enabled", "domain", ...specific],
|
|
41
|
+
};
|
|
42
|
+
}
|
|
43
|
+
function checkDnsBody(body, updates) {
|
|
44
|
+
const { allowed, required } = fieldsForDnsType(body.type);
|
|
45
|
+
const rejected = (0, full_put_js_1.unsupportedFields)(updates, allowed);
|
|
46
|
+
if (rejected.length > 0) {
|
|
47
|
+
throw new Error(`Fields not accepted for type ${String(body.type)}: ${rejected.join(", ")}`);
|
|
48
|
+
}
|
|
49
|
+
const missing = (0, full_put_js_1.missingFields)(body, required);
|
|
50
|
+
if (missing.length > 0) {
|
|
51
|
+
throw new Error(`Missing required fields for type ${String(body.type)}: ${missing.join(", ")}`);
|
|
52
|
+
}
|
|
53
|
+
if ((body.type === "A_RECORD" || body.type === "AAAA_RECORD") &&
|
|
54
|
+
typeof body.ttlSeconds === "number" &&
|
|
55
|
+
body.ttlSeconds > 86400) {
|
|
56
|
+
throw new Error(`ttlSeconds for ${body.type} must be 0-86400 (604800 is the CNAME_RECORD maximum)`);
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
const uint16 = (description) => zod_1.z.number().int().min(0).max(65535).optional().describe(description);
|
|
60
|
+
const hostname = (description) => zod_1.z.string().min(1).max(127).optional().describe(description);
|
|
61
|
+
const dnsFields = {
|
|
62
|
+
ipv4Address: zod_1.z.string().optional().describe("IPv4 address. A_RECORD."),
|
|
63
|
+
ipv6Address: zod_1.z.string().optional().describe("IPv6 address. AAAA_RECORD."),
|
|
64
|
+
targetDomain: hostname("Canonical name the alias points at. CNAME_RECORD."),
|
|
65
|
+
mailServerDomain: hostname("Mail server domain. MX_RECORD."),
|
|
66
|
+
text: zod_1.z.string().min(1).max(1024).optional().describe("TXT value: up to four 255-character strings; lines containing commas must be double-quoted. TXT_RECORD."),
|
|
67
|
+
serverDomain: hostname("Domain of the server running the service. SRV_RECORD."),
|
|
68
|
+
service: zod_1.z.string().optional().describe("Service, e.g. _ldap. SRV_RECORD."),
|
|
69
|
+
protocol: zod_1.z.string().optional().describe("Protocol, e.g. _tcp. SRV_RECORD."),
|
|
70
|
+
port: uint16("Service port. SRV_RECORD."),
|
|
71
|
+
priority: uint16("Priority; lower is preferred. MX_RECORD, SRV_RECORD."),
|
|
72
|
+
weight: uint16("Relative weight among records with the same priority. SRV_RECORD."),
|
|
73
|
+
ipAddress: zod_1.z.string().optional().describe("IP address of the DNS server queries are forwarded to. FORWARD_DOMAIN."),
|
|
74
|
+
ttlSeconds: zod_1.z
|
|
75
|
+
.number()
|
|
76
|
+
.int()
|
|
77
|
+
.min(0)
|
|
78
|
+
.max(604800)
|
|
79
|
+
.optional()
|
|
80
|
+
.describe("Time to live in seconds: 0-86400 for A/AAAA_RECORD, 0-604800 for CNAME_RECORD"),
|
|
81
|
+
};
|
|
9
82
|
function registerDnsPolicyTools(server, client, readOnly = false) {
|
|
10
83
|
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,
|
|
84
|
+
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, metadata.origin, plus type-specific fields: A ipv4Address+ttlSeconds; AAAA ipv6Address+ttlSeconds; CNAME targetDomain+ttlSeconds; MX mailServerDomain+priority; TXT text; SRV serverDomain/service/protocol/port/priority/weight; FORWARD_DOMAIN ipAddress (per 11.0.83 response samples).",
|
|
12
85
|
inputSchema: {
|
|
13
86
|
siteId: zod_1.z.string().describe("Site ID"),
|
|
14
87
|
offset: zod_1.z
|
|
@@ -27,7 +100,7 @@ function registerDnsPolicyTools(server, client, readOnly = false) {
|
|
|
27
100
|
filter: zod_1.z
|
|
28
101
|
.string()
|
|
29
102
|
.optional()
|
|
30
|
-
.describe("Filter expression"),
|
|
103
|
+
.describe("Filter expression, e.g. type.eq('A_RECORD')"),
|
|
31
104
|
},
|
|
32
105
|
outputSchema: output_schemas_js_1.listDnsPoliciesOutputSchema,
|
|
33
106
|
annotations: safety_js_1.READ_ONLY,
|
|
@@ -61,29 +134,13 @@ function registerDnsPolicyTools(server, client, readOnly = false) {
|
|
|
61
134
|
if (readOnly)
|
|
62
135
|
return;
|
|
63
136
|
server.registerTool("unifi_create_dns_policy", {
|
|
64
|
-
description:
|
|
137
|
+
description: `Create a new DNS policy. Every type takes type, enabled, domain; the rest depends on type (per 11.0.83 docs): ${DNS_TYPES_DOC} Fields belonging to other types are rejected. Idempotency: not safe to retry — re-running creates a duplicate record.`,
|
|
65
138
|
inputSchema: {
|
|
66
139
|
siteId: zod_1.z.string().describe("Site ID"),
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
"A_RECORD",
|
|
70
|
-
"AAAA_RECORD",
|
|
71
|
-
"CNAME_RECORD",
|
|
72
|
-
"MX_RECORD",
|
|
73
|
-
"TXT_RECORD",
|
|
74
|
-
"SRV_RECORD",
|
|
75
|
-
"FORWARD_DOMAIN",
|
|
76
|
-
])
|
|
77
|
-
.describe("DNS record type"),
|
|
140
|
+
...dnsFields,
|
|
141
|
+
type: dnsType,
|
|
78
142
|
enabled: zod_1.z.boolean().describe("Whether the DNS policy is enabled"),
|
|
79
143
|
domain: zod_1.z.string().min(1).max(127).describe("Domain name"),
|
|
80
|
-
ipv4Address: zod_1.z.string().describe("IPv4 address for the record"),
|
|
81
|
-
ttlSeconds: zod_1.z
|
|
82
|
-
.number()
|
|
83
|
-
.int()
|
|
84
|
-
.min(0)
|
|
85
|
-
.max(86400)
|
|
86
|
-
.describe("Time to live in seconds"),
|
|
87
144
|
dryRun: zod_1.z
|
|
88
145
|
.boolean()
|
|
89
146
|
.optional()
|
|
@@ -91,9 +148,10 @@ function registerDnsPolicyTools(server, client, readOnly = false) {
|
|
|
91
148
|
},
|
|
92
149
|
outputSchema: output_schemas_js_1.dnsPolicyOutputSchema,
|
|
93
150
|
annotations: safety_js_1.WRITE_NOT_IDEMPOTENT,
|
|
94
|
-
}, async ({ siteId,
|
|
151
|
+
}, async ({ siteId, dryRun, ...fields }) => {
|
|
95
152
|
try {
|
|
96
|
-
const body =
|
|
153
|
+
const body = (0, full_put_js_1.definedEntries)(fields);
|
|
154
|
+
checkDnsBody(body, body);
|
|
97
155
|
if (dryRun)
|
|
98
156
|
return (0, safety_js_1.formatDryRun)("POST", `/sites/${siteId}/dns/policies`, body);
|
|
99
157
|
const data = await client.post(`/sites/${siteId}/dns/policies`, body);
|
|
@@ -104,43 +162,32 @@ function registerDnsPolicyTools(server, client, readOnly = false) {
|
|
|
104
162
|
}
|
|
105
163
|
});
|
|
106
164
|
server.registerTool("unifi_update_dns_policy", {
|
|
107
|
-
description:
|
|
165
|
+
description: `Update a DNS policy. Pass only the fields to change. The API's PUT replaces the whole policy, so this tool reads the current policy, keeps the documented fields for the (possibly new) type, overlays your changes and sends the full body. Changing type drops the old type's fields, so supply the new type's required fields: ${DNS_TYPES_DOC} dryRun shows the merged body.`,
|
|
108
166
|
inputSchema: {
|
|
109
167
|
siteId: zod_1.z.string().describe("Site ID"),
|
|
110
168
|
dnsPolicyId: zod_1.z.string().describe("DNS policy ID"),
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
"CNAME_RECORD",
|
|
116
|
-
"MX_RECORD",
|
|
117
|
-
"TXT_RECORD",
|
|
118
|
-
"SRV_RECORD",
|
|
119
|
-
"FORWARD_DOMAIN",
|
|
120
|
-
])
|
|
121
|
-
.describe("DNS record type"),
|
|
122
|
-
enabled: zod_1.z.boolean().describe("Whether the DNS policy is enabled"),
|
|
123
|
-
domain: zod_1.z.string().min(1).max(127).describe("Domain name"),
|
|
124
|
-
ipv4Address: zod_1.z.string().describe("IPv4 address for the record"),
|
|
125
|
-
ttlSeconds: zod_1.z
|
|
126
|
-
.number()
|
|
127
|
-
.int()
|
|
128
|
-
.min(0)
|
|
129
|
-
.max(86400)
|
|
130
|
-
.describe("Time to live in seconds"),
|
|
169
|
+
...dnsFields,
|
|
170
|
+
type: dnsType.optional(),
|
|
171
|
+
enabled: zod_1.z.boolean().optional().describe("Whether the DNS policy is enabled"),
|
|
172
|
+
domain: zod_1.z.string().min(1).max(127).optional().describe("Domain name"),
|
|
131
173
|
dryRun: zod_1.z
|
|
132
174
|
.boolean()
|
|
133
175
|
.optional()
|
|
134
|
-
.describe("Preview
|
|
176
|
+
.describe("Preview the merged PUT body without executing it (still reads the current policy)"),
|
|
135
177
|
},
|
|
136
178
|
outputSchema: output_schemas_js_1.dnsPolicyOutputSchema,
|
|
137
179
|
annotations: safety_js_1.WRITE,
|
|
138
|
-
}, async ({ siteId, dnsPolicyId,
|
|
180
|
+
}, async ({ siteId, dnsPolicyId, dryRun, ...fields }) => {
|
|
139
181
|
try {
|
|
140
|
-
const
|
|
182
|
+
const path = `/sites/${siteId}/dns/policies/${dnsPolicyId}`;
|
|
183
|
+
const current = (0, full_put_js_1.asObject)(await client.get(path), "the current DNS policy");
|
|
184
|
+
const updates = (0, full_put_js_1.definedEntries)(fields);
|
|
185
|
+
const { allowed } = fieldsForDnsType(updates.type ?? current.type);
|
|
186
|
+
const body = (0, full_put_js_1.mergeForPut)(current, updates, allowed);
|
|
187
|
+
checkDnsBody(body, updates);
|
|
141
188
|
if (dryRun)
|
|
142
|
-
return (0, safety_js_1.formatDryRun)("PUT",
|
|
143
|
-
const data = await client.put(
|
|
189
|
+
return (0, safety_js_1.formatDryRun)("PUT", path, body);
|
|
190
|
+
const data = await client.put(path, body);
|
|
144
191
|
return (0, responses_js_1.formatSuccess)(data, { structured: true });
|
|
145
192
|
}
|
|
146
193
|
catch (err) {
|
package/dist/tools/firewall.js
CHANGED
|
@@ -5,7 +5,35 @@ 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
|
+
// Firewall policy request-body fields, from the 11.0.83 POST/PUT schemas.
|
|
11
|
+
const FIREWALL_POLICY_FIELDS = [
|
|
12
|
+
"enabled",
|
|
13
|
+
"name",
|
|
14
|
+
"description",
|
|
15
|
+
"action",
|
|
16
|
+
"source",
|
|
17
|
+
"destination",
|
|
18
|
+
"ipProtocolScope",
|
|
19
|
+
"connectionStateFilter",
|
|
20
|
+
"ipsecFilter",
|
|
21
|
+
"loggingEnabled",
|
|
22
|
+
"reportFlowEnabled",
|
|
23
|
+
"schedule",
|
|
24
|
+
];
|
|
25
|
+
const FIREWALL_POLICY_REQUIRED = [
|
|
26
|
+
"enabled",
|
|
27
|
+
"name",
|
|
28
|
+
"action",
|
|
29
|
+
"source",
|
|
30
|
+
"destination",
|
|
31
|
+
"ipProtocolScope",
|
|
32
|
+
"loggingEnabled",
|
|
33
|
+
];
|
|
34
|
+
// Present in responses but not part of the request body.
|
|
35
|
+
const FIREWALL_POLICY_READ_ONLY = ["id", "index", "metadata"];
|
|
36
|
+
const FIREWALL_POLICY_BODY_DOC = "required enabled, name (non-empty), action ({type: ALLOW|BLOCK, allowReturnTraffic? on ALLOW}), source and destination ({zoneId, trafficFilter?}), ipProtocolScope ({ipVersion, protocolFilter?}), loggingEnabled; optional description, connectionStateFilter (array of NEW/INVALID/ESTABLISHED/RELATED), ipsecFilter (MATCH_ENCRYPTED/MATCH_NOT_ENCRYPTED), schedule (null = always active), reportFlowEnabled (report matches in Insights Flows; defaults to true when omitted).";
|
|
9
37
|
function registerFirewallTools(server, client, readOnly = false) {
|
|
10
38
|
server.registerTool("unifi_list_firewall_zones", {
|
|
11
39
|
description: "List firewall zones (groupings of networks for zone-based firewalling) at a site. Returns: id, name, networkIds[], metadata.origin (indicates system-defined vs user-defined). Use for: zone inventory; pair with unifi_list_firewall_policies to see rules between zones.",
|
|
@@ -27,7 +55,7 @@ function registerFirewallTools(server, client, readOnly = false) {
|
|
|
27
55
|
filter: zod_1.z
|
|
28
56
|
.string()
|
|
29
57
|
.optional()
|
|
30
|
-
.describe("Filter expression"),
|
|
58
|
+
.describe("Filter expression, e.g. name.eq('Internal'). NOTE: totalCount ignores the filter on this endpoint (observed 11.0.83) — page by count"),
|
|
31
59
|
},
|
|
32
60
|
outputSchema: output_schemas_js_1.listFirewallZonesOutputSchema,
|
|
33
61
|
annotations: safety_js_1.READ_ONLY,
|
|
@@ -59,7 +87,7 @@ function registerFirewallTools(server, client, readOnly = false) {
|
|
|
59
87
|
}
|
|
60
88
|
});
|
|
61
89
|
server.registerTool("unifi_list_firewall_policies", {
|
|
62
|
-
description: "List firewall policies (zone-based rules) at a site. Returns: id, name, enabled, action (object with type field), source/destination (zone reference + trafficFilter), ipProtocolScope, connectionStateFilter, ipsecFilter, schedule, loggingEnabled, index, description, metadata
|
|
90
|
+
description: "List firewall policies (zone-based rules) at a site. Returns: id, name, enabled, action (object with type field), source/destination (zone reference + trafficFilter), ipProtocolScope, connectionStateFilter, ipsecFilter, schedule, loggingEnabled, reportFlowEnabled (Insights Flows reporting; false on the predefined Allow Return Traffic and Block policies), index, description, metadata (origin SYSTEM_DEFINED/USER_DEFINED/DERIVED, configurable on system policies). action is {type: ALLOW|BLOCK, allowReturnTraffic on ALLOW}. The protocol lives in ipProtocolScope.protocolFilter (NAMED_PROTOCOL or PRESET); ports and addresses live in source/destination.trafficFilter, which is omitted when a side matches the whole zone. Unset optional fields (ipsecFilter, schedule, connectionStateFilter) are omitted, not null. Evaluation order within a zone pair comes from unifi_get_firewall_policy_ordering.",
|
|
63
91
|
inputSchema: {
|
|
64
92
|
siteId: zod_1.z.string().describe("Site ID"),
|
|
65
93
|
offset: zod_1.z
|
|
@@ -78,7 +106,7 @@ function registerFirewallTools(server, client, readOnly = false) {
|
|
|
78
106
|
filter: zod_1.z
|
|
79
107
|
.string()
|
|
80
108
|
.optional()
|
|
81
|
-
.describe("Filter expression"),
|
|
109
|
+
.describe("Filter expression, e.g. metadata.origin.eq('USER_DEFINED') or name.eq('Allow Return Traffic'). Boolean fields (enabled, loggingEnabled, reportFlowEnabled) are not filterable"),
|
|
82
110
|
},
|
|
83
111
|
outputSchema: output_schemas_js_1.listFirewallPoliciesOutputSchema,
|
|
84
112
|
annotations: safety_js_1.READ_ONLY,
|
|
@@ -93,7 +121,7 @@ function registerFirewallTools(server, client, readOnly = false) {
|
|
|
93
121
|
}
|
|
94
122
|
});
|
|
95
123
|
server.registerTool("unifi_get_firewall_policy", {
|
|
96
|
-
description: "Get a firewall policy by ID with full match criteria and action.",
|
|
124
|
+
description: "Get a firewall policy by ID with full match criteria and action. Returns the same fields as a unifi_list_firewall_policies entry, including loggingEnabled and reportFlowEnabled.",
|
|
97
125
|
inputSchema: {
|
|
98
126
|
siteId: zod_1.z.string().describe("Site ID"),
|
|
99
127
|
firewallPolicyId: zod_1.z.string().describe("Firewall policy ID"),
|
|
@@ -110,7 +138,7 @@ function registerFirewallTools(server, client, readOnly = false) {
|
|
|
110
138
|
}
|
|
111
139
|
});
|
|
112
140
|
server.registerTool("unifi_get_firewall_policy_ordering", {
|
|
113
|
-
description: "Get the evaluation order of user-defined firewall policies for a specific (source zone, destination zone) pair. Returns: beforeSystemDefined[] and afterSystemDefined[] arrays of policy IDs. System-defined rules sit between these two arrays.",
|
|
141
|
+
description: "Get the evaluation order of user-defined firewall policies for a specific (source zone, destination zone) pair. Returns: orderedFirewallPolicyIds.beforeSystemDefined[] and orderedFirewallPolicyIds.afterSystemDefined[] arrays of policy IDs. System-defined rules sit between these two arrays.",
|
|
114
142
|
inputSchema: {
|
|
115
143
|
siteId: zod_1.z.string().describe("Site ID"),
|
|
116
144
|
sourceFirewallZoneId: zod_1.z
|
|
@@ -219,7 +247,7 @@ function registerFirewallTools(server, client, readOnly = false) {
|
|
|
219
247
|
}
|
|
220
248
|
});
|
|
221
249
|
server.registerTool("unifi_create_firewall_policy", {
|
|
222
|
-
description:
|
|
250
|
+
description: `Create a new firewall policy. Body fields (per 11.0.83 docs): ${FIREWALL_POLICY_BODY_DOC} Copy nested shapes (trafficFilter, protocolFilter) from an existing policy via unifi_get_firewall_policy. Idempotency: not safe to retry — re-running creates a duplicate policy.`,
|
|
223
251
|
inputSchema: {
|
|
224
252
|
siteId: zod_1.z.string().describe("Site ID"),
|
|
225
253
|
policy: zod_1.z
|
|
@@ -244,25 +272,37 @@ function registerFirewallTools(server, client, readOnly = false) {
|
|
|
244
272
|
}
|
|
245
273
|
});
|
|
246
274
|
server.registerTool("unifi_update_firewall_policy", {
|
|
247
|
-
description:
|
|
275
|
+
description: `Update a firewall policy. Pass only the fields to change in policy. The API's PUT replaces the whole policy — and an omitted reportFlowEnabled reverts to true — so this tool reads the current policy, keeps every documented body field, overlays your fields and sends the full body. Top-level fields are replaced whole (e.g. send the complete source object). Read-only response fields (id, index, metadata) are ignored. Body fields: ${FIREWALL_POLICY_BODY_DOC} For just logging/flow-reporting toggles, prefer unifi_patch_firewall_policy. dryRun shows the merged body.`,
|
|
248
276
|
inputSchema: {
|
|
249
277
|
siteId: zod_1.z.string().describe("Site ID"),
|
|
250
278
|
firewallPolicyId: zod_1.z.string().describe("Firewall policy ID"),
|
|
251
279
|
policy: zod_1.z
|
|
252
280
|
.record(zod_1.z.string(), zod_1.z.unknown())
|
|
253
|
-
.describe("Firewall policy
|
|
281
|
+
.describe("Firewall policy fields to change (JSON object); null clears an optional field such as schedule"),
|
|
254
282
|
dryRun: zod_1.z
|
|
255
283
|
.boolean()
|
|
256
284
|
.optional()
|
|
257
|
-
.describe("Preview
|
|
285
|
+
.describe("Preview the merged PUT body without executing it (still reads the current policy)"),
|
|
258
286
|
},
|
|
259
287
|
outputSchema: output_schemas_js_1.firewallPolicyOutputSchema,
|
|
260
288
|
annotations: safety_js_1.WRITE,
|
|
261
289
|
}, async ({ siteId, firewallPolicyId, policy, dryRun }) => {
|
|
262
290
|
try {
|
|
291
|
+
const path = `/sites/${siteId}/firewall/policies/${firewallPolicyId}`;
|
|
292
|
+
const updates = Object.fromEntries(Object.entries(policy).filter(([field]) => !FIREWALL_POLICY_READ_ONLY.includes(field)));
|
|
293
|
+
const rejected = (0, full_put_js_1.unsupportedFields)(updates, FIREWALL_POLICY_FIELDS);
|
|
294
|
+
if (rejected.length > 0) {
|
|
295
|
+
throw new Error(`Fields not accepted by the firewall policy PUT body: ${rejected.join(", ")}`);
|
|
296
|
+
}
|
|
297
|
+
const current = (0, full_put_js_1.asObject)(await client.get(path), "the current firewall policy");
|
|
298
|
+
const body = (0, full_put_js_1.mergeForPut)(current, updates, FIREWALL_POLICY_FIELDS);
|
|
299
|
+
const missing = (0, full_put_js_1.missingFields)(body, FIREWALL_POLICY_REQUIRED);
|
|
300
|
+
if (missing.length > 0) {
|
|
301
|
+
throw new Error(`Missing required fields: ${missing.join(", ")}`);
|
|
302
|
+
}
|
|
263
303
|
if (dryRun)
|
|
264
|
-
return (0, safety_js_1.formatDryRun)("PUT",
|
|
265
|
-
const data = await client.put(
|
|
304
|
+
return (0, safety_js_1.formatDryRun)("PUT", path, body);
|
|
305
|
+
const data = await client.put(path, body);
|
|
266
306
|
return (0, responses_js_1.formatSuccess)(data, { structured: true });
|
|
267
307
|
}
|
|
268
308
|
catch (err) {
|
|
@@ -270,13 +310,13 @@ function registerFirewallTools(server, client, readOnly = false) {
|
|
|
270
310
|
}
|
|
271
311
|
});
|
|
272
312
|
server.registerTool("unifi_patch_firewall_policy", {
|
|
273
|
-
description: "Partially update a firewall policy without resending all fields. loggingEnabled
|
|
313
|
+
description: "Partially update a firewall policy without resending all fields. loggingEnabled and reportFlowEnabled are the only fields documented as patchable in API 11.0.83 (reportFlowEnabled is ignored for the predefined Allow Return Traffic policies, which are never reported); other fields may be rejected — use unifi_update_firewall_policy for a full update. Idempotent for fields supplied.",
|
|
274
314
|
inputSchema: {
|
|
275
315
|
siteId: zod_1.z.string().describe("Site ID"),
|
|
276
316
|
firewallPolicyId: zod_1.z.string().describe("Firewall policy ID"),
|
|
277
317
|
policy: zod_1.z
|
|
278
318
|
.record(zod_1.z.string(), zod_1.z.unknown())
|
|
279
|
-
.describe("Partial firewall policy fields to update (e.g. { loggingEnabled: true })"),
|
|
319
|
+
.describe("Partial firewall policy fields to update (e.g. { loggingEnabled: true } or { reportFlowEnabled: false })"),
|
|
280
320
|
dryRun: zod_1.z
|
|
281
321
|
.boolean()
|
|
282
322
|
.optional()
|