@owine/unifi-network-mcp 2.14.1 → 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 +9 -9
package/dist/tools/wifi.js
CHANGED
|
@@ -5,10 +5,135 @@ 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");
|
|
9
|
+
const secrets_js_1 = require("../utils/secrets.js");
|
|
8
10
|
const output_schemas_js_1 = require("../utils/output-schemas.js");
|
|
11
|
+
// Request-body fields per broadcast type, from the 11.0.83 POST/PUT schemas.
|
|
12
|
+
// IOT_OPTIMIZED accepts a strict subset of STANDARD.
|
|
13
|
+
const IOT_OPTIMIZED_FIELDS = [
|
|
14
|
+
"type",
|
|
15
|
+
"name",
|
|
16
|
+
"network",
|
|
17
|
+
"enabled",
|
|
18
|
+
"securityConfiguration",
|
|
19
|
+
"broadcastingDeviceFilter",
|
|
20
|
+
"mdnsProxyConfiguration",
|
|
21
|
+
"multicastFilteringPolicy",
|
|
22
|
+
"multicastToUnicastConversionEnabled",
|
|
23
|
+
"clientIsolationEnabled",
|
|
24
|
+
"hideName",
|
|
25
|
+
"uapsdEnabled",
|
|
26
|
+
"channel2gLockedTo6",
|
|
27
|
+
"dtimPeriod2gLockedTo3",
|
|
28
|
+
"basicDataRateKbpsByFrequencyGHz",
|
|
29
|
+
"clientFilteringPolicy",
|
|
30
|
+
"blackoutScheduleConfiguration",
|
|
31
|
+
];
|
|
32
|
+
const STANDARD_FIELDS = [
|
|
33
|
+
...IOT_OPTIMIZED_FIELDS,
|
|
34
|
+
"broadcastingFrequenciesGHz",
|
|
35
|
+
"hotspotConfiguration",
|
|
36
|
+
"mloEnabled",
|
|
37
|
+
"bandSteeringEnabled",
|
|
38
|
+
"arpProxyEnabled",
|
|
39
|
+
"bssTransitionEnabled",
|
|
40
|
+
"advertiseDeviceName",
|
|
41
|
+
"dtimPeriodByFrequencyGHzOverride",
|
|
42
|
+
"dnsAssistanceConfiguration",
|
|
43
|
+
"handoffSuggestionsConfiguration",
|
|
44
|
+
];
|
|
45
|
+
const IOT_OPTIMIZED_REQUIRED = [
|
|
46
|
+
"type",
|
|
47
|
+
"name",
|
|
48
|
+
"enabled",
|
|
49
|
+
"securityConfiguration",
|
|
50
|
+
"multicastToUnicastConversionEnabled",
|
|
51
|
+
"clientIsolationEnabled",
|
|
52
|
+
"hideName",
|
|
53
|
+
"uapsdEnabled",
|
|
54
|
+
"channel2gLockedTo6",
|
|
55
|
+
"dtimPeriod2gLockedTo3",
|
|
56
|
+
];
|
|
57
|
+
const STANDARD_REQUIRED = [
|
|
58
|
+
...IOT_OPTIMIZED_REQUIRED,
|
|
59
|
+
"broadcastingFrequenciesGHz",
|
|
60
|
+
"arpProxyEnabled",
|
|
61
|
+
"bssTransitionEnabled",
|
|
62
|
+
"advertiseDeviceName",
|
|
63
|
+
];
|
|
64
|
+
function fieldsForType(type) {
|
|
65
|
+
if (type === "STANDARD")
|
|
66
|
+
return { allowed: STANDARD_FIELDS, required: STANDARD_REQUIRED };
|
|
67
|
+
if (type === "IOT_OPTIMIZED")
|
|
68
|
+
return { allowed: IOT_OPTIMIZED_FIELDS, required: IOT_OPTIMIZED_REQUIRED };
|
|
69
|
+
throw new Error(`Unsupported WiFi broadcast type: ${String(type)}`);
|
|
70
|
+
}
|
|
71
|
+
const frequencies = zod_1.z
|
|
72
|
+
.array(zod_1.z.union([zod_1.z.enum(["2.4", "5", "6"]), zod_1.z.literal(2.4), zod_1.z.literal(5), zod_1.z.literal(6)]))
|
|
73
|
+
.min(1)
|
|
74
|
+
.describe("Frequencies to broadcast on: 2.4, 5, 6 (strings or numbers; sent as unique numbers). STANDARD only.");
|
|
75
|
+
const config = (description) => zod_1.z.record(zod_1.z.string(), zod_1.z.unknown()).nullable().optional().describe(description);
|
|
76
|
+
const flag = (description) => zod_1.z.boolean().optional().describe(description);
|
|
77
|
+
// Every body field as an optional input. Create tightens the core fields to
|
|
78
|
+
// required; update keeps them optional and fills gaps from the current SSID.
|
|
79
|
+
const wifiFields = {
|
|
80
|
+
name: zod_1.z.string().min(1).optional().describe("SSID name"),
|
|
81
|
+
enabled: flag("Enable the WiFi network"),
|
|
82
|
+
type: zod_1.z.enum(["STANDARD", "IOT_OPTIMIZED"]).optional().describe("WiFi type"),
|
|
83
|
+
broadcastingFrequenciesGHz: frequencies.optional(),
|
|
84
|
+
securityConfiguration: zod_1.z
|
|
85
|
+
.record(zod_1.z.string(), zod_1.z.unknown())
|
|
86
|
+
.optional()
|
|
87
|
+
.describe(`Security configuration object (type, passphrase, presharedKeys[], pmfMode, …). A "${secrets_js_1.REDACTED}" value read from unifi_get_wifi is kept as the existing secret on update.`),
|
|
88
|
+
multicastToUnicastConversionEnabled: flag("Enable multicast to unicast conversion"),
|
|
89
|
+
clientIsolationEnabled: flag("Enable client isolation"),
|
|
90
|
+
hideName: flag("Hide SSID name"),
|
|
91
|
+
uapsdEnabled: flag("Enable Unscheduled Automatic Power Save Delivery"),
|
|
92
|
+
channel2gLockedTo6: flag("Lock 2.4GHz channel to 6 on all broadcasting devices"),
|
|
93
|
+
dtimPeriod2gLockedTo3: flag("Lock 2.4GHz DTIM period to 3"),
|
|
94
|
+
arpProxyEnabled: flag("Enable ARP proxy. STANDARD only."),
|
|
95
|
+
bssTransitionEnabled: flag("Enable BSS transition. STANDARD only."),
|
|
96
|
+
advertiseDeviceName: flag("Advertise device name in beacon frames. STANDARD only."),
|
|
97
|
+
mloEnabled: flag("Enable MLO (Multi-Link Operation). STANDARD only."),
|
|
98
|
+
bandSteeringEnabled: flag("Enable band steering. STANDARD only."),
|
|
99
|
+
network: config("Network reference, e.g. {type:'NATIVE'} or {type:'SPECIFIC', networkId:'<uuid>'} (live-observed). Omitted on PPSK SSIDs, where each presharedKeys[] entry carries its own network."),
|
|
100
|
+
broadcastingDeviceFilter: config("Custom scope of broadcasting devices. Null = broadcast from all AP-capable devices."),
|
|
101
|
+
mdnsProxyConfiguration: config("mDNS proxy configuration"),
|
|
102
|
+
multicastFilteringPolicy: config("Multicast filtering policy"),
|
|
103
|
+
basicDataRateKbpsByFrequencyGHz: config("Basic data rate in kbps by frequency"),
|
|
104
|
+
clientFilteringPolicy: config("Client MAC address filtering policy"),
|
|
105
|
+
blackoutScheduleConfiguration: config("Blackout schedule configuration"),
|
|
106
|
+
hotspotConfiguration: config("Hotspot configuration, e.g. {type:'CAPTIVE_PORTAL'}. STANDARD only."),
|
|
107
|
+
dtimPeriodByFrequencyGHzOverride: config("DTIM period override by frequency. STANDARD only."),
|
|
108
|
+
dnsAssistanceConfiguration: config("DNS assistance configuration. STANDARD only."),
|
|
109
|
+
handoffSuggestionsConfiguration: config("Handoff suggestions config (band5GHzRssiThreshold, band6GHzRssiThreshold). Null disables. STANDARD only."),
|
|
110
|
+
};
|
|
111
|
+
function toBodyFields(values) {
|
|
112
|
+
const { broadcastingFrequenciesGHz, ...rest } = values;
|
|
113
|
+
return (0, full_put_js_1.definedEntries)({
|
|
114
|
+
...rest,
|
|
115
|
+
broadcastingFrequenciesGHz: broadcastingFrequenciesGHz && [...new Set(broadcastingFrequenciesGHz.map(Number))],
|
|
116
|
+
});
|
|
117
|
+
}
|
|
118
|
+
function checkBody(body, updates) {
|
|
119
|
+
const { allowed, required } = fieldsForType(body.type);
|
|
120
|
+
const rejected = (0, full_put_js_1.unsupportedFields)(updates, allowed);
|
|
121
|
+
if (rejected.length > 0) {
|
|
122
|
+
throw new Error(`Fields not accepted for type ${String(body.type)}: ${rejected.join(", ")}`);
|
|
123
|
+
}
|
|
124
|
+
const missing = (0, full_put_js_1.missingFields)(body, required);
|
|
125
|
+
if (missing.length > 0) {
|
|
126
|
+
throw new Error(`Missing required fields for type ${String(body.type)}: ${missing.join(", ")}`);
|
|
127
|
+
}
|
|
128
|
+
}
|
|
129
|
+
const includeSecrets = zod_1.z
|
|
130
|
+
.boolean()
|
|
131
|
+
.optional()
|
|
132
|
+
.describe(`Return WiFi passphrases in plaintext instead of "${secrets_js_1.REDACTED}" (default: false). Only set this when the user explicitly needs the key.`);
|
|
133
|
+
const readRedaction = `Passphrases (securityConfiguration.passphrase, presharedKeys[].passphrase) are returned as "${secrets_js_1.REDACTED}" unless includeSecrets is true.`;
|
|
9
134
|
function registerWifiTools(server, client, readOnly = false) {
|
|
10
135
|
server.registerTool("unifi_list_wifi", {
|
|
11
|
-
description:
|
|
136
|
+
description: `List all WiFi broadcasts (SSIDs) at a site. Returns (sparse list view, live-verified on 11.0.83): id, name (SSID), type (STANDARD/IOT_OPTIMIZED), enabled, metadata.origin, network reference (absent on PPSK SSIDs), securityConfiguration.type, broadcastingFrequenciesGHz, hotspotConfiguration (empty object when set). Booleans such as hideName/bandSteeringEnabled/clientIsolationEnabled, the full security configuration and optional configs are only returned by unifi_get_wifi. ${readRedaction} Use for: SSID inventory. For per-AP radio state (channel, txPower), use unifi_get_device on the AP.`,
|
|
12
137
|
inputSchema: {
|
|
13
138
|
siteId: zod_1.z.string().describe("Site ID"),
|
|
14
139
|
offset: zod_1.z
|
|
@@ -27,32 +152,34 @@ function registerWifiTools(server, client, readOnly = false) {
|
|
|
27
152
|
filter: zod_1.z
|
|
28
153
|
.string()
|
|
29
154
|
.optional()
|
|
30
|
-
.describe("Filter expression"),
|
|
155
|
+
.describe("Filter expression, e.g. enabled.eq(true)"),
|
|
156
|
+
includeSecrets,
|
|
31
157
|
},
|
|
32
158
|
outputSchema: output_schemas_js_1.listWifiOutputSchema,
|
|
33
159
|
annotations: safety_js_1.READ_ONLY,
|
|
34
|
-
}, async ({ siteId, offset, limit, filter }) => {
|
|
160
|
+
}, async ({ siteId, offset, limit, filter, includeSecrets }) => {
|
|
35
161
|
try {
|
|
36
162
|
const query = (0, query_js_1.buildQuery)({ offset, limit, filter });
|
|
37
163
|
const data = await client.get(`/sites/${siteId}/wifi/broadcasts${query}`);
|
|
38
|
-
return (0, responses_js_1.formatSuccess)(data, { structured: true });
|
|
164
|
+
return (0, responses_js_1.formatSuccess)(includeSecrets ? data : (0, secrets_js_1.redactSecrets)(data), { structured: true });
|
|
39
165
|
}
|
|
40
166
|
catch (err) {
|
|
41
167
|
return (0, responses_js_1.formatError)(err);
|
|
42
168
|
}
|
|
43
169
|
});
|
|
44
170
|
server.registerTool("unifi_get_wifi", {
|
|
45
|
-
description:
|
|
171
|
+
description: `Get full configuration for a WiFi broadcast (SSID). Returns the list fields plus the booleans (hideName, clientIsolationEnabled, uapsdEnabled, multicastToUnicastConversionEnabled, bandSteeringEnabled, arpProxyEnabled, bssTransitionEnabled, advertiseDeviceName, channel2gLockedTo6, dtimPeriod2gLockedTo3), the full securityConfiguration (type, pmfMode, fastRoamingEnabled, saeConfiguration, presharedKeys[] on PPSK SSIDs, …), and optional configs when configured (omitted otherwise): clientFilteringPolicy, blackoutScheduleConfiguration, hotspotConfiguration, mdnsProxyConfiguration, handoffSuggestionsConfiguration, dtimPeriodByFrequencyGHzOverride, broadcastingDeviceFilter, mloEnabled. ${readRedaction}`,
|
|
46
172
|
inputSchema: {
|
|
47
173
|
siteId: zod_1.z.string().describe("Site ID"),
|
|
48
174
|
wifiBroadcastId: zod_1.z.string().describe("WiFi Broadcast ID"),
|
|
175
|
+
includeSecrets,
|
|
49
176
|
},
|
|
50
177
|
outputSchema: output_schemas_js_1.getWifiOutputSchema,
|
|
51
178
|
annotations: safety_js_1.READ_ONLY,
|
|
52
|
-
}, async ({ siteId, wifiBroadcastId }) => {
|
|
179
|
+
}, async ({ siteId, wifiBroadcastId, includeSecrets }) => {
|
|
53
180
|
try {
|
|
54
181
|
const data = await client.get(`/sites/${siteId}/wifi/broadcasts/${wifiBroadcastId}`);
|
|
55
|
-
return (0, responses_js_1.formatSuccess)(data, { structured: true });
|
|
182
|
+
return (0, responses_js_1.formatSuccess)(includeSecrets ? data : (0, secrets_js_1.redactSecrets)(data), { structured: true });
|
|
56
183
|
}
|
|
57
184
|
catch (err) {
|
|
58
185
|
return (0, responses_js_1.formatError)(err);
|
|
@@ -61,93 +188,22 @@ function registerWifiTools(server, client, readOnly = false) {
|
|
|
61
188
|
if (readOnly)
|
|
62
189
|
return;
|
|
63
190
|
server.registerTool("unifi_create_wifi", {
|
|
64
|
-
description:
|
|
191
|
+
description: `Create a new WiFi network (SSID). Required for both types: type, name, enabled, securityConfiguration, multicastToUnicastConversionEnabled, clientIsolationEnabled, hideName, uapsdEnabled, channel2gLockedTo6, dtimPeriod2gLockedTo3. STANDARD additionally requires broadcastingFrequenciesGHz, arpProxyEnabled, bssTransitionEnabled, advertiseDeviceName; fields marked "STANDARD only" are rejected for IOT_OPTIMIZED (per 11.0.83 docs). The response echo has passphrases redacted. Idempotency: not safe to retry — re-running creates a duplicate SSID.`,
|
|
65
192
|
inputSchema: {
|
|
66
193
|
siteId: zod_1.z.string().describe("Site ID"),
|
|
67
|
-
|
|
194
|
+
...wifiFields,
|
|
195
|
+
name: zod_1.z.string().min(1).describe("SSID name"),
|
|
68
196
|
enabled: zod_1.z.boolean().describe("Enable the WiFi network"),
|
|
69
197
|
type: zod_1.z.enum(["STANDARD", "IOT_OPTIMIZED"]).describe("WiFi type"),
|
|
70
|
-
broadcastingFrequenciesGHz: zod_1.z
|
|
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
198
|
securityConfiguration: zod_1.z
|
|
74
199
|
.record(zod_1.z.string(), zod_1.z.unknown())
|
|
75
|
-
.describe("Security configuration object"),
|
|
76
|
-
multicastToUnicastConversionEnabled: zod_1.z
|
|
77
|
-
|
|
78
|
-
.describe("Enable multicast to unicast conversion"),
|
|
79
|
-
clientIsolationEnabled: zod_1.z
|
|
80
|
-
.boolean()
|
|
81
|
-
.describe("Enable client isolation"),
|
|
200
|
+
.describe("Security configuration object (type, passphrase, …)"),
|
|
201
|
+
multicastToUnicastConversionEnabled: zod_1.z.boolean().describe("Enable multicast to unicast conversion"),
|
|
202
|
+
clientIsolationEnabled: zod_1.z.boolean().describe("Enable client isolation"),
|
|
82
203
|
hideName: zod_1.z.boolean().describe("Hide SSID name"),
|
|
83
|
-
uapsdEnabled: zod_1.z
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
arpProxyEnabled: zod_1.z.boolean().describe("Enable ARP proxy"),
|
|
87
|
-
bssTransitionEnabled: zod_1.z
|
|
88
|
-
.boolean()
|
|
89
|
-
.describe("Enable BSS transition"),
|
|
90
|
-
advertiseDeviceName: zod_1.z
|
|
91
|
-
.boolean()
|
|
92
|
-
.describe("Advertise device name in beacon frames"),
|
|
93
|
-
channel2gLockedTo6: zod_1.z
|
|
94
|
-
.boolean()
|
|
95
|
-
.describe("Lock 2.4GHz channel to 6 on all broadcasting devices"),
|
|
96
|
-
dtimPeriod2gLockedTo3: zod_1.z
|
|
97
|
-
.boolean()
|
|
98
|
-
.describe("Lock 2.4GHz DTIM period to 3"),
|
|
99
|
-
network: zod_1.z
|
|
100
|
-
.record(zod_1.z.string(), zod_1.z.unknown())
|
|
101
|
-
.optional()
|
|
102
|
-
.describe("Network reference"),
|
|
103
|
-
broadcastingDeviceFilter: zod_1.z
|
|
104
|
-
.record(zod_1.z.string(), zod_1.z.unknown())
|
|
105
|
-
.optional()
|
|
106
|
-
.describe("Custom scope of broadcasting devices"),
|
|
107
|
-
mdnsProxyConfiguration: zod_1.z
|
|
108
|
-
.record(zod_1.z.string(), zod_1.z.unknown())
|
|
109
|
-
.optional()
|
|
110
|
-
.describe("mDNS proxy configuration"),
|
|
111
|
-
multicastFilteringPolicy: zod_1.z
|
|
112
|
-
.record(zod_1.z.string(), zod_1.z.unknown())
|
|
113
|
-
.optional()
|
|
114
|
-
.describe("Multicast filtering policy"),
|
|
115
|
-
basicDataRateKbpsByFrequencyGHz: zod_1.z
|
|
116
|
-
.record(zod_1.z.string(), zod_1.z.unknown())
|
|
117
|
-
.optional()
|
|
118
|
-
.describe("Basic data rate in kbps by frequency"),
|
|
119
|
-
clientFilteringPolicy: zod_1.z
|
|
120
|
-
.record(zod_1.z.string(), zod_1.z.unknown())
|
|
121
|
-
.optional()
|
|
122
|
-
.describe("Client MAC address filtering policy"),
|
|
123
|
-
blackoutScheduleConfiguration: zod_1.z
|
|
124
|
-
.record(zod_1.z.string(), zod_1.z.unknown())
|
|
125
|
-
.optional()
|
|
126
|
-
.describe("Blackout schedule configuration"),
|
|
127
|
-
hotspotConfiguration: zod_1.z
|
|
128
|
-
.record(zod_1.z.string(), zod_1.z.unknown())
|
|
129
|
-
.optional()
|
|
130
|
-
.describe("Hotspot configuration"),
|
|
131
|
-
mloEnabled: zod_1.z
|
|
132
|
-
.boolean()
|
|
133
|
-
.optional()
|
|
134
|
-
.describe("Enable MLO (Multi-Link Operation)"),
|
|
135
|
-
bandSteeringEnabled: zod_1.z
|
|
136
|
-
.boolean()
|
|
137
|
-
.optional()
|
|
138
|
-
.describe("Enable band steering"),
|
|
139
|
-
dtimPeriodByFrequencyGHzOverride: zod_1.z
|
|
140
|
-
.record(zod_1.z.string(), zod_1.z.unknown())
|
|
141
|
-
.optional()
|
|
142
|
-
.describe("DTIM period override by frequency"),
|
|
143
|
-
dnsAssistanceConfiguration: zod_1.z
|
|
144
|
-
.record(zod_1.z.string(), zod_1.z.unknown())
|
|
145
|
-
.optional()
|
|
146
|
-
.describe("DNS assistance configuration"),
|
|
147
|
-
handoffSuggestionsConfiguration: zod_1.z
|
|
148
|
-
.record(zod_1.z.string(), zod_1.z.unknown())
|
|
149
|
-
.optional()
|
|
150
|
-
.describe("Handoff suggestions config (band5GHzRssiThreshold, band6GHzRssiThreshold). Null disables"),
|
|
204
|
+
uapsdEnabled: zod_1.z.boolean().describe("Enable Unscheduled Automatic Power Save Delivery"),
|
|
205
|
+
channel2gLockedTo6: zod_1.z.boolean().describe("Lock 2.4GHz channel to 6 on all broadcasting devices"),
|
|
206
|
+
dtimPeriod2gLockedTo3: zod_1.z.boolean().describe("Lock 2.4GHz DTIM period to 3"),
|
|
151
207
|
dryRun: zod_1.z
|
|
152
208
|
.boolean()
|
|
153
209
|
.optional()
|
|
@@ -155,232 +211,47 @@ function registerWifiTools(server, client, readOnly = false) {
|
|
|
155
211
|
},
|
|
156
212
|
outputSchema: output_schemas_js_1.getWifiOutputSchema,
|
|
157
213
|
annotations: safety_js_1.WRITE_NOT_IDEMPOTENT,
|
|
158
|
-
}, async ({ siteId,
|
|
214
|
+
}, async ({ siteId, dryRun, ...fields }) => {
|
|
159
215
|
try {
|
|
160
|
-
const body =
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
securityConfiguration,
|
|
166
|
-
multicastToUnicastConversionEnabled,
|
|
167
|
-
clientIsolationEnabled,
|
|
168
|
-
hideName,
|
|
169
|
-
uapsdEnabled,
|
|
170
|
-
arpProxyEnabled,
|
|
171
|
-
bssTransitionEnabled,
|
|
172
|
-
advertiseDeviceName,
|
|
173
|
-
channel2gLockedTo6,
|
|
174
|
-
dtimPeriod2gLockedTo3,
|
|
175
|
-
};
|
|
176
|
-
if (network !== undefined)
|
|
177
|
-
body.network = network;
|
|
178
|
-
if (broadcastingDeviceFilter !== undefined)
|
|
179
|
-
body.broadcastingDeviceFilter = broadcastingDeviceFilter;
|
|
180
|
-
if (mdnsProxyConfiguration !== undefined)
|
|
181
|
-
body.mdnsProxyConfiguration = mdnsProxyConfiguration;
|
|
182
|
-
if (multicastFilteringPolicy !== undefined)
|
|
183
|
-
body.multicastFilteringPolicy = multicastFilteringPolicy;
|
|
184
|
-
if (basicDataRateKbpsByFrequencyGHz !== undefined)
|
|
185
|
-
body.basicDataRateKbpsByFrequencyGHz = basicDataRateKbpsByFrequencyGHz;
|
|
186
|
-
if (clientFilteringPolicy !== undefined)
|
|
187
|
-
body.clientFilteringPolicy = clientFilteringPolicy;
|
|
188
|
-
if (blackoutScheduleConfiguration !== undefined)
|
|
189
|
-
body.blackoutScheduleConfiguration = blackoutScheduleConfiguration;
|
|
190
|
-
if (hotspotConfiguration !== undefined)
|
|
191
|
-
body.hotspotConfiguration = hotspotConfiguration;
|
|
192
|
-
if (mloEnabled !== undefined)
|
|
193
|
-
body.mloEnabled = mloEnabled;
|
|
194
|
-
if (bandSteeringEnabled !== undefined)
|
|
195
|
-
body.bandSteeringEnabled = bandSteeringEnabled;
|
|
196
|
-
if (dtimPeriodByFrequencyGHzOverride !== undefined)
|
|
197
|
-
body.dtimPeriodByFrequencyGHzOverride = dtimPeriodByFrequencyGHzOverride;
|
|
198
|
-
if (dnsAssistanceConfiguration !== undefined)
|
|
199
|
-
body.dnsAssistanceConfiguration = dnsAssistanceConfiguration;
|
|
200
|
-
if (handoffSuggestionsConfiguration !== undefined)
|
|
201
|
-
body.handoffSuggestionsConfiguration = handoffSuggestionsConfiguration;
|
|
216
|
+
const body = toBodyFields(fields);
|
|
217
|
+
checkBody(body, body);
|
|
218
|
+
if ((0, secrets_js_1.containsRedacted)(body)) {
|
|
219
|
+
throw new Error(`securityConfiguration contains "${secrets_js_1.REDACTED}" — a new SSID needs the real passphrase`);
|
|
220
|
+
}
|
|
202
221
|
if (dryRun)
|
|
203
|
-
return (0, safety_js_1.formatDryRun)("POST", `/sites/${siteId}/wifi/broadcasts`, body);
|
|
222
|
+
return (0, safety_js_1.formatDryRun)("POST", `/sites/${siteId}/wifi/broadcasts`, (0, secrets_js_1.redactSecrets)(body));
|
|
204
223
|
const data = await client.post(`/sites/${siteId}/wifi/broadcasts`, body);
|
|
205
|
-
return (0, responses_js_1.formatSuccess)(data, { structured: true });
|
|
224
|
+
return (0, responses_js_1.formatSuccess)((0, secrets_js_1.redactSecrets)(data), { structured: true });
|
|
206
225
|
}
|
|
207
226
|
catch (err) {
|
|
208
227
|
return (0, responses_js_1.formatError)(err);
|
|
209
228
|
}
|
|
210
229
|
});
|
|
211
230
|
server.registerTool("unifi_update_wifi", {
|
|
212
|
-
description:
|
|
231
|
+
description: `Update an existing WiFi network. Pass only the fields to change. The API's PUT replaces the whole SSID (11.0.83 documents 14 required fields for STANDARD, 10 for IOT_OPTIMIZED), so this tool reads the current SSID, keeps every documented body field, overlays your changes and sends the full body — omitted fields keep their current values instead of being reset. Pass null to clear an optional config (e.g. broadcastingDeviceFilter: null broadcasts from all APs). Passphrases are kept unless you send a new one; "${secrets_js_1.REDACTED}" values are treated as "keep". dryRun shows the merged body (secrets redacted).`,
|
|
213
232
|
inputSchema: {
|
|
214
233
|
siteId: zod_1.z.string().describe("Site ID"),
|
|
215
234
|
wifiBroadcastId: zod_1.z.string().describe("WiFi Broadcast ID"),
|
|
216
|
-
|
|
217
|
-
enabled: zod_1.z.boolean().optional().describe("Enable the WiFi network"),
|
|
218
|
-
type: zod_1.z
|
|
219
|
-
.enum(["STANDARD", "IOT_OPTIMIZED"])
|
|
220
|
-
.optional()
|
|
221
|
-
.describe("WiFi type"),
|
|
222
|
-
broadcastingFrequenciesGHz: zod_1.z
|
|
223
|
-
.array(zod_1.z.enum(["2.4", "5", "6"]))
|
|
224
|
-
.optional()
|
|
225
|
-
.describe("Frequencies to broadcast on. Accepted as strings and sent to the API as numbers (2.4, 5, 6)."),
|
|
226
|
-
securityConfiguration: zod_1.z
|
|
227
|
-
.record(zod_1.z.string(), zod_1.z.unknown())
|
|
228
|
-
.optional()
|
|
229
|
-
.describe("Security configuration object"),
|
|
230
|
-
multicastToUnicastConversionEnabled: zod_1.z
|
|
231
|
-
.boolean()
|
|
232
|
-
.optional()
|
|
233
|
-
.describe("Enable multicast to unicast conversion"),
|
|
234
|
-
clientIsolationEnabled: zod_1.z
|
|
235
|
-
.boolean()
|
|
236
|
-
.optional()
|
|
237
|
-
.describe("Enable client isolation"),
|
|
238
|
-
hideName: zod_1.z.boolean().optional().describe("Hide SSID name"),
|
|
239
|
-
uapsdEnabled: zod_1.z
|
|
240
|
-
.boolean()
|
|
241
|
-
.optional()
|
|
242
|
-
.describe("Enable Unscheduled Automatic Power Save Delivery"),
|
|
243
|
-
arpProxyEnabled: zod_1.z
|
|
244
|
-
.boolean()
|
|
245
|
-
.optional()
|
|
246
|
-
.describe("Enable ARP proxy"),
|
|
247
|
-
bssTransitionEnabled: zod_1.z
|
|
248
|
-
.boolean()
|
|
249
|
-
.optional()
|
|
250
|
-
.describe("Enable BSS transition"),
|
|
251
|
-
advertiseDeviceName: zod_1.z
|
|
252
|
-
.boolean()
|
|
253
|
-
.optional()
|
|
254
|
-
.describe("Advertise device name in beacon frames"),
|
|
255
|
-
channel2gLockedTo6: zod_1.z
|
|
256
|
-
.boolean()
|
|
257
|
-
.optional()
|
|
258
|
-
.describe("Lock 2.4GHz channel to 6 on all broadcasting devices"),
|
|
259
|
-
dtimPeriod2gLockedTo3: zod_1.z
|
|
260
|
-
.boolean()
|
|
261
|
-
.optional()
|
|
262
|
-
.describe("Lock 2.4GHz DTIM period to 3"),
|
|
263
|
-
network: zod_1.z
|
|
264
|
-
.record(zod_1.z.string(), zod_1.z.unknown())
|
|
265
|
-
.optional()
|
|
266
|
-
.describe("Network reference"),
|
|
267
|
-
broadcastingDeviceFilter: zod_1.z
|
|
268
|
-
.record(zod_1.z.string(), zod_1.z.unknown())
|
|
269
|
-
.optional()
|
|
270
|
-
.describe("Custom scope of broadcasting devices"),
|
|
271
|
-
mdnsProxyConfiguration: zod_1.z
|
|
272
|
-
.record(zod_1.z.string(), zod_1.z.unknown())
|
|
273
|
-
.optional()
|
|
274
|
-
.describe("mDNS proxy configuration"),
|
|
275
|
-
multicastFilteringPolicy: zod_1.z
|
|
276
|
-
.record(zod_1.z.string(), zod_1.z.unknown())
|
|
277
|
-
.optional()
|
|
278
|
-
.describe("Multicast filtering policy"),
|
|
279
|
-
basicDataRateKbpsByFrequencyGHz: zod_1.z
|
|
280
|
-
.record(zod_1.z.string(), zod_1.z.unknown())
|
|
281
|
-
.optional()
|
|
282
|
-
.describe("Basic data rate in kbps by frequency"),
|
|
283
|
-
clientFilteringPolicy: zod_1.z
|
|
284
|
-
.record(zod_1.z.string(), zod_1.z.unknown())
|
|
285
|
-
.optional()
|
|
286
|
-
.describe("Client MAC address filtering policy"),
|
|
287
|
-
blackoutScheduleConfiguration: zod_1.z
|
|
288
|
-
.record(zod_1.z.string(), zod_1.z.unknown())
|
|
289
|
-
.optional()
|
|
290
|
-
.describe("Blackout schedule configuration"),
|
|
291
|
-
hotspotConfiguration: zod_1.z
|
|
292
|
-
.record(zod_1.z.string(), zod_1.z.unknown())
|
|
293
|
-
.optional()
|
|
294
|
-
.describe("Hotspot configuration"),
|
|
295
|
-
mloEnabled: zod_1.z
|
|
296
|
-
.boolean()
|
|
297
|
-
.optional()
|
|
298
|
-
.describe("Enable MLO (Multi-Link Operation)"),
|
|
299
|
-
bandSteeringEnabled: zod_1.z
|
|
300
|
-
.boolean()
|
|
301
|
-
.optional()
|
|
302
|
-
.describe("Enable band steering"),
|
|
303
|
-
dtimPeriodByFrequencyGHzOverride: zod_1.z
|
|
304
|
-
.record(zod_1.z.string(), zod_1.z.unknown())
|
|
305
|
-
.optional()
|
|
306
|
-
.describe("DTIM period override by frequency"),
|
|
307
|
-
dnsAssistanceConfiguration: zod_1.z
|
|
308
|
-
.record(zod_1.z.string(), zod_1.z.unknown())
|
|
309
|
-
.optional()
|
|
310
|
-
.describe("DNS assistance configuration"),
|
|
311
|
-
handoffSuggestionsConfiguration: zod_1.z
|
|
312
|
-
.record(zod_1.z.string(), zod_1.z.unknown())
|
|
313
|
-
.optional()
|
|
314
|
-
.describe("Handoff suggestions config (band5GHzRssiThreshold, band6GHzRssiThreshold). Null disables"),
|
|
235
|
+
...wifiFields,
|
|
315
236
|
dryRun: zod_1.z
|
|
316
237
|
.boolean()
|
|
317
238
|
.optional()
|
|
318
|
-
.describe("Preview
|
|
239
|
+
.describe("Preview the merged PUT body without executing it (still reads the current SSID)"),
|
|
319
240
|
},
|
|
320
241
|
outputSchema: output_schemas_js_1.getWifiOutputSchema,
|
|
321
242
|
annotations: safety_js_1.WRITE,
|
|
322
|
-
}, async ({ siteId, wifiBroadcastId,
|
|
243
|
+
}, async ({ siteId, wifiBroadcastId, dryRun, ...fields }) => {
|
|
323
244
|
try {
|
|
324
|
-
const
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
body.type = type;
|
|
331
|
-
if (broadcastingFrequenciesGHz !== undefined)
|
|
332
|
-
body.broadcastingFrequenciesGHz = broadcastingFrequenciesGHz.map(Number);
|
|
333
|
-
if (securityConfiguration !== undefined)
|
|
334
|
-
body.securityConfiguration = securityConfiguration;
|
|
335
|
-
if (multicastToUnicastConversionEnabled !== undefined)
|
|
336
|
-
body.multicastToUnicastConversionEnabled =
|
|
337
|
-
multicastToUnicastConversionEnabled;
|
|
338
|
-
if (clientIsolationEnabled !== undefined)
|
|
339
|
-
body.clientIsolationEnabled = clientIsolationEnabled;
|
|
340
|
-
if (hideName !== undefined)
|
|
341
|
-
body.hideName = hideName;
|
|
342
|
-
if (uapsdEnabled !== undefined)
|
|
343
|
-
body.uapsdEnabled = uapsdEnabled;
|
|
344
|
-
if (arpProxyEnabled !== undefined)
|
|
345
|
-
body.arpProxyEnabled = arpProxyEnabled;
|
|
346
|
-
if (bssTransitionEnabled !== undefined)
|
|
347
|
-
body.bssTransitionEnabled = bssTransitionEnabled;
|
|
348
|
-
if (advertiseDeviceName !== undefined)
|
|
349
|
-
body.advertiseDeviceName = advertiseDeviceName;
|
|
350
|
-
if (channel2gLockedTo6 !== undefined)
|
|
351
|
-
body.channel2gLockedTo6 = channel2gLockedTo6;
|
|
352
|
-
if (dtimPeriod2gLockedTo3 !== undefined)
|
|
353
|
-
body.dtimPeriod2gLockedTo3 = dtimPeriod2gLockedTo3;
|
|
354
|
-
if (network !== undefined)
|
|
355
|
-
body.network = network;
|
|
356
|
-
if (broadcastingDeviceFilter !== undefined)
|
|
357
|
-
body.broadcastingDeviceFilter = broadcastingDeviceFilter;
|
|
358
|
-
if (mdnsProxyConfiguration !== undefined)
|
|
359
|
-
body.mdnsProxyConfiguration = mdnsProxyConfiguration;
|
|
360
|
-
if (multicastFilteringPolicy !== undefined)
|
|
361
|
-
body.multicastFilteringPolicy = multicastFilteringPolicy;
|
|
362
|
-
if (basicDataRateKbpsByFrequencyGHz !== undefined)
|
|
363
|
-
body.basicDataRateKbpsByFrequencyGHz = basicDataRateKbpsByFrequencyGHz;
|
|
364
|
-
if (clientFilteringPolicy !== undefined)
|
|
365
|
-
body.clientFilteringPolicy = clientFilteringPolicy;
|
|
366
|
-
if (blackoutScheduleConfiguration !== undefined)
|
|
367
|
-
body.blackoutScheduleConfiguration = blackoutScheduleConfiguration;
|
|
368
|
-
if (hotspotConfiguration !== undefined)
|
|
369
|
-
body.hotspotConfiguration = hotspotConfiguration;
|
|
370
|
-
if (mloEnabled !== undefined)
|
|
371
|
-
body.mloEnabled = mloEnabled;
|
|
372
|
-
if (bandSteeringEnabled !== undefined)
|
|
373
|
-
body.bandSteeringEnabled = bandSteeringEnabled;
|
|
374
|
-
if (dtimPeriodByFrequencyGHzOverride !== undefined)
|
|
375
|
-
body.dtimPeriodByFrequencyGHzOverride = dtimPeriodByFrequencyGHzOverride;
|
|
376
|
-
if (dnsAssistanceConfiguration !== undefined)
|
|
377
|
-
body.dnsAssistanceConfiguration = dnsAssistanceConfiguration;
|
|
378
|
-
if (handoffSuggestionsConfiguration !== undefined)
|
|
379
|
-
body.handoffSuggestionsConfiguration = handoffSuggestionsConfiguration;
|
|
245
|
+
const path = `/sites/${siteId}/wifi/broadcasts/${wifiBroadcastId}`;
|
|
246
|
+
const current = (0, full_put_js_1.asObject)(await client.get(path), "the current WiFi broadcast");
|
|
247
|
+
const updates = (0, secrets_js_1.restoreRedacted)(toBodyFields(fields), current);
|
|
248
|
+
const { allowed } = fieldsForType(updates.type ?? current.type);
|
|
249
|
+
const body = (0, full_put_js_1.mergeForPut)(current, updates, allowed);
|
|
250
|
+
checkBody(body, updates);
|
|
380
251
|
if (dryRun)
|
|
381
|
-
return (0, safety_js_1.formatDryRun)("PUT",
|
|
382
|
-
const data = await client.put(
|
|
383
|
-
return (0, responses_js_1.formatSuccess)(data, { structured: true });
|
|
252
|
+
return (0, safety_js_1.formatDryRun)("PUT", path, (0, secrets_js_1.redactSecrets)(body));
|
|
253
|
+
const data = await client.put(path, body);
|
|
254
|
+
return (0, responses_js_1.formatSuccess)((0, secrets_js_1.redactSecrets)(data), { structured: true });
|
|
384
255
|
}
|
|
385
256
|
catch (err) {
|
|
386
257
|
return (0, responses_js_1.formatError)(err);
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Read-merge-write support for UniFi's full-replacement PUT endpoints.
|
|
3
|
+
*
|
|
4
|
+
* Several Integration API PUTs (WiFi broadcasts, networks, firewall policies)
|
|
5
|
+
* replace the whole resource: required fields must all be present, and some
|
|
6
|
+
* optional fields carry meaning when omitted (a missing `dhcpGuarding`
|
|
7
|
+
* disables DHCP guarding; a missing `reportFlowEnabled` reverts to true). A
|
|
8
|
+
* sparse body therefore either fails or silently resets settings.
|
|
9
|
+
*
|
|
10
|
+
* Instead, tools GET the current resource, keep only the fields the PUT body
|
|
11
|
+
* documents (so read-only response fields like id/metadata never leak into the
|
|
12
|
+
* request), and overlay the caller's changes. `undefined` keeps the current
|
|
13
|
+
* value; `null` is sent as an explicit null, which the API uses to clear.
|
|
14
|
+
*/
|
|
15
|
+
export type JsonObject = Record<string, unknown>;
|
|
16
|
+
export declare function asObject(value: unknown, what: string): JsonObject;
|
|
17
|
+
export declare function mergeForPut(current: JsonObject, updates: JsonObject, allowedFields: readonly string[]): JsonObject;
|
|
18
|
+
/** Fields the caller set that the target variant's PUT body does not accept. */
|
|
19
|
+
export declare function unsupportedFields(updates: JsonObject, allowedFields: readonly string[]): string[];
|
|
20
|
+
/** Required fields that are absent (undefined or null) from a request body. */
|
|
21
|
+
export declare function missingFields(body: JsonObject, requiredFields: readonly string[]): string[];
|
|
22
|
+
/** Copy only the defined entries, so optional inputs the caller skipped are not sent. */
|
|
23
|
+
export declare function definedEntries(values: JsonObject): JsonObject;
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
/**
|
|
3
|
+
* Read-merge-write support for UniFi's full-replacement PUT endpoints.
|
|
4
|
+
*
|
|
5
|
+
* Several Integration API PUTs (WiFi broadcasts, networks, firewall policies)
|
|
6
|
+
* replace the whole resource: required fields must all be present, and some
|
|
7
|
+
* optional fields carry meaning when omitted (a missing `dhcpGuarding`
|
|
8
|
+
* disables DHCP guarding; a missing `reportFlowEnabled` reverts to true). A
|
|
9
|
+
* sparse body therefore either fails or silently resets settings.
|
|
10
|
+
*
|
|
11
|
+
* Instead, tools GET the current resource, keep only the fields the PUT body
|
|
12
|
+
* documents (so read-only response fields like id/metadata never leak into the
|
|
13
|
+
* request), and overlay the caller's changes. `undefined` keeps the current
|
|
14
|
+
* value; `null` is sent as an explicit null, which the API uses to clear.
|
|
15
|
+
*/
|
|
16
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
17
|
+
exports.asObject = asObject;
|
|
18
|
+
exports.mergeForPut = mergeForPut;
|
|
19
|
+
exports.unsupportedFields = unsupportedFields;
|
|
20
|
+
exports.missingFields = missingFields;
|
|
21
|
+
exports.definedEntries = definedEntries;
|
|
22
|
+
function asObject(value, what) {
|
|
23
|
+
if (value === null || typeof value !== "object" || Array.isArray(value)) {
|
|
24
|
+
throw new Error(`Expected ${what} to be a JSON object`);
|
|
25
|
+
}
|
|
26
|
+
return value;
|
|
27
|
+
}
|
|
28
|
+
function mergeForPut(current, updates, allowedFields) {
|
|
29
|
+
const body = {};
|
|
30
|
+
for (const field of allowedFields) {
|
|
31
|
+
if (current[field] !== undefined)
|
|
32
|
+
body[field] = current[field];
|
|
33
|
+
}
|
|
34
|
+
for (const [field, value] of Object.entries(updates)) {
|
|
35
|
+
if (value !== undefined && allowedFields.includes(field))
|
|
36
|
+
body[field] = value;
|
|
37
|
+
}
|
|
38
|
+
return body;
|
|
39
|
+
}
|
|
40
|
+
/** Fields the caller set that the target variant's PUT body does not accept. */
|
|
41
|
+
function unsupportedFields(updates, allowedFields) {
|
|
42
|
+
return Object.keys(updates).filter((field) => updates[field] !== undefined && !allowedFields.includes(field));
|
|
43
|
+
}
|
|
44
|
+
/** Required fields that are absent (undefined or null) from a request body. */
|
|
45
|
+
function missingFields(body, requiredFields) {
|
|
46
|
+
return requiredFields.filter((field) => body[field] === undefined || body[field] === null);
|
|
47
|
+
}
|
|
48
|
+
/** Copy only the defined entries, so optional inputs the caller skipped are not sent. */
|
|
49
|
+
function definedEntries(values) {
|
|
50
|
+
return Object.fromEntries(Object.entries(values).filter(([, value]) => value !== undefined));
|
|
51
|
+
}
|