@owine/unifi-network-mcp 2.14.2 → 3.0.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.
Files changed (51) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +235 -80
  3. package/dist/client.d.ts +13 -1
  4. package/dist/client.js +142 -16
  5. package/dist/config.d.ts +11 -0
  6. package/dist/config.js +102 -3
  7. package/dist/guard.d.ts +46 -0
  8. package/dist/guard.js +171 -0
  9. package/dist/index.js +15 -4
  10. package/dist/instructions.d.ts +9 -0
  11. package/dist/instructions.js +32 -0
  12. package/dist/tls.d.ts +14 -0
  13. package/dist/tls.js +58 -0
  14. package/dist/tools/acl.js +126 -53
  15. package/dist/tools/client-history.js +20 -10
  16. package/dist/tools/clients.js +14 -13
  17. package/dist/tools/devices.js +30 -29
  18. package/dist/tools/dns-policies.js +116 -69
  19. package/dist/tools/firewall.d.ts +2 -1
  20. package/dist/tools/firewall.js +245 -80
  21. package/dist/tools/hotspot.js +35 -28
  22. package/dist/tools/index.d.ts +13 -1
  23. package/dist/tools/index.js +63 -4
  24. package/dist/tools/networks.d.ts +2 -1
  25. package/dist/tools/networks.js +152 -58
  26. package/dist/tools/sites.js +3 -3
  27. package/dist/tools/supporting.js +31 -30
  28. package/dist/tools/switching.js +22 -21
  29. package/dist/tools/system.js +1 -1
  30. package/dist/tools/traffic-matching.d.ts +2 -1
  31. package/dist/tools/traffic-matching.js +31 -28
  32. package/dist/tools/wifi.js +228 -311
  33. package/dist/utils/full-put.d.ts +32 -0
  34. package/dist/utils/full-put.js +74 -0
  35. package/dist/utils/output-schemas.d.ts +77 -9
  36. package/dist/utils/output-schemas.js +106 -54
  37. package/dist/utils/params.d.ts +7 -0
  38. package/dist/utils/params.js +12 -0
  39. package/dist/utils/responses.d.ts +2 -0
  40. package/dist/utils/responses.js +28 -2
  41. package/dist/utils/safety.d.ts +35 -1
  42. package/dist/utils/safety.js +53 -3
  43. package/dist/utils/secrets.d.ts +24 -0
  44. package/dist/utils/secrets.js +92 -0
  45. package/dist/utils/wan-exposure.d.ts +69 -0
  46. package/dist/utils/wan-exposure.js +245 -0
  47. package/dist/write-log.d.ts +52 -0
  48. package/dist/write-log.js +178 -0
  49. package/dist/write-tools.d.ts +27 -0
  50. package/dist/write-tools.js +50 -0
  51. package/package.json +19 -15
@@ -1,20 +1,27 @@
1
1
  "use strict";
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
3
  exports.listClientSessionsOutputSchema = exports.listClientHistoryOutputSchema = exports.ClientSessionEnvelope = exports.ClientSession = exports.HistoryClient = exports.listPendingDevicesOutputSchema = exports.listCountriesOutputSchema = exports.listDpiApplicationsOutputSchema = exports.listDpiCategoriesOutputSchema = exports.listDeviceTagsOutputSchema = exports.listRadiusProfilesOutputSchema = exports.listVpnTunnelsOutputSchema = exports.listVpnServersOutputSchema = exports.listWansOutputSchema = exports.getWifiOutputSchema = exports.listWifiOutputSchema = exports.getClientOutputSchema = exports.listClientsOutputSchema = exports.getDeviceStatisticsOutputSchema = exports.getDeviceOutputSchema = exports.listDevicesOutputSchema = exports.listTrafficMatchingListsOutputSchema = exports.trafficMatchingListOutputSchema = exports.listDnsPoliciesOutputSchema = exports.dnsPolicyOutputSchema = exports.listLagsOutputSchema = exports.lagOutputSchema = exports.listMcLagDomainsOutputSchema = exports.mcLagDomainOutputSchema = exports.listSwitchStacksOutputSchema = exports.switchStackOutputSchema = exports.aclRuleOrderingOutputSchema = exports.listAclRulesOutputSchema = exports.aclRuleOutputSchema = exports.firewallPolicyOrderingOutputSchema = exports.listFirewallPoliciesOutputSchema = exports.firewallPolicyOutputSchema = exports.listFirewallZonesOutputSchema = exports.firewallZoneOutputSchema = exports.createVouchersOutputSchema = exports.listVouchersOutputSchema = exports.voucherOutputSchema = exports.getNetworkReferencesOutputSchema = exports.listNetworksOutputSchema = exports.networkOutputSchema = exports.listSitesOutputSchema = exports.getInfoOutputSchema = void 0;
4
+ exports.looseOutput = looseOutput;
5
+ exports.writeResult = writeResult;
4
6
  /**
5
- * Output schemas for high-value read tools (loose strategy: every non-key
6
- * field optional, every nested object uses .passthrough() to allow
7
+ * Output schemas for every read tool, and for write tools whose response
8
+ * echoes the resource (loose strategy: every non-key
9
+ * field optional, every nested object uses .loose() to allow
7
10
  * firmware/hardware-specific fields to flow through unchanged).
8
11
  *
9
- * Verified against UniFi Network API 10.6.106. Where the docs collapse
12
+ * Verified against UniFi Network API 11.0.83. Where the docs collapse
10
13
  * nested arrays/objects (e.g. interfaces.ports[], radios[]), the schema
11
- * uses passthrough records so the contract doesn't lock to fields we
12
- * haven't verified.
14
+ * uses loose records so the contract doesn't lock to fields we
15
+ * haven't verified. Tools register these raw shapes, and registerAllTools()
16
+ * publishes each as a loose object (src/tools/index.ts), so fields a schema
17
+ * does not list never fail client-side validation.
13
18
  */
14
19
  const zod_1 = require("zod");
20
+ // configurable is live-verified (11.0.83) on system-defined zones, policies,
21
+ // networks and RADIUS profiles; it is absent on user-defined resources.
15
22
  const Metadata = zod_1.z
16
- .object({ origin: zod_1.z.string().optional() })
17
- .passthrough();
23
+ .object({ origin: zod_1.z.string().optional(), configurable: zod_1.z.boolean().optional() })
24
+ .loose();
18
25
  const PaginationFields = {
19
26
  offset: zod_1.z.number().int().optional(),
20
27
  limit: zod_1.z.number().int().optional(),
@@ -43,7 +50,7 @@ const DeviceListItem = zod_1.z
43
50
  features: zod_1.z.array(zod_1.z.string()).optional(),
44
51
  interfaces: zod_1.z.array(zod_1.z.string()).optional(),
45
52
  })
46
- .passthrough();
53
+ .loose();
47
54
  const DevicePort = zod_1.z
48
55
  .object({
49
56
  idx: zod_1.z.number(),
@@ -59,10 +66,10 @@ const DevicePort = zod_1.z
59
66
  enabled: zod_1.z.boolean().optional(),
60
67
  state: zod_1.z.string().optional(),
61
68
  })
62
- .passthrough()
69
+ .loose()
63
70
  .optional(),
64
71
  })
65
- .passthrough();
72
+ .loose();
66
73
  const DeviceRadio = zod_1.z
67
74
  .object({
68
75
  wlanStandard: zod_1.z.string().optional(),
@@ -70,7 +77,7 @@ const DeviceRadio = zod_1.z
70
77
  channelWidthMHz: zod_1.z.number().optional(),
71
78
  channel: zod_1.z.number().optional(),
72
79
  })
73
- .passthrough();
80
+ .loose();
74
81
  const DeviceDetail = zod_1.z
75
82
  .object({
76
83
  ...DeviceCommon,
@@ -79,7 +86,7 @@ const DeviceDetail = zod_1.z
79
86
  configurationId: zod_1.z.string().optional(),
80
87
  uplink: zod_1.z
81
88
  .object({ deviceId: zod_1.z.string().optional() })
82
- .passthrough()
89
+ .loose()
83
90
  .optional(),
84
91
  // Detail view: object keyed by capability (switching/accessPoint).
85
92
  features: zod_1.z.record(zod_1.z.string(), zod_1.z.unknown()).optional(),
@@ -88,10 +95,10 @@ const DeviceDetail = zod_1.z
88
95
  ports: zod_1.z.array(DevicePort).optional(),
89
96
  radios: zod_1.z.array(DeviceRadio).optional(),
90
97
  })
91
- .passthrough()
98
+ .loose()
92
99
  .optional(),
93
100
  })
94
- .passthrough();
101
+ .loose();
95
102
  const DeviceStatistics = zod_1.z
96
103
  .object({
97
104
  uptimeSec: zod_1.z.number().optional(),
@@ -107,7 +114,7 @@ const DeviceStatistics = zod_1.z
107
114
  txRateBps: zod_1.z.number().optional(),
108
115
  rxRateBps: zod_1.z.number().optional(),
109
116
  })
110
- .passthrough()
117
+ .loose()
111
118
  .optional(),
112
119
  interfaces: zod_1.z
113
120
  .object({
@@ -117,13 +124,13 @@ const DeviceStatistics = zod_1.z
117
124
  frequencyGHz: zod_1.z.number().optional(),
118
125
  txRetriesPct: zod_1.z.number().optional(),
119
126
  })
120
- .passthrough())
127
+ .loose())
121
128
  .optional(),
122
129
  })
123
- .passthrough()
130
+ .loose()
124
131
  .optional(),
125
132
  })
126
- .passthrough();
133
+ .loose();
127
134
  const Client = zod_1.z
128
135
  .object({
129
136
  id: zod_1.z.string(),
@@ -135,10 +142,10 @@ const Client = zod_1.z
135
142
  uplinkDeviceId: zod_1.z.string().optional(),
136
143
  access: zod_1.z
137
144
  .object({ type: zod_1.z.string().optional() })
138
- .passthrough()
145
+ .loose()
139
146
  .optional(),
140
147
  })
141
- .passthrough();
148
+ .loose();
142
149
  const WifiBroadcast = zod_1.z
143
150
  .object({
144
151
  id: zod_1.z.string(),
@@ -157,20 +164,20 @@ const WifiBroadcast = zod_1.z
157
164
  broadcastingDeviceFilter: zod_1.z.unknown().optional(),
158
165
  metadata: Metadata.optional(),
159
166
  })
160
- .passthrough();
167
+ .loose();
161
168
  const ApplicationInfo = zod_1.z
162
169
  .object({
163
- // Live-verified (10.6.106): only applicationVersion is returned.
170
+ // Live-verified (11.0.83): only applicationVersion is returned.
164
171
  applicationVersion: zod_1.z.string().optional(),
165
172
  })
166
- .passthrough();
173
+ .loose();
167
174
  const Site = zod_1.z
168
175
  .object({
169
176
  id: zod_1.z.string(),
170
177
  internalReference: zod_1.z.string().optional(),
171
178
  name: zod_1.z.string().optional(),
172
179
  })
173
- .passthrough();
180
+ .loose();
174
181
  const Network = zod_1.z
175
182
  .object({
176
183
  id: zod_1.z.string(),
@@ -190,12 +197,12 @@ const Network = zod_1.z
190
197
  ipv6Configuration: zod_1.z.unknown().optional(),
191
198
  metadata: Metadata.optional(),
192
199
  })
193
- .passthrough();
200
+ .loose();
194
201
  const NetworkReferences = zod_1.z
195
202
  .object({
196
203
  referenceResources: zod_1.z.array(zod_1.z.unknown()).optional(),
197
204
  })
198
- .passthrough();
205
+ .loose();
199
206
  const Voucher = zod_1.z
200
207
  .object({
201
208
  id: zod_1.z.string(),
@@ -212,7 +219,7 @@ const Voucher = zod_1.z
212
219
  authorizedGuestCount: zod_1.z.number().optional(),
213
220
  expired: zod_1.z.boolean().optional(),
214
221
  })
215
- .passthrough();
222
+ .loose();
216
223
  const FirewallZone = zod_1.z
217
224
  .object({
218
225
  id: zod_1.z.string(),
@@ -220,7 +227,7 @@ const FirewallZone = zod_1.z
220
227
  networkIds: zod_1.z.array(zod_1.z.string()).optional(),
221
228
  metadata: Metadata.optional(),
222
229
  })
223
- .passthrough();
230
+ .loose();
224
231
  const FirewallPolicy = zod_1.z
225
232
  .object({
226
233
  id: zod_1.z.string(),
@@ -236,11 +243,13 @@ const FirewallPolicy = zod_1.z
236
243
  ipsecFilter: zod_1.z.unknown().optional(),
237
244
  schedule: zod_1.z.unknown().optional(),
238
245
  loggingEnabled: zod_1.z.boolean().optional(),
246
+ // New in 11.0.83; live-verified as a boolean on every policy.
247
+ reportFlowEnabled: zod_1.z.boolean().optional(),
239
248
  index: zod_1.z.number().optional(),
240
249
  description: zod_1.z.string().optional(),
241
250
  metadata: Metadata.optional(),
242
251
  })
243
- .passthrough();
252
+ .loose();
244
253
  const FirewallPolicyOrdering = zod_1.z
245
254
  .object({
246
255
  orderedFirewallPolicyIds: zod_1.z
@@ -248,10 +257,10 @@ const FirewallPolicyOrdering = zod_1.z
248
257
  beforeSystemDefined: zod_1.z.array(zod_1.z.string()).optional(),
249
258
  afterSystemDefined: zod_1.z.array(zod_1.z.string()).optional(),
250
259
  })
251
- .passthrough()
260
+ .loose()
252
261
  .optional(),
253
262
  })
254
- .passthrough();
263
+ .loose();
255
264
  const AclRule = zod_1.z
256
265
  .object({
257
266
  id: zod_1.z.string(),
@@ -259,8 +268,11 @@ const AclRule = zod_1.z
259
268
  name: zod_1.z.string().optional(),
260
269
  enabled: zod_1.z.boolean().optional(),
261
270
  action: zod_1.z.string().optional(),
262
- description: zod_1.z.string().optional(),
263
- protocolFilter: zod_1.z.array(zod_1.z.string()).optional(),
271
+ // The docs give null a meaning ("When null, the rule will be applied to
272
+ // all protocols") and ACL samples already carry explicit nulls, so both
273
+ // must accept null or a protocol-agnostic rule hard-fails with -32602.
274
+ description: zod_1.z.string().nullable().optional(),
275
+ protocolFilter: zod_1.z.array(zod_1.z.string()).nullable().optional(),
264
276
  sourceFilter: zod_1.z.unknown().optional(),
265
277
  destinationFilter: zod_1.z.unknown().optional(),
266
278
  enforcingDeviceFilter: zod_1.z.unknown().optional(),
@@ -269,12 +281,12 @@ const AclRule = zod_1.z
269
281
  index: zod_1.z.number().int().optional(),
270
282
  metadata: Metadata.optional(),
271
283
  })
272
- .passthrough();
284
+ .loose();
273
285
  const AclRuleOrdering = zod_1.z
274
286
  .object({
275
287
  orderedAclRuleIds: zod_1.z.array(zod_1.z.string()).optional(),
276
288
  })
277
- .passthrough();
289
+ .loose();
278
290
  const SwitchStack = zod_1.z
279
291
  .object({
280
292
  id: zod_1.z.string(),
@@ -289,7 +301,7 @@ const SwitchStack = zod_1.z
289
301
  lags: zod_1.z.array(zod_1.z.unknown()).optional(),
290
302
  metadata: Metadata.optional(),
291
303
  })
292
- .passthrough();
304
+ .loose();
293
305
  const McLagDomain = zod_1.z
294
306
  .object({
295
307
  id: zod_1.z.string(),
@@ -298,7 +310,7 @@ const McLagDomain = zod_1.z
298
310
  lags: zod_1.z.array(zod_1.z.unknown()).optional(),
299
311
  metadata: Metadata.optional(),
300
312
  })
301
- .passthrough();
313
+ .loose();
302
314
  const Lag = zod_1.z
303
315
  .object({
304
316
  id: zod_1.z.string(),
@@ -309,11 +321,11 @@ const Lag = zod_1.z
309
321
  deviceId: zod_1.z.string().optional(),
310
322
  portIdxs: zod_1.z.array(zod_1.z.number()).optional(),
311
323
  })
312
- .passthrough())
324
+ .loose())
313
325
  .optional(),
314
326
  metadata: Metadata.optional(),
315
327
  })
316
- .passthrough();
328
+ .loose();
317
329
  const DnsPolicy = zod_1.z
318
330
  .object({
319
331
  id: zod_1.z.string(),
@@ -322,9 +334,22 @@ const DnsPolicy = zod_1.z
322
334
  domain: zod_1.z.string().optional(),
323
335
  ipv4Address: zod_1.z.string().optional(),
324
336
  ttlSeconds: zod_1.z.number().optional(),
337
+ // Type-specific fields from the 11.0.83 response samples (no live DNS
338
+ // policies to sample), so nullable to be safe.
339
+ ipv6Address: zod_1.z.string().nullable().optional(),
340
+ targetDomain: zod_1.z.string().nullable().optional(),
341
+ mailServerDomain: zod_1.z.string().nullable().optional(),
342
+ text: zod_1.z.string().nullable().optional(),
343
+ serverDomain: zod_1.z.string().nullable().optional(),
344
+ service: zod_1.z.string().nullable().optional(),
345
+ protocol: zod_1.z.string().nullable().optional(),
346
+ port: zod_1.z.number().nullable().optional(),
347
+ priority: zod_1.z.number().nullable().optional(),
348
+ weight: zod_1.z.number().nullable().optional(),
349
+ ipAddress: zod_1.z.string().nullable().optional(),
325
350
  metadata: Metadata.optional(),
326
351
  })
327
- .passthrough();
352
+ .loose();
328
353
  const TrafficMatchingList = zod_1.z
329
354
  .object({
330
355
  id: zod_1.z.string(),
@@ -333,15 +358,15 @@ const TrafficMatchingList = zod_1.z
333
358
  items: zod_1.z.array(zod_1.z.unknown()).optional(),
334
359
  metadata: Metadata.optional(),
335
360
  })
336
- .passthrough();
337
- // Verified against the live Integration API (10.6.106). The WAN and
361
+ .loose();
362
+ // Verified against the live Integration API (11.0.83). The WAN and
338
363
  // site-to-site-tunnel list rows are intentionally minimal in the API.
339
364
  const Wan = zod_1.z
340
365
  .object({
341
366
  id: zod_1.z.string(),
342
367
  name: zod_1.z.string().optional(),
343
368
  })
344
- .passthrough();
369
+ .loose();
345
370
  const VpnServer = zod_1.z
346
371
  .object({
347
372
  id: zod_1.z.string(),
@@ -350,21 +375,21 @@ const VpnServer = zod_1.z
350
375
  enabled: zod_1.z.boolean().optional(),
351
376
  metadata: Metadata.optional(),
352
377
  })
353
- .passthrough();
378
+ .loose();
354
379
  // No tunnels present in the verification environment — keep loose.
355
380
  const VpnTunnel = zod_1.z
356
381
  .object({
357
382
  id: zod_1.z.string(),
358
383
  name: zod_1.z.string().optional(),
359
384
  })
360
- .passthrough();
385
+ .loose();
361
386
  const RadiusProfile = zod_1.z
362
387
  .object({
363
388
  id: zod_1.z.string(),
364
389
  name: zod_1.z.string().optional(),
365
390
  metadata: Metadata.optional(),
366
391
  })
367
- .passthrough();
392
+ .loose();
368
393
  // No tags present in the verification environment — documented shape.
369
394
  const DeviceTag = zod_1.z
370
395
  .object({
@@ -372,27 +397,27 @@ const DeviceTag = zod_1.z
372
397
  name: zod_1.z.string().optional(),
373
398
  deviceIds: zod_1.z.array(zod_1.z.string()).optional(),
374
399
  })
375
- .passthrough();
400
+ .loose();
376
401
  // DPI category/application ids are NUMERIC (verified live).
377
402
  const DpiCategory = zod_1.z
378
403
  .object({
379
404
  id: zod_1.z.number(),
380
405
  name: zod_1.z.string().optional(),
381
406
  })
382
- .passthrough();
407
+ .loose();
383
408
  const DpiApplication = zod_1.z
384
409
  .object({
385
410
  id: zod_1.z.number(),
386
411
  name: zod_1.z.string().optional(),
387
412
  })
388
- .passthrough();
413
+ .loose();
389
414
  // Countries key on `code` (ISO alpha-2), NOT `id` (verified live).
390
415
  const Country = zod_1.z
391
416
  .object({
392
417
  code: zod_1.z.string(),
393
418
  name: zod_1.z.string().optional(),
394
419
  })
395
- .passthrough();
420
+ .loose();
396
421
  // No pending devices in the verification environment — keep loose.
397
422
  const PendingDevice = zod_1.z
398
423
  .object({
@@ -401,7 +426,31 @@ const PendingDevice = zod_1.z
401
426
  ipAddress: zod_1.z.string().optional(),
402
427
  firmwareVersion: zod_1.z.string().optional(),
403
428
  })
404
- .passthrough();
429
+ .loose();
430
+ // A write tool returns either the API's echo of the resource or, with dryRun,
431
+ // a preview of the request it would send. The SDK validates both against the
432
+ // one outputSchema, so the resource's key fields relax to optional alongside
433
+ // the preview fields; read tools keep their stricter schemas.
434
+ const DryRunPreview = {
435
+ dryRun: zod_1.z.literal(true).optional(),
436
+ wouldExecute: zod_1.z
437
+ .object({ method: zod_1.z.string(), path: zod_1.z.string(), body: zod_1.z.unknown().optional() })
438
+ .loose()
439
+ .optional(),
440
+ confirmationRequired: zod_1.z.string().optional(),
441
+ };
442
+ /**
443
+ * The outputSchema to publish for a tool: always a loose object, whether the
444
+ * tool passed a raw shape or a Zod object (the SDK accepts both). Wrapping a
445
+ * Zod object in z.looseObject() would build a broken schema that makes
446
+ * tools/list throw for every tool.
447
+ */
448
+ function looseOutput(schema) {
449
+ return schema instanceof zod_1.z.ZodObject ? schema.loose() : zod_1.z.looseObject(schema);
450
+ }
451
+ function writeResult(shape) {
452
+ return { ...zod_1.z.object(shape).partial().shape, ...DryRunPreview };
453
+ }
405
454
  exports.getInfoOutputSchema = ApplicationInfo.shape;
406
455
  exports.listSitesOutputSchema = {
407
456
  ...PaginationFields,
@@ -532,7 +581,7 @@ exports.HistoryClient = zod_1.z
532
581
  is_wired: zod_1.z.boolean().optional(),
533
582
  status: zod_1.z.string().optional(),
534
583
  })
535
- .passthrough();
584
+ .loose();
536
585
  exports.ClientSession = zod_1.z
537
586
  .object({
538
587
  _id: zod_1.z.string(),
@@ -545,15 +594,18 @@ exports.ClientSession = zod_1.z
545
594
  is_wired: zod_1.z.boolean().optional(),
546
595
  roaming_sessions: zod_1.z.array(zod_1.z.unknown()).optional(),
547
596
  })
548
- .passthrough();
597
+ .loose();
549
598
  /** Envelope returned by the controller's stat/session endpoint. */
550
599
  exports.ClientSessionEnvelope = zod_1.z.object({
551
- meta: zod_1.z.object({ rc: zod_1.z.literal("ok") }).passthrough(),
600
+ meta: zod_1.z.object({ rc: zod_1.z.literal("ok") }).loose(),
552
601
  data: zod_1.z.array(exports.ClientSession),
553
602
  });
554
603
  exports.listClientHistoryOutputSchema = {
555
604
  data: zod_1.z.array(exports.HistoryClient),
556
605
  count: zod_1.z.number(),
606
+ totalCount: zod_1.z.number(),
607
+ limit: zod_1.z.number(),
608
+ truncated: zod_1.z.boolean(),
557
609
  withinHours: zod_1.z.number(),
558
610
  coverage: zod_1.z.string(),
559
611
  };
@@ -0,0 +1,7 @@
1
+ import { z } from "zod";
2
+ /**
3
+ * An Integration API resource ID. IDs are interpolated into request paths, so
4
+ * only the plain ID charset is accepted — anything else could smuggle a query
5
+ * (`?force=true`), a fragment, or extra path segments into the request.
6
+ */
7
+ export declare function resourceId(): z.ZodString;
@@ -0,0 +1,12 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.resourceId = resourceId;
4
+ const zod_1 = require("zod");
5
+ /**
6
+ * An Integration API resource ID. IDs are interpolated into request paths, so
7
+ * only the plain ID charset is accepted — anything else could smuggle a query
8
+ * (`?force=true`), a fragment, or extra path segments into the request.
9
+ */
10
+ function resourceId() {
11
+ return zod_1.z.string().regex(/^[A-Za-z0-9_-]{1,64}$/, "Use an ID returned by a list or get tool");
12
+ }
@@ -7,6 +7,8 @@ export declare function formatSuccess(data: unknown, opts?: {
7
7
  }[];
8
8
  structuredContent?: Record<string, unknown>;
9
9
  };
10
+ /** The message of a thrown value, which need not be an Error. */
11
+ export declare function errorMessage(err: unknown): string;
10
12
  export declare function formatError(err: unknown): {
11
13
  content: {
12
14
  type: "text";
@@ -1,10 +1,11 @@
1
1
  "use strict";
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
3
  exports.formatSuccess = formatSuccess;
4
+ exports.errorMessage = errorMessage;
4
5
  exports.formatError = formatError;
5
6
  function formatSuccess(data, opts) {
6
7
  const result = {
7
- content: [{ type: "text", text: JSON.stringify(data, null, 2) }],
8
+ content: [{ type: "text", text: JSON.stringify(data) }],
8
9
  };
9
10
  if (opts?.structured &&
10
11
  data !== null &&
@@ -14,10 +15,35 @@ function formatSuccess(data, opts) {
14
15
  }
15
16
  return result;
16
17
  }
18
+ /** The message of a thrown value, which need not be an Error. */
19
+ function errorMessage(err) {
20
+ return err instanceof Error ? err.message : String(err);
21
+ }
17
22
  function formatError(err) {
18
- const message = err instanceof Error ? err.message : String(err);
23
+ let message = errorMessage(err);
24
+ // fetch reports every network failure as "fetch failed" and keeps the reason
25
+ // (ECONNREFUSED, ENOTFOUND, a certificate error) in cause.
26
+ const cause = err instanceof Error ? err.cause : undefined;
27
+ const code = cause instanceof Error && "code" in cause && typeof cause.code === "string" ? cause.code : undefined;
28
+ if (code) {
29
+ message += ` (${code})`;
30
+ message += tlsHint(code);
31
+ }
19
32
  return {
20
33
  content: [{ type: "text", text: `Error: ${message}` }],
21
34
  isError: true,
22
35
  };
23
36
  }
37
+ /**
38
+ * What to do about a certificate that did not verify. Pinning makes a
39
+ * console's own certificate trusted, but it does not lift the validity dates,
40
+ * so an expired certificate gets its own advice.
41
+ */
42
+ function tlsHint(code) {
43
+ if (code === "CERT_HAS_EXPIRED" || code === "CERT_NOT_YET_VALID") {
44
+ return ". The console's TLS certificate is outside its validity dates; check the clocks on the console and this machine, and renew the certificate if it has expired (pinning it with UNIFI_NETWORK_CA_CERT does not override the dates)";
45
+ }
46
+ if (!/CERT|SELF_SIGNED|LEAF_SIGNATURE/.test(code))
47
+ return "";
48
+ return ". The console's TLS certificate did not verify. For a local console with a self-signed certificate, save the certificate and point UNIFI_NETWORK_CA_CERT at it; if it is already set, the console has a new certificate, so save it again. Against api.ui.com, something between you and Ubiquiti presented this certificate: do not work around it";
49
+ }
@@ -1,14 +1,48 @@
1
+ import { z } from "zod";
1
2
  import type { ToolAnnotations } from "@modelcontextprotocol/sdk/types.js";
3
+ /** The confirm input of a DESTRUCTIVE tool. */
4
+ export declare const confirmDestructive: () => z.ZodOptional<z.ZodBoolean>;
2
5
  export declare const READ_ONLY: ToolAnnotations;
3
6
  export declare const WRITE: ToolAnnotations;
4
7
  export declare const WRITE_NOT_IDEMPOTENT: ToolAnnotations;
5
8
  export declare const DESTRUCTIVE: ToolAnnotations;
6
- export declare function formatDryRun(method: string, path: string, body?: unknown): {
9
+ /**
10
+ * A dry run's result. confirmationRequired is the reason the real call needs
11
+ * confirm, if it does: the preview is what the user approves, so it has to
12
+ * say that the change opens access before they approve it, not after.
13
+ */
14
+ export declare function formatDryRun(method: string, path: string, body?: unknown, confirmationRequired?: string): {
7
15
  content: {
8
16
  type: "text";
9
17
  text: string;
10
18
  }[];
19
+ structuredContent: Record<string, unknown>;
11
20
  };
21
+ /**
22
+ * Confirmation for a write that is routine in general but, with these inputs,
23
+ * opens access — an ALLOW rule, an open SSID, a batch of guest codes. reason
24
+ * says what opens, or is undefined when nothing does; a dry run changes
25
+ * nothing, so it never needs confirming.
26
+ */
27
+ export declare function requireConfirmationIf(reason: string | undefined, confirm: boolean | undefined, dryRun: boolean | undefined): {
28
+ content: {
29
+ type: "text";
30
+ text: string;
31
+ }[];
32
+ isError: true;
33
+ } | null;
34
+ /** How a rule in an evaluation order treats the traffic it matches. */
35
+ export interface RuleEffect {
36
+ letsThrough: boolean;
37
+ blocks: boolean;
38
+ }
39
+ /**
40
+ * Whether a reorder opens access: whether it moves a rule that lets traffic
41
+ * through ahead of one that blocks, which it used to follow. Only rules in
42
+ * both orders are compared, and effect is asked only about the rules a
43
+ * reorder actually moves past one another. IDs are compared without case.
44
+ */
45
+ export declare function reorderOpens(before: readonly string[], after: readonly string[], effect: (id: string) => Promise<RuleEffect>): Promise<boolean>;
12
46
  export declare function requireConfirmation(confirm: boolean | undefined, action: string): {
13
47
  content: {
14
48
  type: "text";
@@ -1,38 +1,88 @@
1
1
  "use strict";
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
- exports.DESTRUCTIVE = exports.WRITE_NOT_IDEMPOTENT = exports.WRITE = exports.READ_ONLY = void 0;
3
+ exports.DESTRUCTIVE = exports.WRITE_NOT_IDEMPOTENT = exports.WRITE = exports.READ_ONLY = exports.confirmDestructive = void 0;
4
4
  exports.formatDryRun = formatDryRun;
5
+ exports.requireConfirmationIf = requireConfirmationIf;
6
+ exports.reorderOpens = reorderOpens;
5
7
  exports.requireConfirmation = requireConfirmation;
8
+ const zod_1 = require("zod");
9
+ const write_log_js_1 = require("../write-log.js");
10
+ /** The confirm input of a DESTRUCTIVE tool. */
11
+ const confirmDestructive = () => zod_1.z.boolean().optional().describe("Must be true to execute this destructive action; not needed with dryRun: true");
12
+ exports.confirmDestructive = confirmDestructive;
6
13
  exports.READ_ONLY = {
7
14
  readOnlyHint: true,
8
15
  destructiveHint: false,
16
+ openWorldHint: false,
9
17
  };
10
18
  exports.WRITE = {
11
19
  readOnlyHint: false,
12
20
  destructiveHint: false,
13
21
  idempotentHint: true,
22
+ openWorldHint: false,
14
23
  };
15
24
  exports.WRITE_NOT_IDEMPOTENT = {
16
25
  readOnlyHint: false,
17
26
  destructiveHint: false,
18
27
  idempotentHint: false,
28
+ openWorldHint: false,
19
29
  };
20
30
  exports.DESTRUCTIVE = {
21
31
  readOnlyHint: false,
22
32
  destructiveHint: true,
23
33
  idempotentHint: false,
34
+ openWorldHint: false,
24
35
  };
25
- function formatDryRun(method, path, body) {
36
+ /**
37
+ * A dry run's result. confirmationRequired is the reason the real call needs
38
+ * confirm, if it does: the preview is what the user approves, so it has to
39
+ * say that the change opens access before they approve it, not after.
40
+ */
41
+ function formatDryRun(method, path, body, confirmationRequired) {
26
42
  const preview = {
27
43
  dryRun: true,
28
44
  wouldExecute: { method, path, ...(body !== undefined ? { body } : {}) },
45
+ ...(confirmationRequired !== undefined ? { confirmationRequired } : {}),
29
46
  };
47
+ // structuredContent is required: the SDK rejects a result without it from
48
+ // any tool that declares an outputSchema.
30
49
  return {
31
- content: [{ type: "text", text: JSON.stringify(preview, null, 2) }],
50
+ content: [{ type: "text", text: JSON.stringify(preview) }],
51
+ structuredContent: preview,
32
52
  };
33
53
  }
54
+ /**
55
+ * Confirmation for a write that is routine in general but, with these inputs,
56
+ * opens access — an ALLOW rule, an open SSID, a batch of guest codes. reason
57
+ * says what opens, or is undefined when nothing does; a dry run changes
58
+ * nothing, so it never needs confirming.
59
+ */
60
+ function requireConfirmationIf(reason, confirm, dryRun) {
61
+ return reason === undefined || dryRun ? null : requireConfirmation(confirm, reason);
62
+ }
63
+ /**
64
+ * Whether a reorder opens access: whether it moves a rule that lets traffic
65
+ * through ahead of one that blocks, which it used to follow. Only rules in
66
+ * both orders are compared, and effect is asked only about the rules a
67
+ * reorder actually moves past one another. IDs are compared without case.
68
+ */
69
+ async function reorderOpens(before, after, effect) {
70
+ const rank = new Map(before.map((id, index) => [id.toLowerCase(), index]));
71
+ const passes = [];
72
+ after.forEach((ahead, i) => {
73
+ for (const behind of after.slice(i + 1)) {
74
+ const [was, other] = [rank.get(ahead.toLowerCase()), rank.get(behind.toLowerCase())];
75
+ if (was !== undefined && other !== undefined && was > other)
76
+ passes.push([ahead, behind]);
77
+ }
78
+ });
79
+ const ids = [...new Set(passes.flat())];
80
+ const effects = new Map(await Promise.all(ids.map(async (id) => [id, await effect(id)])));
81
+ return passes.some(([ahead, behind]) => effects.get(ahead)?.letsThrough === true && effects.get(behind)?.blocks === true);
82
+ }
34
83
  function requireConfirmation(confirm, action) {
35
84
  if (confirm !== true) {
85
+ (0, write_log_js_1.noteRefusal)("confirmation_required");
36
86
  return {
37
87
  content: [
38
88
  {
@@ -0,0 +1,24 @@
1
+ /**
2
+ * WiFi broadcasts come back from the API with their passphrases in plaintext
3
+ * (`securityConfiguration.passphrase`, and one per VLAN in
4
+ * `securityConfiguration.presharedKeys[].passphrase` on PPSK SSIDs). None of
5
+ * this is in the published docs. Tools redact these values by default so they
6
+ * do not land in LLM transcripts; callers opt in with `includeSecrets: true`.
7
+ */
8
+ export declare const REDACTED = "[REDACTED]";
9
+ /** Deep copy of `data` with every secret-named string value replaced by REDACTED. */
10
+ export declare function redactSecrets<T>(data: T): T;
11
+ /**
12
+ * Replace each REDACTED placeholder in `value` with the real value found at the
13
+ * same path in `current`. This lets a caller echo back a redacted object from a
14
+ * read tool without overwriting the passphrase with the placeholder text.
15
+ * Throws if a placeholder has no real value to restore (e.g. on create, or when
16
+ * a new PPSK entry is added without its passphrase).
17
+ *
18
+ * Array items are matched by index, which is only safe while the item is
19
+ * otherwise unchanged: a reordered `presharedKeys[]` must not hand one VLAN's
20
+ * passphrase to another. So a placeholder inside an array item is restored only
21
+ * when that item equals the current item at the same index apart from secrets.
22
+ */
23
+ export declare function restoreRedacted(value: unknown, current: unknown, path?: string): unknown;
24
+ export declare function containsRedacted(value: unknown): boolean;