@owine/unifi-network-mcp 2.6.0 → 2.7.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/dist/tools/acl.js CHANGED
@@ -5,9 +5,10 @@ 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 output_schemas_js_1 = require("../utils/output-schemas.js");
8
9
  function registerAclTools(server, client, readOnly = false) {
9
10
  server.registerTool("unifi_list_acl_rules", {
10
- description: "List all ACL rules at a site",
11
+ description: "List ACL rules (switch/AP-level access control lists, distinct from zone-based firewall policies) at a site. Returns: id, type (IPV4/MAC), name, enabled, action (ALLOW/BLOCK), description, protocolFilter, source/destination matchers. ACLs apply earlier in the path than firewall policies.",
11
12
  inputSchema: {
12
13
  siteId: zod_1.z.string().describe("Site ID"),
13
14
  offset: zod_1.z
@@ -28,43 +29,46 @@ function registerAclTools(server, client, readOnly = false) {
28
29
  .optional()
29
30
  .describe("Filter expression"),
30
31
  },
32
+ outputSchema: output_schemas_js_1.listAclRulesOutputSchema,
31
33
  annotations: safety_js_1.READ_ONLY,
32
34
  }, async ({ siteId, offset, limit, filter }) => {
33
35
  try {
34
36
  const query = (0, query_js_1.buildQuery)({ offset, limit, filter });
35
37
  const data = await client.get(`/sites/${siteId}/acl-rules${query}`);
36
- return (0, responses_js_1.formatSuccess)(data);
38
+ return (0, responses_js_1.formatSuccess)(data, { structured: true });
37
39
  }
38
40
  catch (err) {
39
41
  return (0, responses_js_1.formatError)(err);
40
42
  }
41
43
  });
42
44
  server.registerTool("unifi_get_acl_rule", {
43
- description: "Get a specific ACL rule by ID",
45
+ description: "Get a specific ACL rule by ID (full match criteria and action).",
44
46
  inputSchema: {
45
47
  siteId: zod_1.z.string().describe("Site ID"),
46
48
  aclRuleId: zod_1.z.string().describe("ACL rule ID"),
47
49
  },
50
+ outputSchema: output_schemas_js_1.aclRuleOutputSchema,
48
51
  annotations: safety_js_1.READ_ONLY,
49
52
  }, async ({ siteId, aclRuleId }) => {
50
53
  try {
51
54
  const data = await client.get(`/sites/${siteId}/acl-rules/${aclRuleId}`);
52
- return (0, responses_js_1.formatSuccess)(data);
55
+ return (0, responses_js_1.formatSuccess)(data, { structured: true });
53
56
  }
54
57
  catch (err) {
55
58
  return (0, responses_js_1.formatError)(err);
56
59
  }
57
60
  });
58
61
  server.registerTool("unifi_get_acl_rule_ordering", {
59
- description: "Get user-defined ACL rule ordering",
62
+ description: "Get the evaluation order of user-defined ACL rules. Returns: orderedAclRuleIds[]. Rules higher in the list win.",
60
63
  inputSchema: {
61
64
  siteId: zod_1.z.string().describe("Site ID"),
62
65
  },
66
+ outputSchema: output_schemas_js_1.aclRuleOrderingOutputSchema,
63
67
  annotations: safety_js_1.READ_ONLY,
64
68
  }, async ({ siteId }) => {
65
69
  try {
66
70
  const data = await client.get(`/sites/${siteId}/acl-rules/ordering`);
67
- return (0, responses_js_1.formatSuccess)(data);
71
+ return (0, responses_js_1.formatSuccess)(data, { structured: true });
68
72
  }
69
73
  catch (err) {
70
74
  return (0, responses_js_1.formatError)(err);
@@ -93,6 +97,7 @@ function registerAclTools(server, client, readOnly = false) {
93
97
  .optional()
94
98
  .describe("Preview this action without executing it"),
95
99
  },
100
+ outputSchema: output_schemas_js_1.aclRuleOutputSchema,
96
101
  annotations: safety_js_1.WRITE_NOT_IDEMPOTENT,
97
102
  }, async ({ siteId, type, name, enabled, action, description, protocolFilter, dryRun }) => {
98
103
  try {
@@ -104,7 +109,7 @@ function registerAclTools(server, client, readOnly = false) {
104
109
  if (dryRun)
105
110
  return (0, safety_js_1.formatDryRun)("POST", `/sites/${siteId}/acl-rules`, body);
106
111
  const data = await client.post(`/sites/${siteId}/acl-rules`, body);
107
- return (0, responses_js_1.formatSuccess)(data);
112
+ return (0, responses_js_1.formatSuccess)(data, { structured: true });
108
113
  }
109
114
  catch (err) {
110
115
  return (0, responses_js_1.formatError)(err);
@@ -132,6 +137,7 @@ function registerAclTools(server, client, readOnly = false) {
132
137
  .optional()
133
138
  .describe("Preview this action without executing it"),
134
139
  },
140
+ outputSchema: output_schemas_js_1.aclRuleOutputSchema,
135
141
  annotations: safety_js_1.WRITE,
136
142
  }, async ({ siteId, aclRuleId, type, name, enabled, action, description, protocolFilter, dryRun }) => {
137
143
  try {
@@ -143,7 +149,7 @@ function registerAclTools(server, client, readOnly = false) {
143
149
  if (dryRun)
144
150
  return (0, safety_js_1.formatDryRun)("PUT", `/sites/${siteId}/acl-rules/${aclRuleId}`, body);
145
151
  const data = await client.put(`/sites/${siteId}/acl-rules/${aclRuleId}`, body);
146
- return (0, responses_js_1.formatSuccess)(data);
152
+ return (0, responses_js_1.formatSuccess)(data, { structured: true });
147
153
  }
148
154
  catch (err) {
149
155
  return (0, responses_js_1.formatError)(err);
@@ -5,9 +5,10 @@ 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 output_schemas_js_1 = require("../utils/output-schemas.js");
8
9
  function registerClientTools(server, client, readOnly = false) {
9
10
  server.registerTool("unifi_list_clients", {
10
- description: "List all connected clients (wired, wireless, VPN) at a site",
11
+ description: "List currently connected clients at a site. Returns per client: id, name, type (WIRED/WIRELESS/VPN/TELEPORT), macAddress, ipAddress, connectedAt, uplinkDeviceId (the switch/AP they're attached to), access.type. NOTE: verified against 10.4.55 — the Integration API client schema is minimal and identical across types; it does NOT expose signal strength, channel, or per-port binding. Use for: who's online right now. Disconnected/historical clients are NOT in the Integration API.",
11
12
  inputSchema: {
12
13
  siteId: zod_1.z.string().describe("Site ID"),
13
14
  offset: zod_1.z
@@ -25,28 +26,30 @@ function registerClientTools(server, client, readOnly = false) {
25
26
  .describe("Number of records to return (default: 25, max: 200)"),
26
27
  filter: zod_1.z.string().optional().describe("Filter expression"),
27
28
  },
29
+ outputSchema: output_schemas_js_1.listClientsOutputSchema,
28
30
  annotations: safety_js_1.READ_ONLY,
29
31
  }, async ({ siteId, offset, limit, filter }) => {
30
32
  try {
31
33
  const query = (0, query_js_1.buildQuery)({ offset, limit, filter });
32
34
  const data = await client.get(`/sites/${siteId}/clients${query}`);
33
- return (0, responses_js_1.formatSuccess)(data);
35
+ return (0, responses_js_1.formatSuccess)(data, { structured: true });
34
36
  }
35
37
  catch (err) {
36
38
  return (0, responses_js_1.formatError)(err);
37
39
  }
38
40
  });
39
41
  server.registerTool("unifi_get_client", {
40
- description: "Get a specific client by ID",
42
+ description: "Get a specific connected client by ID. Returns same shape as unifi_list_clients entries.",
41
43
  inputSchema: {
42
44
  siteId: zod_1.z.string().describe("Site ID"),
43
45
  clientId: zod_1.z.string().describe("Client ID"),
44
46
  },
47
+ outputSchema: output_schemas_js_1.getClientOutputSchema,
45
48
  annotations: safety_js_1.READ_ONLY,
46
49
  }, async ({ siteId, clientId }) => {
47
50
  try {
48
51
  const data = await client.get(`/sites/${siteId}/clients/${clientId}`);
49
- return (0, responses_js_1.formatSuccess)(data);
52
+ return (0, responses_js_1.formatSuccess)(data, { structured: true });
50
53
  }
51
54
  catch (err) {
52
55
  return (0, responses_js_1.formatError)(err);
@@ -55,7 +58,7 @@ function registerClientTools(server, client, readOnly = false) {
55
58
  if (readOnly)
56
59
  return;
57
60
  server.registerTool("unifi_authorize_guest", {
58
- description: "Authorize a guest client on a hotspot network",
61
+ description: "Authorize a guest client through a captive-portal hotspot. Optional limits override hotspot defaults. Idempotency: not safe to retry — re-authorizing extends the session.",
59
62
  inputSchema: {
60
63
  siteId: zod_1.z.string().describe("Site ID"),
61
64
  clientId: zod_1.z.string().describe("Client ID"),
@@ -113,7 +116,7 @@ function registerClientTools(server, client, readOnly = false) {
113
116
  }
114
117
  });
115
118
  server.registerTool("unifi_unauthorize_guest", {
116
- description: "Unauthorize a guest client",
119
+ description: "Revoke guest authorization; the client returns to the captive portal on next request. Idempotent.",
117
120
  inputSchema: {
118
121
  siteId: zod_1.z.string().describe("Site ID"),
119
122
  clientId: zod_1.z.string().describe("Client ID"),
@@ -5,9 +5,12 @@ 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 output_schemas_js_1 = require("../utils/output-schemas.js");
9
+ // Adopt echoes the adopted device — reuse the device detail schema.
10
+ const adoptDeviceOutputSchema = output_schemas_js_1.getDeviceOutputSchema;
8
11
  function registerDeviceTools(server, client, readOnly = false) {
9
12
  server.registerTool("unifi_list_devices", {
10
- description: "List all adopted devices at a site",
13
+ 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.",
11
14
  inputSchema: {
12
15
  siteId: zod_1.z.string().describe("Site ID"),
13
16
  offset: zod_1.z
@@ -25,51 +28,54 @@ function registerDeviceTools(server, client, readOnly = false) {
25
28
  .describe("Number of records to return (default: 25, max: 200)"),
26
29
  filter: zod_1.z.string().optional().describe("Filter expression"),
27
30
  },
31
+ outputSchema: output_schemas_js_1.listDevicesOutputSchema,
28
32
  annotations: safety_js_1.READ_ONLY,
29
33
  }, async ({ siteId, offset, limit, filter }) => {
30
34
  try {
31
35
  const query = (0, query_js_1.buildQuery)({ offset, limit, filter });
32
36
  const data = await client.get(`/sites/${siteId}/devices${query}`);
33
- return (0, responses_js_1.formatSuccess)(data);
37
+ return (0, responses_js_1.formatSuccess)(data, { structured: true });
34
38
  }
35
39
  catch (err) {
36
40
  return (0, responses_js_1.formatError)(err);
37
41
  }
38
42
  });
39
43
  server.registerTool("unifi_get_device", {
40
- description: "Get a specific device by ID",
44
+ description: "Get full configuration for a device. Returns (in addition to list fields): supported, firmwareUpdatable, 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.",
41
45
  inputSchema: {
42
46
  siteId: zod_1.z.string().describe("Site ID"),
43
47
  deviceId: zod_1.z.string().describe("Device ID"),
44
48
  },
49
+ outputSchema: output_schemas_js_1.getDeviceOutputSchema,
45
50
  annotations: safety_js_1.READ_ONLY,
46
51
  }, async ({ siteId, deviceId }) => {
47
52
  try {
48
53
  const data = await client.get(`/sites/${siteId}/devices/${deviceId}`);
49
- return (0, responses_js_1.formatSuccess)(data);
54
+ return (0, responses_js_1.formatSuccess)(data, { structured: true });
50
55
  }
51
56
  catch (err) {
52
57
  return (0, responses_js_1.formatError)(err);
53
58
  }
54
59
  });
55
60
  server.registerTool("unifi_get_device_statistics", {
56
- description: "Get latest statistics for a device",
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.4.55 — 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.",
57
62
  inputSchema: {
58
63
  siteId: zod_1.z.string().describe("Site ID"),
59
64
  deviceId: zod_1.z.string().describe("Device ID"),
60
65
  },
66
+ outputSchema: output_schemas_js_1.getDeviceStatisticsOutputSchema,
61
67
  annotations: safety_js_1.READ_ONLY,
62
68
  }, async ({ siteId, deviceId }) => {
63
69
  try {
64
70
  const data = await client.get(`/sites/${siteId}/devices/${deviceId}/statistics/latest`);
65
- return (0, responses_js_1.formatSuccess)(data);
71
+ return (0, responses_js_1.formatSuccess)(data, { structured: true });
66
72
  }
67
73
  catch (err) {
68
74
  return (0, responses_js_1.formatError)(err);
69
75
  }
70
76
  });
71
77
  server.registerTool("unifi_list_pending_devices", {
72
- description: "List devices pending adoption (global, not site-specific)",
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.4.55 docs). Use for: discovering new devices on the network before calling unifi_adopt_device.",
73
79
  inputSchema: {
74
80
  offset: zod_1.z
75
81
  .number()
@@ -86,12 +92,13 @@ function registerDeviceTools(server, client, readOnly = false) {
86
92
  .describe("Number of records to return (default: 25, max: 200)"),
87
93
  filter: zod_1.z.string().optional().describe("Filter expression"),
88
94
  },
95
+ outputSchema: output_schemas_js_1.listPendingDevicesOutputSchema,
89
96
  annotations: safety_js_1.READ_ONLY,
90
97
  }, async ({ offset, limit, filter }) => {
91
98
  try {
92
99
  const query = (0, query_js_1.buildQuery)({ offset, limit, filter });
93
100
  const data = await client.get(`/pending-devices${query}`);
94
- return (0, responses_js_1.formatSuccess)(data);
101
+ return (0, responses_js_1.formatSuccess)(data, { structured: true });
95
102
  }
96
103
  catch (err) {
97
104
  return (0, responses_js_1.formatError)(err);
@@ -100,13 +107,14 @@ function registerDeviceTools(server, client, readOnly = false) {
100
107
  if (readOnly)
101
108
  return;
102
109
  server.registerTool("unifi_adopt_device", {
103
- description: "Adopt a pending device",
110
+ 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.",
104
111
  inputSchema: {
105
112
  siteId: zod_1.z.string().describe("Site ID"),
106
113
  macAddress: zod_1.z.string().describe("MAC address of the device"),
107
114
  ignoreDeviceLimit: zod_1.z.boolean().optional().describe("Ignore device limit when adopting (default: false)"),
108
115
  dryRun: zod_1.z.boolean().optional().describe("Preview this action without executing it"),
109
116
  },
117
+ outputSchema: adoptDeviceOutputSchema,
110
118
  annotations: safety_js_1.WRITE_NOT_IDEMPOTENT,
111
119
  }, async ({ siteId, macAddress, ignoreDeviceLimit, dryRun }) => {
112
120
  const body = {
@@ -117,7 +125,7 @@ function registerDeviceTools(server, client, readOnly = false) {
117
125
  return (0, safety_js_1.formatDryRun)("POST", `/sites/${siteId}/devices`, body);
118
126
  try {
119
127
  const data = await client.post(`/sites/${siteId}/devices`, body);
120
- return (0, responses_js_1.formatSuccess)(data);
128
+ return (0, responses_js_1.formatSuccess)(data, { structured: true });
121
129
  }
122
130
  catch (err) {
123
131
  return (0, responses_js_1.formatError)(err);
@@ -147,7 +155,7 @@ function registerDeviceTools(server, client, readOnly = false) {
147
155
  }
148
156
  });
149
157
  server.registerTool("unifi_restart_device", {
150
- description: "Restart a device",
158
+ description: "Restart (reboot) a device. The device will be unreachable for ~1–3 minutes. Idempotent: repeated calls trigger fresh reboots.",
151
159
  inputSchema: {
152
160
  siteId: zod_1.z.string().describe("Site ID"),
153
161
  deviceId: zod_1.z.string().describe("Device ID"),
@@ -167,7 +175,7 @@ function registerDeviceTools(server, client, readOnly = false) {
167
175
  }
168
176
  });
169
177
  server.registerTool("unifi_power_cycle_port", {
170
- description: "Power cycle a specific port on a device (PoE restart)",
178
+ 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.",
171
179
  inputSchema: {
172
180
  siteId: zod_1.z.string().describe("Site ID"),
173
181
  deviceId: zod_1.z.string().describe("Device ID"),
@@ -5,9 +5,10 @@ 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 output_schemas_js_1 = require("../utils/output-schemas.js");
8
9
  function registerDnsPolicyTools(server, client, readOnly = false) {
9
10
  server.registerTool("unifi_list_dns_policies", {
10
- description: "List all DNS policies at a site",
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.",
11
12
  inputSchema: {
12
13
  siteId: zod_1.z.string().describe("Site ID"),
13
14
  offset: zod_1.z
@@ -28,28 +29,30 @@ function registerDnsPolicyTools(server, client, readOnly = false) {
28
29
  .optional()
29
30
  .describe("Filter expression"),
30
31
  },
32
+ outputSchema: output_schemas_js_1.listDnsPoliciesOutputSchema,
31
33
  annotations: safety_js_1.READ_ONLY,
32
34
  }, async ({ siteId, offset, limit, filter }) => {
33
35
  try {
34
36
  const query = (0, query_js_1.buildQuery)({ offset, limit, filter });
35
37
  const data = await client.get(`/sites/${siteId}/dns/policies${query}`);
36
- return (0, responses_js_1.formatSuccess)(data);
38
+ return (0, responses_js_1.formatSuccess)(data, { structured: true });
37
39
  }
38
40
  catch (err) {
39
41
  return (0, responses_js_1.formatError)(err);
40
42
  }
41
43
  });
42
44
  server.registerTool("unifi_get_dns_policy", {
43
- description: "Get a specific DNS policy by ID",
45
+ description: "Get a specific DNS policy by ID (same fields as the list entry).",
44
46
  inputSchema: {
45
47
  siteId: zod_1.z.string().describe("Site ID"),
46
48
  dnsPolicyId: zod_1.z.string().describe("DNS policy ID"),
47
49
  },
50
+ outputSchema: output_schemas_js_1.dnsPolicyOutputSchema,
48
51
  annotations: safety_js_1.READ_ONLY,
49
52
  }, async ({ siteId, dnsPolicyId }) => {
50
53
  try {
51
54
  const data = await client.get(`/sites/${siteId}/dns/policies/${dnsPolicyId}`);
52
- return (0, responses_js_1.formatSuccess)(data);
55
+ return (0, responses_js_1.formatSuccess)(data, { structured: true });
53
56
  }
54
57
  catch (err) {
55
58
  return (0, responses_js_1.formatError)(err);
@@ -86,6 +89,7 @@ function registerDnsPolicyTools(server, client, readOnly = false) {
86
89
  .optional()
87
90
  .describe("Preview this action without executing it"),
88
91
  },
92
+ outputSchema: output_schemas_js_1.dnsPolicyOutputSchema,
89
93
  annotations: safety_js_1.WRITE_NOT_IDEMPOTENT,
90
94
  }, async ({ siteId, type, enabled, domain, ipv4Address, ttlSeconds, dryRun }) => {
91
95
  try {
@@ -93,7 +97,7 @@ function registerDnsPolicyTools(server, client, readOnly = false) {
93
97
  if (dryRun)
94
98
  return (0, safety_js_1.formatDryRun)("POST", `/sites/${siteId}/dns/policies`, body);
95
99
  const data = await client.post(`/sites/${siteId}/dns/policies`, body);
96
- return (0, responses_js_1.formatSuccess)(data);
100
+ return (0, responses_js_1.formatSuccess)(data, { structured: true });
97
101
  }
98
102
  catch (err) {
99
103
  return (0, responses_js_1.formatError)(err);
@@ -129,6 +133,7 @@ function registerDnsPolicyTools(server, client, readOnly = false) {
129
133
  .optional()
130
134
  .describe("Preview this action without executing it"),
131
135
  },
136
+ outputSchema: output_schemas_js_1.dnsPolicyOutputSchema,
132
137
  annotations: safety_js_1.WRITE,
133
138
  }, async ({ siteId, dnsPolicyId, type, enabled, domain, ipv4Address, ttlSeconds, dryRun }) => {
134
139
  try {
@@ -136,7 +141,7 @@ function registerDnsPolicyTools(server, client, readOnly = false) {
136
141
  if (dryRun)
137
142
  return (0, safety_js_1.formatDryRun)("PUT", `/sites/${siteId}/dns/policies/${dnsPolicyId}`, body);
138
143
  const data = await client.put(`/sites/${siteId}/dns/policies/${dnsPolicyId}`, body);
139
- return (0, responses_js_1.formatSuccess)(data);
144
+ return (0, responses_js_1.formatSuccess)(data, { structured: true });
140
145
  }
141
146
  catch (err) {
142
147
  return (0, responses_js_1.formatError)(err);
@@ -5,9 +5,10 @@ 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 output_schemas_js_1 = require("../utils/output-schemas.js");
8
9
  function registerFirewallTools(server, client, readOnly = false) {
9
10
  server.registerTool("unifi_list_firewall_zones", {
10
- description: "List all firewall zones at a site",
11
+ description: "List firewall zones (groupings of networks for zone-based firewalling) at a site. Returns: id, name, networkIds[], metadata.origin (indicates system-defined vs user-defined). Use for: zone inventory; pair with unifi_list_firewall_policies to see rules between zones.",
11
12
  inputSchema: {
12
13
  siteId: zod_1.z.string().describe("Site ID"),
13
14
  offset: zod_1.z
@@ -28,35 +29,37 @@ function registerFirewallTools(server, client, readOnly = false) {
28
29
  .optional()
29
30
  .describe("Filter expression"),
30
31
  },
32
+ outputSchema: output_schemas_js_1.listFirewallZonesOutputSchema,
31
33
  annotations: safety_js_1.READ_ONLY,
32
34
  }, async ({ siteId, offset, limit, filter }) => {
33
35
  try {
34
36
  const query = (0, query_js_1.buildQuery)({ offset, limit, filter });
35
37
  const data = await client.get(`/sites/${siteId}/firewall/zones${query}`);
36
- return (0, responses_js_1.formatSuccess)(data);
38
+ return (0, responses_js_1.formatSuccess)(data, { structured: true });
37
39
  }
38
40
  catch (err) {
39
41
  return (0, responses_js_1.formatError)(err);
40
42
  }
41
43
  });
42
44
  server.registerTool("unifi_get_firewall_zone", {
43
- description: "Get a specific firewall zone by ID",
45
+ description: "Get a firewall zone by ID (same fields as the list entry).",
44
46
  inputSchema: {
45
47
  siteId: zod_1.z.string().describe("Site ID"),
46
48
  firewallZoneId: zod_1.z.string().describe("Firewall zone ID"),
47
49
  },
50
+ outputSchema: output_schemas_js_1.firewallZoneOutputSchema,
48
51
  annotations: safety_js_1.READ_ONLY,
49
52
  }, async ({ siteId, firewallZoneId }) => {
50
53
  try {
51
54
  const data = await client.get(`/sites/${siteId}/firewall/zones/${firewallZoneId}`);
52
- return (0, responses_js_1.formatSuccess)(data);
55
+ return (0, responses_js_1.formatSuccess)(data, { structured: true });
53
56
  }
54
57
  catch (err) {
55
58
  return (0, responses_js_1.formatError)(err);
56
59
  }
57
60
  });
58
61
  server.registerTool("unifi_list_firewall_policies", {
59
- description: "List all firewall policies at a site",
62
+ description: "List firewall policies (zone-based rules) at a site. Returns: id, name, enabled, action (object with type field), source/destination (zone reference + trafficFilter), ipProtocolScope, connectionStateFilter, ipsecFilter, schedule, loggingEnabled, index, description, metadata.origin. Protocols/ports are encoded inside source/destination.trafficFilter, not as top-level fields. Evaluation order within a zone pair comes from unifi_get_firewall_policy_ordering.",
60
63
  inputSchema: {
61
64
  siteId: zod_1.z.string().describe("Site ID"),
62
65
  offset: zod_1.z
@@ -77,35 +80,37 @@ function registerFirewallTools(server, client, readOnly = false) {
77
80
  .optional()
78
81
  .describe("Filter expression"),
79
82
  },
83
+ outputSchema: output_schemas_js_1.listFirewallPoliciesOutputSchema,
80
84
  annotations: safety_js_1.READ_ONLY,
81
85
  }, async ({ siteId, offset, limit, filter }) => {
82
86
  try {
83
87
  const query = (0, query_js_1.buildQuery)({ offset, limit, filter });
84
88
  const data = await client.get(`/sites/${siteId}/firewall/policies${query}`);
85
- return (0, responses_js_1.formatSuccess)(data);
89
+ return (0, responses_js_1.formatSuccess)(data, { structured: true });
86
90
  }
87
91
  catch (err) {
88
92
  return (0, responses_js_1.formatError)(err);
89
93
  }
90
94
  });
91
95
  server.registerTool("unifi_get_firewall_policy", {
92
- description: "Get a specific firewall policy by ID",
96
+ description: "Get a firewall policy by ID with full match criteria and action.",
93
97
  inputSchema: {
94
98
  siteId: zod_1.z.string().describe("Site ID"),
95
99
  firewallPolicyId: zod_1.z.string().describe("Firewall policy ID"),
96
100
  },
101
+ outputSchema: output_schemas_js_1.firewallPolicyOutputSchema,
97
102
  annotations: safety_js_1.READ_ONLY,
98
103
  }, async ({ siteId, firewallPolicyId }) => {
99
104
  try {
100
105
  const data = await client.get(`/sites/${siteId}/firewall/policies/${firewallPolicyId}`);
101
- return (0, responses_js_1.formatSuccess)(data);
106
+ return (0, responses_js_1.formatSuccess)(data, { structured: true });
102
107
  }
103
108
  catch (err) {
104
109
  return (0, responses_js_1.formatError)(err);
105
110
  }
106
111
  });
107
112
  server.registerTool("unifi_get_firewall_policy_ordering", {
108
- description: "Get user-defined firewall policy ordering for a zone pair",
113
+ description: "Get the evaluation order of user-defined firewall policies for a specific (source zone, destination zone) pair. Returns: beforeSystemDefined[] and afterSystemDefined[] arrays of policy IDs. System-defined rules sit between these two arrays.",
109
114
  inputSchema: {
110
115
  siteId: zod_1.z.string().describe("Site ID"),
111
116
  sourceFirewallZoneId: zod_1.z
@@ -115,12 +120,13 @@ function registerFirewallTools(server, client, readOnly = false) {
115
120
  .string()
116
121
  .describe("Destination firewall zone ID"),
117
122
  },
123
+ outputSchema: output_schemas_js_1.firewallPolicyOrderingOutputSchema,
118
124
  annotations: safety_js_1.READ_ONLY,
119
125
  }, async ({ siteId, sourceFirewallZoneId, destinationFirewallZoneId }) => {
120
126
  try {
121
127
  const query = `?sourceFirewallZoneId=${encodeURIComponent(sourceFirewallZoneId)}&destinationFirewallZoneId=${encodeURIComponent(destinationFirewallZoneId)}`;
122
128
  const data = await client.get(`/sites/${siteId}/firewall/policies/ordering${query}`);
123
- return (0, responses_js_1.formatSuccess)(data);
129
+ return (0, responses_js_1.formatSuccess)(data, { structured: true });
124
130
  }
125
131
  catch (err) {
126
132
  return (0, responses_js_1.formatError)(err);
@@ -141,6 +147,7 @@ function registerFirewallTools(server, client, readOnly = false) {
141
147
  .optional()
142
148
  .describe("Preview this action without executing it"),
143
149
  },
150
+ outputSchema: output_schemas_js_1.firewallZoneOutputSchema,
144
151
  annotations: safety_js_1.WRITE_NOT_IDEMPOTENT,
145
152
  }, async ({ siteId, name, networkIds, dryRun }) => {
146
153
  try {
@@ -148,7 +155,7 @@ function registerFirewallTools(server, client, readOnly = false) {
148
155
  if (dryRun)
149
156
  return (0, safety_js_1.formatDryRun)("POST", `/sites/${siteId}/firewall/zones`, body);
150
157
  const data = await client.post(`/sites/${siteId}/firewall/zones`, body);
151
- return (0, responses_js_1.formatSuccess)(data);
158
+ return (0, responses_js_1.formatSuccess)(data, { structured: true });
152
159
  }
153
160
  catch (err) {
154
161
  return (0, responses_js_1.formatError)(err);
@@ -168,6 +175,7 @@ function registerFirewallTools(server, client, readOnly = false) {
168
175
  .optional()
169
176
  .describe("Preview this action without executing it"),
170
177
  },
178
+ outputSchema: output_schemas_js_1.firewallZoneOutputSchema,
171
179
  annotations: safety_js_1.WRITE,
172
180
  }, async ({ siteId, firewallZoneId, name, networkIds, dryRun }) => {
173
181
  try {
@@ -175,7 +183,7 @@ function registerFirewallTools(server, client, readOnly = false) {
175
183
  if (dryRun)
176
184
  return (0, safety_js_1.formatDryRun)("PUT", `/sites/${siteId}/firewall/zones/${firewallZoneId}`, body);
177
185
  const data = await client.put(`/sites/${siteId}/firewall/zones/${firewallZoneId}`, body);
178
- return (0, responses_js_1.formatSuccess)(data);
186
+ return (0, responses_js_1.formatSuccess)(data, { structured: true });
179
187
  }
180
188
  catch (err) {
181
189
  return (0, responses_js_1.formatError)(err);
@@ -222,13 +230,14 @@ function registerFirewallTools(server, client, readOnly = false) {
222
230
  .optional()
223
231
  .describe("Preview this action without executing it"),
224
232
  },
233
+ outputSchema: output_schemas_js_1.firewallPolicyOutputSchema,
225
234
  annotations: safety_js_1.WRITE_NOT_IDEMPOTENT,
226
235
  }, async ({ siteId, policy, dryRun }) => {
227
236
  try {
228
237
  if (dryRun)
229
238
  return (0, safety_js_1.formatDryRun)("POST", `/sites/${siteId}/firewall/policies`, policy);
230
239
  const data = await client.post(`/sites/${siteId}/firewall/policies`, policy);
231
- return (0, responses_js_1.formatSuccess)(data);
240
+ return (0, responses_js_1.formatSuccess)(data, { structured: true });
232
241
  }
233
242
  catch (err) {
234
243
  return (0, responses_js_1.formatError)(err);
@@ -247,20 +256,21 @@ function registerFirewallTools(server, client, readOnly = false) {
247
256
  .optional()
248
257
  .describe("Preview this action without executing it"),
249
258
  },
259
+ outputSchema: output_schemas_js_1.firewallPolicyOutputSchema,
250
260
  annotations: safety_js_1.WRITE,
251
261
  }, async ({ siteId, firewallPolicyId, policy, dryRun }) => {
252
262
  try {
253
263
  if (dryRun)
254
264
  return (0, safety_js_1.formatDryRun)("PUT", `/sites/${siteId}/firewall/policies/${firewallPolicyId}`, policy);
255
265
  const data = await client.put(`/sites/${siteId}/firewall/policies/${firewallPolicyId}`, policy);
256
- return (0, responses_js_1.formatSuccess)(data);
266
+ return (0, responses_js_1.formatSuccess)(data, { structured: true });
257
267
  }
258
268
  catch (err) {
259
269
  return (0, responses_js_1.formatError)(err);
260
270
  }
261
271
  });
262
272
  server.registerTool("unifi_patch_firewall_policy", {
263
- description: "Partially update a firewall policy (e.g. toggle logging)",
273
+ description: "Partially update a firewall policy without resending all fields. Common use: toggle loggingEnabled or enabled. Idempotent for fields supplied.",
264
274
  inputSchema: {
265
275
  siteId: zod_1.z.string().describe("Site ID"),
266
276
  firewallPolicyId: zod_1.z.string().describe("Firewall policy ID"),
@@ -272,13 +282,14 @@ function registerFirewallTools(server, client, readOnly = false) {
272
282
  .optional()
273
283
  .describe("Preview this action without executing it"),
274
284
  },
285
+ outputSchema: output_schemas_js_1.firewallPolicyOutputSchema,
275
286
  annotations: safety_js_1.WRITE,
276
287
  }, async ({ siteId, firewallPolicyId, policy, dryRun }) => {
277
288
  try {
278
289
  if (dryRun)
279
290
  return (0, safety_js_1.formatDryRun)("PATCH", `/sites/${siteId}/firewall/policies/${firewallPolicyId}`, policy);
280
291
  const data = await client.patch(`/sites/${siteId}/firewall/policies/${firewallPolicyId}`, policy);
281
- return (0, responses_js_1.formatSuccess)(data);
292
+ return (0, responses_js_1.formatSuccess)(data, { structured: true });
282
293
  }
283
294
  catch (err) {
284
295
  return (0, responses_js_1.formatError)(err);
@@ -5,9 +5,10 @@ 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 output_schemas_js_1 = require("../utils/output-schemas.js");
8
9
  function registerHotspotTools(server, client, readOnly = false) {
9
10
  server.registerTool("unifi_list_vouchers", {
10
- description: "List all hotspot vouchers at a site",
11
+ description: "List hotspot/guest-portal vouchers at a site. Returns: id, code, name, createdAt, activatedAt (when first guest used it), expiresAt, timeLimitMinutes, dataUsageLimitMBytes, rxRateLimitKbps, txRateLimitKbps, authorizedGuestLimit, authorizedGuestCount, expired. Use filter like 'expired.eq(true)' to bulk-find stale vouchers.",
11
12
  inputSchema: {
12
13
  siteId: zod_1.z.string().describe("Site ID"),
13
14
  offset: zod_1.z
@@ -30,28 +31,30 @@ function registerHotspotTools(server, client, readOnly = false) {
30
31
  .optional()
31
32
  .describe("Filter expression (e.g., 'expired.eq(true)')"),
32
33
  },
34
+ outputSchema: output_schemas_js_1.listVouchersOutputSchema,
33
35
  annotations: safety_js_1.READ_ONLY,
34
36
  }, async ({ siteId, offset, limit, filter }) => {
35
37
  try {
36
38
  const query = (0, query_js_1.buildQuery)({ offset, limit, filter });
37
39
  const data = await client.get(`/sites/${siteId}/hotspot/vouchers${query}`);
38
- return (0, responses_js_1.formatSuccess)(data);
40
+ return (0, responses_js_1.formatSuccess)(data, { structured: true });
39
41
  }
40
42
  catch (err) {
41
43
  return (0, responses_js_1.formatError)(err);
42
44
  }
43
45
  });
44
46
  server.registerTool("unifi_get_voucher", {
45
- description: "Get a specific hotspot voucher by ID",
47
+ description: "Get a specific hotspot voucher by ID (same fields as the list entry).",
46
48
  inputSchema: {
47
49
  siteId: zod_1.z.string().describe("Site ID"),
48
50
  voucherId: zod_1.z.string().describe("Voucher ID"),
49
51
  },
52
+ outputSchema: output_schemas_js_1.voucherOutputSchema,
50
53
  annotations: safety_js_1.READ_ONLY,
51
54
  }, async ({ siteId, voucherId }) => {
52
55
  try {
53
56
  const data = await client.get(`/sites/${siteId}/hotspot/vouchers/${voucherId}`);
54
- return (0, responses_js_1.formatSuccess)(data);
57
+ return (0, responses_js_1.formatSuccess)(data, { structured: true });
55
58
  }
56
59
  catch (err) {
57
60
  return (0, responses_js_1.formatError)(err);
@@ -60,7 +63,7 @@ function registerHotspotTools(server, client, readOnly = false) {
60
63
  if (readOnly)
61
64
  return;
62
65
  server.registerTool("unifi_create_voucher", {
63
- description: "Create hotspot vouchers",
66
+ description: "Create one or more hotspot vouchers (use count for batch creation, up to 1000 at once). Returns: array of created vouchers with their generated codes. Idempotency: not safe to retry — each call mints fresh codes.",
64
67
  inputSchema: {
65
68
  siteId: zod_1.z.string().describe("Site ID"),
66
69
  name: zod_1.z
@@ -112,6 +115,7 @@ function registerHotspotTools(server, client, readOnly = false) {
112
115
  .optional()
113
116
  .describe("Preview this action without executing it"),
114
117
  },
118
+ outputSchema: output_schemas_js_1.createVouchersOutputSchema,
115
119
  annotations: safety_js_1.WRITE_NOT_IDEMPOTENT,
116
120
  }, async ({ siteId, name, timeLimitMinutes, count, authorizedGuestLimit, dataUsageLimitMBytes, rxRateLimitKbps, txRateLimitKbps, dryRun, }) => {
117
121
  try {
@@ -132,7 +136,7 @@ function registerHotspotTools(server, client, readOnly = false) {
132
136
  if (dryRun)
133
137
  return (0, safety_js_1.formatDryRun)("POST", `/sites/${siteId}/hotspot/vouchers`, body);
134
138
  const data = await client.post(`/sites/${siteId}/hotspot/vouchers`, body);
135
- return (0, responses_js_1.formatSuccess)(data);
139
+ return (0, responses_js_1.formatSuccess)(data, { structured: true });
136
140
  }
137
141
  catch (err) {
138
142
  return (0, responses_js_1.formatError)(err);