@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
@@ -6,27 +6,28 @@ const responses_js_1 = require("../utils/responses.js");
6
6
  const query_js_1 = require("../utils/query.js");
7
7
  const safety_js_1 = require("../utils/safety.js");
8
8
  const output_schemas_js_1 = require("../utils/output-schemas.js");
9
+ const params_js_1 = require("../utils/params.js");
9
10
  // Adopt echoes the adopted device — reuse the device detail schema.
10
11
  const adoptDeviceOutputSchema = output_schemas_js_1.getDeviceOutputSchema;
11
12
  function registerDeviceTools(server, client, readOnly = false) {
12
13
  server.registerTool("unifi_list_devices", {
13
14
  description: "List all adopted devices (gateways, switches, APs) at a site. Returns: id, name, model, macAddress, ipAddress, state (ONLINE/OFFLINE/etc), supported, firmwareVersion, firmwareUpdatable, features[] (capability tags, e.g. ['switching'] or ['accessPoint']), interfaces[] (e.g. ['ports'] or ['radios']). NOTE: features/interfaces are string arrays here; unifi_get_device expands them into objects. Use for: device inventory; pair with unifi_get_device for full config (port table, radios) and unifi_get_device_statistics for live metrics.",
14
15
  inputSchema: {
15
- siteId: zod_1.z.string().describe("Site ID"),
16
+ siteId: (0, params_js_1.resourceId)().describe("Site ID"),
16
17
  offset: zod_1.z
17
18
  .number()
18
19
  .int()
19
20
  .nonnegative()
20
21
  .optional()
21
- .describe("Number of records to skip (default: 0)"),
22
+ .describe("Number of records to skip"),
22
23
  limit: zod_1.z
23
24
  .number()
24
25
  .int()
25
26
  .min(0)
26
27
  .max(200)
27
28
  .optional()
28
- .describe("Number of records to return (default: 25, max: 200)"),
29
- filter: zod_1.z.string().optional().describe("Filter expression"),
29
+ .describe("Number of records to return (API default: 25)"),
30
+ 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
31
  },
31
32
  outputSchema: output_schemas_js_1.listDevicesOutputSchema,
32
33
  annotations: safety_js_1.READ_ONLY,
@@ -41,10 +42,10 @@ function registerDeviceTools(server, client, readOnly = false) {
41
42
  }
42
43
  });
43
44
  server.registerTool("unifi_get_device", {
44
- description: "Get full configuration for a device. Returns (in addition to list fields): supported, firmwareUpdatable, adoptedAt, provisionedAt, configurationId, uplink.deviceId, features (object keyed by capability: switching {lags[]} / accessPoint {}), interfaces.ports[] for switches ({idx, state, connector, maxSpeedMbps, speedMbps, poe:{standard, type, enabled, state}}), interfaces.radios[] for APs ({wlanStandard, frequencyGHz, channelWidthMHz, channel}). 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
+ 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
46
  inputSchema: {
46
- siteId: zod_1.z.string().describe("Site ID"),
47
- deviceId: zod_1.z.string().describe("Device ID"),
47
+ siteId: (0, params_js_1.resourceId)().describe("Site ID"),
48
+ deviceId: (0, params_js_1.resourceId)().describe("Device ID"),
48
49
  },
49
50
  outputSchema: output_schemas_js_1.getDeviceOutputSchema,
50
51
  annotations: safety_js_1.READ_ONLY,
@@ -58,10 +59,10 @@ function registerDeviceTools(server, client, readOnly = false) {
58
59
  }
59
60
  });
60
61
  server.registerTool("unifi_get_device_statistics", {
61
- description: "Get latest live statistics for a device. Returns: uptimeSec, lastHeartbeatAt, nextHeartbeatAt, loadAverage1/5/15Min, cpuUtilizationPct, memoryUtilizationPct, uplink (txRateBps, rxRateBps), interfaces.radios[] for APs ({frequencyGHz, txRetriesPct}). NOTE: verified against 10.6.106 — 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
+ 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
63
  inputSchema: {
63
- siteId: zod_1.z.string().describe("Site ID"),
64
- deviceId: zod_1.z.string().describe("Device ID"),
64
+ siteId: (0, params_js_1.resourceId)().describe("Site ID"),
65
+ deviceId: (0, params_js_1.resourceId)().describe("Device ID"),
65
66
  },
66
67
  outputSchema: output_schemas_js_1.getDeviceStatisticsOutputSchema,
67
68
  annotations: safety_js_1.READ_ONLY,
@@ -75,22 +76,22 @@ function registerDeviceTools(server, client, readOnly = false) {
75
76
  }
76
77
  });
77
78
  server.registerTool("unifi_list_pending_devices", {
78
- description: "List devices pending adoption across all sites (global endpoint, not site-scoped). Returns: basic device info per pending device (macAddress, model, ipAddress, firmwareVersion, etc. — exact per-row schema is not rendered in the 10.6.106 docs). Use for: discovering new devices on the network before calling unifi_adopt_device.",
79
+ 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
80
  inputSchema: {
80
81
  offset: zod_1.z
81
82
  .number()
82
83
  .int()
83
84
  .nonnegative()
84
85
  .optional()
85
- .describe("Number of records to skip (default: 0)"),
86
+ .describe("Number of records to skip"),
86
87
  limit: zod_1.z
87
88
  .number()
88
89
  .int()
89
90
  .min(0)
90
91
  .max(200)
91
92
  .optional()
92
- .describe("Number of records to return (default: 25, max: 200)"),
93
- filter: zod_1.z.string().optional().describe("Filter expression"),
93
+ .describe("Number of records to return (API default: 25)"),
94
+ filter: zod_1.z.string().optional().describe("Filter expression, e.g. model.eq('U7 Pro')"),
94
95
  },
95
96
  outputSchema: output_schemas_js_1.listPendingDevicesOutputSchema,
96
97
  annotations: safety_js_1.READ_ONLY,
@@ -109,14 +110,14 @@ function registerDeviceTools(server, client, readOnly = false) {
109
110
  server.registerTool("unifi_adopt_device", {
110
111
  description: "Adopt a pending device into a site by MAC address. The device must already appear in unifi_list_pending_devices. Idempotency: not safe to retry — re-adopting may error or duplicate.",
111
112
  inputSchema: {
112
- siteId: zod_1.z.string().describe("Site ID"),
113
+ siteId: (0, params_js_1.resourceId)().describe("Site ID"),
113
114
  macAddress: zod_1.z.string().describe("MAC address of the device"),
114
115
  // Required by the API; kept optional here and defaulted to false below
115
116
  // so callers need not pass it explicitly.
116
117
  ignoreDeviceLimit: zod_1.z.boolean().optional().describe("Ignore device limit when adopting (required by the API; sent as false when omitted)"),
117
118
  dryRun: zod_1.z.boolean().optional().describe("Preview this action without executing it"),
118
119
  },
119
- outputSchema: adoptDeviceOutputSchema,
120
+ outputSchema: (0, output_schemas_js_1.writeResult)(adoptDeviceOutputSchema),
120
121
  annotations: safety_js_1.WRITE_NOT_IDEMPOTENT,
121
122
  }, async ({ siteId, macAddress, ignoreDeviceLimit, dryRun }) => {
122
123
  const body = {
@@ -136,18 +137,18 @@ function registerDeviceTools(server, client, readOnly = false) {
136
137
  server.registerTool("unifi_remove_device", {
137
138
  description: "DESTRUCTIVE: Remove (unadopt) a device from a site. If the device is online, it will be reset to factory defaults",
138
139
  inputSchema: {
139
- siteId: zod_1.z.string().describe("Site ID"),
140
- deviceId: zod_1.z.string().describe("Device ID"),
141
- confirm: zod_1.z.boolean().optional().describe("Must be true to execute this destructive action"),
140
+ siteId: (0, params_js_1.resourceId)().describe("Site ID"),
141
+ deviceId: (0, params_js_1.resourceId)().describe("Device ID"),
142
+ confirm: (0, safety_js_1.confirmDestructive)(),
142
143
  dryRun: zod_1.z.boolean().optional().describe("Preview this action without executing it"),
143
144
  },
144
145
  annotations: safety_js_1.DESTRUCTIVE,
145
146
  }, async ({ siteId, deviceId, confirm, dryRun }) => {
146
- const guard = (0, safety_js_1.requireConfirmation)(confirm, "This will remove the device from the site. If the device is online, it will be reset to factory defaults");
147
+ const guard = dryRun ? null : (0, safety_js_1.requireConfirmation)(confirm, "This will remove the device from the site. If the device is online, it will be reset to factory defaults");
147
148
  if (guard)
148
149
  return guard;
149
150
  if (dryRun)
150
- return (0, safety_js_1.formatDryRun)("DELETE", `/sites/${siteId}/devices/${deviceId}`, {});
151
+ return (0, safety_js_1.formatDryRun)("DELETE", `/sites/${siteId}/devices/${deviceId}`);
151
152
  try {
152
153
  const data = await client.delete(`/sites/${siteId}/devices/${deviceId}`);
153
154
  return (0, responses_js_1.formatSuccess)(data);
@@ -157,13 +158,13 @@ function registerDeviceTools(server, client, readOnly = false) {
157
158
  }
158
159
  });
159
160
  server.registerTool("unifi_restart_device", {
160
- description: "Restart (reboot) a device. The device will be unreachable for ~1–3 minutes. Idempotent: repeated calls trigger fresh reboots.",
161
+ description: "Restart (reboot) a device. The device will be unreachable for ~1–3 minutes. Not idempotent: each call triggers a fresh reboot.",
161
162
  inputSchema: {
162
- siteId: zod_1.z.string().describe("Site ID"),
163
- deviceId: zod_1.z.string().describe("Device ID"),
163
+ siteId: (0, params_js_1.resourceId)().describe("Site ID"),
164
+ deviceId: (0, params_js_1.resourceId)().describe("Device ID"),
164
165
  dryRun: zod_1.z.boolean().optional().describe("Preview this action without executing it"),
165
166
  },
166
- annotations: safety_js_1.WRITE,
167
+ annotations: safety_js_1.WRITE_NOT_IDEMPOTENT,
167
168
  }, async ({ siteId, deviceId, dryRun }) => {
168
169
  const body = { action: "RESTART" };
169
170
  if (dryRun)
@@ -179,12 +180,12 @@ function registerDeviceTools(server, client, readOnly = false) {
179
180
  server.registerTool("unifi_power_cycle_port", {
180
181
  description: "Power-cycle PoE on a specific switch port (briefly drops then restores power). portIdx is the port number (1-based) as shown in unifi_get_device interfaces.ports[].idx. Use for: rebooting a PoE-powered camera/AP without touching the device.",
181
182
  inputSchema: {
182
- siteId: zod_1.z.string().describe("Site ID"),
183
- deviceId: zod_1.z.string().describe("Device ID"),
184
- portIdx: zod_1.z.number().int().describe("Port index number"),
183
+ siteId: (0, params_js_1.resourceId)().describe("Site ID"),
184
+ deviceId: (0, params_js_1.resourceId)().describe("Device ID"),
185
+ portIdx: zod_1.z.number().int().min(1).describe("Port index number (1-based)"),
185
186
  dryRun: zod_1.z.boolean().optional().describe("Preview this action without executing it"),
186
187
  },
187
- annotations: safety_js_1.WRITE,
188
+ annotations: safety_js_1.WRITE_NOT_IDEMPOTENT,
188
189
  }, async ({ siteId, deviceId, portIdx, dryRun }) => {
189
190
  const body = { action: "POWER_CYCLE" };
190
191
  if (dryRun)
@@ -5,29 +5,103 @@ 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
+ const params_js_1 = require("../utils/params.js");
11
+ // Type-specific request-body fields, from the 11.0.83 POST/PUT schemas (every
12
+ // type also takes type, enabled, domain). MX/TXT/SRV/FORWARD_DOMAIN have no TTL.
13
+ const DNS_TYPE_FIELDS = {
14
+ A_RECORD: ["ipv4Address", "ttlSeconds"],
15
+ AAAA_RECORD: ["ipv6Address", "ttlSeconds"],
16
+ CNAME_RECORD: ["targetDomain", "ttlSeconds"],
17
+ MX_RECORD: ["mailServerDomain", "priority"],
18
+ TXT_RECORD: ["text"],
19
+ SRV_RECORD: ["serverDomain", "service", "protocol", "port", "priority", "weight"],
20
+ FORWARD_DOMAIN: ["ipAddress"],
21
+ };
22
+ 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.";
23
+ const dnsType = zod_1.z
24
+ .enum([
25
+ "A_RECORD",
26
+ "AAAA_RECORD",
27
+ "CNAME_RECORD",
28
+ "MX_RECORD",
29
+ "TXT_RECORD",
30
+ "SRV_RECORD",
31
+ "FORWARD_DOMAIN",
32
+ ])
33
+ .describe("DNS record type");
34
+ function fieldsForDnsType(type) {
35
+ if (typeof type !== "string" || !(type in DNS_TYPE_FIELDS)) {
36
+ throw new Error(`Unsupported DNS policy type: ${String(type)}`);
37
+ }
38
+ const specific = DNS_TYPE_FIELDS[type];
39
+ return {
40
+ allowed: ["type", "enabled", "domain", ...specific],
41
+ required: ["type", "enabled", "domain", ...specific],
42
+ };
43
+ }
44
+ function checkDnsBody(body, updates) {
45
+ const { allowed, required } = fieldsForDnsType(body.type);
46
+ const rejected = (0, full_put_js_1.unsupportedFields)(updates, allowed);
47
+ if (rejected.length > 0) {
48
+ throw new Error(`Fields not accepted for type ${String(body.type)}: ${rejected.join(", ")}`);
49
+ }
50
+ const missing = (0, full_put_js_1.missingFields)(body, required);
51
+ if (missing.length > 0) {
52
+ throw new Error(`Missing required fields for type ${String(body.type)}: ${missing.join(", ")}`);
53
+ }
54
+ if ((body.type === "A_RECORD" || body.type === "AAAA_RECORD") &&
55
+ typeof body.ttlSeconds === "number" &&
56
+ body.ttlSeconds > 86400) {
57
+ throw new Error(`ttlSeconds for ${body.type} must be 0-86400 (604800 is the CNAME_RECORD maximum)`);
58
+ }
59
+ }
60
+ const uint16 = (description) => zod_1.z.number().int().min(0).max(65535).optional().describe(description);
61
+ const hostname = (description) => zod_1.z.string().min(1).max(127).optional().describe(description);
62
+ const dnsFields = {
63
+ ipv4Address: zod_1.z.string().optional().describe("IPv4 address. A_RECORD."),
64
+ ipv6Address: zod_1.z.string().optional().describe("IPv6 address. AAAA_RECORD."),
65
+ targetDomain: hostname("Canonical name the alias points at. CNAME_RECORD."),
66
+ mailServerDomain: hostname("Mail server domain. MX_RECORD."),
67
+ 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."),
68
+ serverDomain: hostname("Domain of the server running the service. SRV_RECORD."),
69
+ service: zod_1.z.string().optional().describe("Service, e.g. _ldap. SRV_RECORD."),
70
+ protocol: zod_1.z.string().optional().describe("Protocol, e.g. _tcp. SRV_RECORD."),
71
+ port: uint16("Service port. SRV_RECORD."),
72
+ priority: uint16("Priority; lower is preferred. MX_RECORD, SRV_RECORD."),
73
+ weight: uint16("Relative weight among records with the same priority. SRV_RECORD."),
74
+ ipAddress: zod_1.z.string().optional().describe("IP address of the DNS server queries are forwarded to. FORWARD_DOMAIN."),
75
+ ttlSeconds: zod_1.z
76
+ .number()
77
+ .int()
78
+ .min(0)
79
+ .max(604800)
80
+ .optional()
81
+ .describe("Time to live in seconds: 0-86400 for A/AAAA_RECORD, 0-604800 for CNAME_RECORD"),
82
+ };
9
83
  function registerDnsPolicyTools(server, client, readOnly = false) {
10
84
  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, ipv4Address, ttlSeconds, metadata.origin.",
85
+ 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
86
  inputSchema: {
13
- siteId: zod_1.z.string().describe("Site ID"),
87
+ siteId: (0, params_js_1.resourceId)().describe("Site ID"),
14
88
  offset: zod_1.z
15
89
  .number()
16
90
  .int()
17
91
  .nonnegative()
18
92
  .optional()
19
- .describe("Number of records to skip (default: 0)"),
93
+ .describe("Number of records to skip"),
20
94
  limit: zod_1.z
21
95
  .number()
22
96
  .int()
23
97
  .min(0)
24
98
  .max(200)
25
99
  .optional()
26
- .describe("Number of records to return (default: 25, max: 200)"),
100
+ .describe("Number of records to return (API default: 25)"),
27
101
  filter: zod_1.z
28
102
  .string()
29
103
  .optional()
30
- .describe("Filter expression"),
104
+ .describe("Filter expression, e.g. type.eq('A_RECORD')"),
31
105
  },
32
106
  outputSchema: output_schemas_js_1.listDnsPoliciesOutputSchema,
33
107
  annotations: safety_js_1.READ_ONLY,
@@ -44,8 +118,8 @@ function registerDnsPolicyTools(server, client, readOnly = false) {
44
118
  server.registerTool("unifi_get_dns_policy", {
45
119
  description: "Get a specific DNS policy by ID (same fields as the list entry).",
46
120
  inputSchema: {
47
- siteId: zod_1.z.string().describe("Site ID"),
48
- dnsPolicyId: zod_1.z.string().describe("DNS policy ID"),
121
+ siteId: (0, params_js_1.resourceId)().describe("Site ID"),
122
+ dnsPolicyId: (0, params_js_1.resourceId)().describe("DNS policy ID"),
49
123
  },
50
124
  outputSchema: output_schemas_js_1.dnsPolicyOutputSchema,
51
125
  annotations: safety_js_1.READ_ONLY,
@@ -61,39 +135,24 @@ function registerDnsPolicyTools(server, client, readOnly = false) {
61
135
  if (readOnly)
62
136
  return;
63
137
  server.registerTool("unifi_create_dns_policy", {
64
- description: "Create a new DNS policy",
138
+ 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
139
  inputSchema: {
66
- siteId: zod_1.z.string().describe("Site ID"),
67
- type: zod_1.z
68
- .enum([
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
+ siteId: (0, params_js_1.resourceId)().describe("Site ID"),
141
+ ...dnsFields,
142
+ type: dnsType,
78
143
  enabled: zod_1.z.boolean().describe("Whether the DNS policy is enabled"),
79
144
  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
145
  dryRun: zod_1.z
88
146
  .boolean()
89
147
  .optional()
90
148
  .describe("Preview this action without executing it"),
91
149
  },
92
- outputSchema: output_schemas_js_1.dnsPolicyOutputSchema,
150
+ outputSchema: (0, output_schemas_js_1.writeResult)(output_schemas_js_1.dnsPolicyOutputSchema),
93
151
  annotations: safety_js_1.WRITE_NOT_IDEMPOTENT,
94
- }, async ({ siteId, type, enabled, domain, ipv4Address, ttlSeconds, dryRun }) => {
152
+ }, async ({ siteId, dryRun, ...fields }) => {
95
153
  try {
96
- const body = { type, enabled, domain, ipv4Address, ttlSeconds };
154
+ const body = (0, full_put_js_1.definedEntries)(fields);
155
+ checkDnsBody(body, body);
97
156
  if (dryRun)
98
157
  return (0, safety_js_1.formatDryRun)("POST", `/sites/${siteId}/dns/policies`, body);
99
158
  const data = await client.post(`/sites/${siteId}/dns/policies`, body);
@@ -104,44 +163,35 @@ function registerDnsPolicyTools(server, client, readOnly = false) {
104
163
  }
105
164
  });
106
165
  server.registerTool("unifi_update_dns_policy", {
107
- description: "Update a DNS policy",
166
+ 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
167
  inputSchema: {
109
- siteId: zod_1.z.string().describe("Site ID"),
110
- dnsPolicyId: zod_1.z.string().describe("DNS policy ID"),
111
- type: zod_1.z
112
- .enum([
113
- "A_RECORD",
114
- "AAAA_RECORD",
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"),
168
+ siteId: (0, params_js_1.resourceId)().describe("Site ID"),
169
+ dnsPolicyId: (0, params_js_1.resourceId)().describe("DNS policy ID"),
170
+ ...dnsFields,
171
+ type: dnsType.optional(),
172
+ enabled: zod_1.z.boolean().optional().describe("Whether the DNS policy is enabled"),
173
+ domain: zod_1.z.string().min(1).max(127).optional().describe("Domain name"),
131
174
  dryRun: zod_1.z
132
175
  .boolean()
133
176
  .optional()
134
- .describe("Preview this action without executing it"),
177
+ .describe("Preview the merged PUT body without executing it (still reads the current policy)"),
135
178
  },
136
- outputSchema: output_schemas_js_1.dnsPolicyOutputSchema,
179
+ outputSchema: (0, output_schemas_js_1.writeResult)(output_schemas_js_1.dnsPolicyOutputSchema),
137
180
  annotations: safety_js_1.WRITE,
138
- }, async ({ siteId, dnsPolicyId, type, enabled, domain, ipv4Address, ttlSeconds, dryRun }) => {
181
+ }, async ({ siteId, dnsPolicyId, dryRun, ...fields }) => {
139
182
  try {
140
- const body = { type, enabled, domain, ipv4Address, ttlSeconds };
141
- if (dryRun)
142
- return (0, safety_js_1.formatDryRun)("PUT", `/sites/${siteId}/dns/policies/${dnsPolicyId}`, body);
143
- const data = await client.put(`/sites/${siteId}/dns/policies/${dnsPolicyId}`, body);
144
- return (0, responses_js_1.formatSuccess)(data, { structured: true });
183
+ const path = `/sites/${siteId}/dns/policies/${dnsPolicyId}`;
184
+ return await (0, full_put_js_1.withResourceLock)(path, async () => {
185
+ const current = (0, full_put_js_1.asObject)(await client.get(path), "the current DNS policy");
186
+ const updates = (0, full_put_js_1.definedEntries)(fields);
187
+ const { allowed } = fieldsForDnsType(updates.type ?? current.type);
188
+ const body = (0, full_put_js_1.mergeForPut)(current, updates, allowed);
189
+ checkDnsBody(body, updates);
190
+ if (dryRun)
191
+ return (0, safety_js_1.formatDryRun)("PUT", path, body);
192
+ const data = await client.put(path, body);
193
+ return (0, responses_js_1.formatSuccess)(data, { structured: true });
194
+ });
145
195
  }
146
196
  catch (err) {
147
197
  return (0, responses_js_1.formatError)(err);
@@ -150,12 +200,9 @@ function registerDnsPolicyTools(server, client, readOnly = false) {
150
200
  server.registerTool("unifi_delete_dns_policy", {
151
201
  description: "DESTRUCTIVE: Delete a DNS policy",
152
202
  inputSchema: {
153
- siteId: zod_1.z.string().describe("Site ID"),
154
- dnsPolicyId: zod_1.z.string().describe("DNS policy ID"),
155
- confirm: zod_1.z
156
- .boolean()
157
- .optional()
158
- .describe("Must be true to execute this destructive action"),
203
+ siteId: (0, params_js_1.resourceId)().describe("Site ID"),
204
+ dnsPolicyId: (0, params_js_1.resourceId)().describe("DNS policy ID"),
205
+ confirm: (0, safety_js_1.confirmDestructive)(),
159
206
  dryRun: zod_1.z
160
207
  .boolean()
161
208
  .optional()
@@ -163,12 +210,12 @@ function registerDnsPolicyTools(server, client, readOnly = false) {
163
210
  },
164
211
  annotations: safety_js_1.DESTRUCTIVE,
165
212
  }, async ({ siteId, dnsPolicyId, confirm, dryRun }) => {
166
- const guard = (0, safety_js_1.requireConfirmation)(confirm, "This will delete the DNS policy");
213
+ const guard = dryRun ? null : (0, safety_js_1.requireConfirmation)(confirm, "This will delete the DNS policy");
167
214
  if (guard)
168
215
  return guard;
169
216
  try {
170
217
  if (dryRun)
171
- return (0, safety_js_1.formatDryRun)("DELETE", `/sites/${siteId}/dns/policies/${dnsPolicyId}`, {});
218
+ return (0, safety_js_1.formatDryRun)("DELETE", `/sites/${siteId}/dns/policies/${dnsPolicyId}`);
172
219
  const data = await client.delete(`/sites/${siteId}/dns/policies/${dnsPolicyId}`);
173
220
  return (0, responses_js_1.formatSuccess)(data);
174
221
  }
@@ -1,3 +1,4 @@
1
1
  import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
2
2
  import { NetworkClient } from "../client.js";
3
- export declare function registerFirewallTools(server: McpServer, client: NetworkClient, readOnly?: boolean): void;
3
+ import { type WanPolicy } from "../utils/wan-exposure.js";
4
+ export declare function registerFirewallTools(server: McpServer, client: NetworkClient, readOnly?: boolean, wan?: WanPolicy): void;