@owine/unifi-network-mcp 2.1.0 → 2.2.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -1,6 +1,6 @@
1
1
  # UniFi Network MCP Server
2
2
 
3
- An MCP (Model Context Protocol) server that exposes the UniFi Network Integration API as tools for Claude Code and other MCP clients. Provides 68 tools for managing sites, devices, clients, networks, WiFi, firewalls, ACLs, DNS policies, hotspot vouchers, VPNs, and more.
3
+ An MCP (Model Context Protocol) server that exposes the UniFi Network Integration API as tools for Claude Code and other MCP clients. Provides 74 tools for managing sites, devices, clients, networks, WiFi, firewalls, ACLs, switching, DNS policies, hotspot vouchers, VPNs, and more.
4
4
 
5
5
  ## Prerequisites
6
6
 
@@ -76,7 +76,7 @@ This server provides layered safety controls for responsible operation:
76
76
  - **Confirmation parameter** — The most dangerous tools (e.g., `unifi_remove_device`, `unifi_bulk_delete_vouchers`) require an explicit `confirm: true` parameter that must be present for the call to succeed
77
77
  - **Dry-run support** — All write tools accept an optional `dryRun: true` parameter that returns a preview of the HTTP request (method, path, body) without making any changes
78
78
 
79
- ## Tools (67 total)
79
+ ## Tools (74 total)
80
80
 
81
81
  ### System (1)
82
82
  | Tool | Description |
@@ -136,7 +136,7 @@ This server provides layered safety controls for responsible operation:
136
136
  | `unifi_delete_voucher` | **DESTRUCTIVE:** Delete a hotspot voucher |
137
137
  | `unifi_bulk_delete_vouchers` | **DESTRUCTIVE:** Bulk delete vouchers matching a filter |
138
138
 
139
- ### Firewall Zones & Policies (12)
139
+ ### Firewall Zones & Policies (13)
140
140
  | Tool | Description |
141
141
  |---|---|
142
142
  | `unifi_list_firewall_zones` | List all firewall zones at a site |
@@ -156,7 +156,7 @@ This server provides layered safety controls for responsible operation:
156
156
  ### ACL Rules (7)
157
157
  | Tool | Description |
158
158
  |---|---|
159
- | `unifi_list_acl_rules` | List all ACL (firewall) rules at a site |
159
+ | `unifi_list_acl_rules` | List all ACL rules at a site |
160
160
  | `unifi_get_acl_rule` | Get a specific ACL rule by ID |
161
161
  | `unifi_get_acl_rule_ordering` | Get user-defined ACL rule ordering |
162
162
  | `unifi_create_acl_rule` | Create a new ACL rule |
@@ -164,6 +164,16 @@ This server provides layered safety controls for responsible operation:
164
164
  | `unifi_delete_acl_rule` | **DESTRUCTIVE:** Delete an ACL rule |
165
165
  | `unifi_reorder_acl_rules` | Reorder user-defined ACL rules |
166
166
 
167
+ ### Switching (6)
168
+ | Tool | Description |
169
+ |---|---|
170
+ | `unifi_list_switch_stacks` | List all Switch Stacks at a site |
171
+ | `unifi_get_switch_stack` | Get details of a specific Switch Stack |
172
+ | `unifi_list_mc_lag_domains` | List all MC-LAG (Multi-Chassis LAG) Domains at a site |
173
+ | `unifi_get_mc_lag_domain` | Get details of a specific MC-LAG Domain |
174
+ | `unifi_list_lags` | List all LAGs (Link Aggregation Groups) at a site |
175
+ | `unifi_get_lag` | Get details of a specific LAG |
176
+
167
177
  ### DNS Policies (5)
168
178
  | Tool | Description |
169
179
  |---|---|
package/dist/tools/acl.js CHANGED
@@ -7,7 +7,7 @@ const query_js_1 = require("../utils/query.js");
7
7
  const safety_js_1 = require("../utils/safety.js");
8
8
  function registerAclTools(server, client, readOnly = false) {
9
9
  server.registerTool("unifi_list_acl_rules", {
10
- description: "List all ACL (firewall) rules at a site",
10
+ description: "List all ACL rules at a site",
11
11
  inputSchema: {
12
12
  siteId: zod_1.z.string().describe("Site ID"),
13
13
  offset: zod_1.z
@@ -73,7 +73,7 @@ function registerAclTools(server, client, readOnly = false) {
73
73
  if (readOnly)
74
74
  return;
75
75
  server.registerTool("unifi_create_acl_rule", {
76
- description: "Create a new ACL (firewall) rule",
76
+ description: "Create a new ACL rule",
77
77
  inputSchema: {
78
78
  siteId: zod_1.z.string().describe("Site ID"),
79
79
  type: zod_1.z.enum(["IPV4", "MAC"]).describe("Rule type"),
@@ -115,20 +115,34 @@ function registerAclTools(server, client, readOnly = false) {
115
115
  inputSchema: {
116
116
  siteId: zod_1.z.string().describe("Site ID"),
117
117
  aclRuleId: zod_1.z.string().describe("ACL rule ID"),
118
- rule: zod_1.z
118
+ type: zod_1.z.string().describe("ACL rule type"),
119
+ name: zod_1.z.string().describe("ACL rule name"),
120
+ enabled: zod_1.z.boolean().describe("Whether the rule is enabled"),
121
+ action: zod_1.z.string().describe("Rule action (ALLOW, DENY, etc.)"),
122
+ description: zod_1.z
123
+ .string()
124
+ .optional()
125
+ .describe("Rule description"),
126
+ protocolFilter: zod_1.z
119
127
  .record(zod_1.z.string(), zod_1.z.unknown())
120
- .describe("ACL rule configuration (JSON object)"),
128
+ .optional()
129
+ .describe("Protocol filter configuration"),
121
130
  dryRun: zod_1.z
122
131
  .boolean()
123
132
  .optional()
124
133
  .describe("Preview this action without executing it"),
125
134
  },
126
135
  annotations: safety_js_1.WRITE,
127
- }, async ({ siteId, aclRuleId, rule, dryRun }) => {
136
+ }, async ({ siteId, aclRuleId, type, name, enabled, action, description, protocolFilter, dryRun }) => {
128
137
  try {
138
+ const body = { type, name, enabled, action };
139
+ if (description !== undefined)
140
+ body.description = description;
141
+ if (protocolFilter !== undefined)
142
+ body.protocolFilter = protocolFilter;
129
143
  if (dryRun)
130
- return (0, safety_js_1.formatDryRun)("PUT", `/sites/${siteId}/acl-rules/${aclRuleId}`, rule);
131
- const data = await client.put(`/sites/${siteId}/acl-rules/${aclRuleId}`, rule);
144
+ return (0, safety_js_1.formatDryRun)("PUT", `/sites/${siteId}/acl-rules/${aclRuleId}`, body);
145
+ const data = await client.put(`/sites/${siteId}/acl-rules/${aclRuleId}`, body);
132
146
  return (0, responses_js_1.formatSuccess)(data);
133
147
  }
134
148
  catch (err) {
@@ -104,13 +104,14 @@ function registerDeviceTools(server, client, readOnly = false) {
104
104
  inputSchema: {
105
105
  siteId: zod_1.z.string().describe("Site ID"),
106
106
  macAddress: zod_1.z.string().describe("MAC address of the device"),
107
+ ignoreDeviceLimit: zod_1.z.boolean().optional().describe("Ignore device limit when adopting (default: false)"),
107
108
  dryRun: zod_1.z.boolean().optional().describe("Preview this action without executing it"),
108
109
  },
109
110
  annotations: safety_js_1.WRITE_NOT_IDEMPOTENT,
110
- }, async ({ siteId, macAddress, dryRun }) => {
111
+ }, async ({ siteId, macAddress, ignoreDeviceLimit, dryRun }) => {
111
112
  const body = {
112
113
  macAddress,
113
- ignoreDeviceLimit: false,
114
+ ignoreDeviceLimit: ignoreDeviceLimit ?? false,
114
115
  };
115
116
  if (dryRun)
116
117
  return (0, safety_js_1.formatDryRun)("POST", `/sites/${siteId}/devices`, body);
@@ -61,20 +61,38 @@ function registerDnsPolicyTools(server, client, readOnly = false) {
61
61
  description: "Create a new DNS policy",
62
62
  inputSchema: {
63
63
  siteId: zod_1.z.string().describe("Site ID"),
64
- policy: zod_1.z
65
- .record(zod_1.z.string(), zod_1.z.unknown())
66
- .describe("DNS policy configuration (JSON object)"),
64
+ type: zod_1.z
65
+ .enum([
66
+ "A_RECORD",
67
+ "AAAA_RECORD",
68
+ "CNAME_RECORD",
69
+ "MX_RECORD",
70
+ "TXT_RECORD",
71
+ "SRV_RECORD",
72
+ "FORWARD_DOMAIN",
73
+ ])
74
+ .describe("DNS record type"),
75
+ enabled: zod_1.z.boolean().describe("Whether the DNS policy is enabled"),
76
+ domain: zod_1.z.string().min(1).max(127).describe("Domain name"),
77
+ ipv4Address: zod_1.z.string().describe("IPv4 address for the record"),
78
+ ttlSeconds: zod_1.z
79
+ .number()
80
+ .int()
81
+ .min(0)
82
+ .max(86400)
83
+ .describe("Time to live in seconds"),
67
84
  dryRun: zod_1.z
68
85
  .boolean()
69
86
  .optional()
70
87
  .describe("Preview this action without executing it"),
71
88
  },
72
89
  annotations: safety_js_1.WRITE_NOT_IDEMPOTENT,
73
- }, async ({ siteId, policy, dryRun }) => {
90
+ }, async ({ siteId, type, enabled, domain, ipv4Address, ttlSeconds, dryRun }) => {
74
91
  try {
92
+ const body = { type, enabled, domain, ipv4Address, ttlSeconds };
75
93
  if (dryRun)
76
- return (0, safety_js_1.formatDryRun)("POST", `/sites/${siteId}/dns/policies`, policy);
77
- const data = await client.post(`/sites/${siteId}/dns/policies`, policy);
94
+ return (0, safety_js_1.formatDryRun)("POST", `/sites/${siteId}/dns/policies`, body);
95
+ const data = await client.post(`/sites/${siteId}/dns/policies`, body);
78
96
  return (0, responses_js_1.formatSuccess)(data);
79
97
  }
80
98
  catch (err) {
@@ -86,20 +104,38 @@ function registerDnsPolicyTools(server, client, readOnly = false) {
86
104
  inputSchema: {
87
105
  siteId: zod_1.z.string().describe("Site ID"),
88
106
  dnsPolicyId: zod_1.z.string().describe("DNS policy ID"),
89
- policy: zod_1.z
90
- .record(zod_1.z.string(), zod_1.z.unknown())
91
- .describe("DNS policy configuration (JSON object)"),
107
+ type: zod_1.z
108
+ .enum([
109
+ "A_RECORD",
110
+ "AAAA_RECORD",
111
+ "CNAME_RECORD",
112
+ "MX_RECORD",
113
+ "TXT_RECORD",
114
+ "SRV_RECORD",
115
+ "FORWARD_DOMAIN",
116
+ ])
117
+ .describe("DNS record type"),
118
+ enabled: zod_1.z.boolean().describe("Whether the DNS policy is enabled"),
119
+ domain: zod_1.z.string().min(1).max(127).describe("Domain name"),
120
+ ipv4Address: zod_1.z.string().describe("IPv4 address for the record"),
121
+ ttlSeconds: zod_1.z
122
+ .number()
123
+ .int()
124
+ .min(0)
125
+ .max(86400)
126
+ .describe("Time to live in seconds"),
92
127
  dryRun: zod_1.z
93
128
  .boolean()
94
129
  .optional()
95
130
  .describe("Preview this action without executing it"),
96
131
  },
97
132
  annotations: safety_js_1.WRITE,
98
- }, async ({ siteId, dnsPolicyId, policy, dryRun }) => {
133
+ }, async ({ siteId, dnsPolicyId, type, enabled, domain, ipv4Address, ttlSeconds, dryRun }) => {
99
134
  try {
135
+ const body = { type, enabled, domain, ipv4Address, ttlSeconds };
100
136
  if (dryRun)
101
- return (0, safety_js_1.formatDryRun)("PUT", `/sites/${siteId}/dns/policies/${dnsPolicyId}`, policy);
102
- const data = await client.put(`/sites/${siteId}/dns/policies/${dnsPolicyId}`, policy);
137
+ return (0, safety_js_1.formatDryRun)("PUT", `/sites/${siteId}/dns/policies/${dnsPolicyId}`, body);
138
+ const data = await client.put(`/sites/${siteId}/dns/policies/${dnsPolicyId}`, body);
103
139
  return (0, responses_js_1.formatSuccess)(data);
104
140
  }
105
141
  catch (err) {
@@ -108,15 +108,29 @@ function registerFirewallTools(server, client, readOnly = false) {
108
108
  description: "Get user-defined firewall policy ordering for a zone pair",
109
109
  inputSchema: {
110
110
  siteId: zod_1.z.string().describe("Site ID"),
111
- sourceFirewallZoneId: zod_1.z.string().describe("Source firewall zone ID"),
111
+ sourceFirewallZoneId: zod_1.z
112
+ .string()
113
+ .optional()
114
+ .describe("Source firewall zone ID"),
112
115
  destinationFirewallZoneId: zod_1.z
113
116
  .string()
117
+ .optional()
114
118
  .describe("Destination firewall zone ID"),
115
119
  },
116
120
  annotations: safety_js_1.READ_ONLY,
117
121
  }, async ({ siteId, sourceFirewallZoneId, destinationFirewallZoneId }) => {
118
122
  try {
119
- const query = `?sourceFirewallZoneId=${encodeURIComponent(sourceFirewallZoneId)}&destinationFirewallZoneId=${encodeURIComponent(destinationFirewallZoneId)}`;
123
+ let query = "";
124
+ const params = new URLSearchParams();
125
+ if (sourceFirewallZoneId !== undefined) {
126
+ params.set("sourceFirewallZoneId", sourceFirewallZoneId);
127
+ }
128
+ if (destinationFirewallZoneId !== undefined) {
129
+ params.set("destinationFirewallZoneId", destinationFirewallZoneId);
130
+ }
131
+ const qs = params.toString();
132
+ if (qs)
133
+ query = `?${qs}`;
120
134
  const data = await client.get(`/sites/${siteId}/firewall/policies/ordering${query}`);
121
135
  return (0, responses_js_1.formatSuccess)(data);
122
136
  }
@@ -12,6 +12,7 @@ const firewall_js_1 = require("./firewall.js");
12
12
  const acl_js_1 = require("./acl.js");
13
13
  const dns_policies_js_1 = require("./dns-policies.js");
14
14
  const traffic_matching_js_1 = require("./traffic-matching.js");
15
+ const switching_js_1 = require("./switching.js");
15
16
  const supporting_js_1 = require("./supporting.js");
16
17
  function registerAllTools(server, client, readOnly = false) {
17
18
  (0, system_js_1.registerSystemTools)(server, client);
@@ -25,5 +26,6 @@ function registerAllTools(server, client, readOnly = false) {
25
26
  (0, acl_js_1.registerAclTools)(server, client, readOnly);
26
27
  (0, dns_policies_js_1.registerDnsPolicyTools)(server, client, readOnly);
27
28
  (0, traffic_matching_js_1.registerTrafficMatchingTools)(server, client, readOnly);
29
+ (0, switching_js_1.registerSwitchingTools)(server, client);
28
30
  (0, supporting_js_1.registerSupportingTools)(server, client);
29
31
  }
@@ -88,15 +88,21 @@ function registerNetworkTools(server, client, readOnly = false) {
88
88
  .min(1)
89
89
  .max(4009)
90
90
  .describe("VLAN ID (1 for default, 2+ for additional)"),
91
+ dhcpGuarding: zod_1.z
92
+ .record(zod_1.z.string(), zod_1.z.unknown())
93
+ .optional()
94
+ .describe("DHCP Guarding settings (trustedDhcpServerIpAddresses)"),
91
95
  dryRun: zod_1.z
92
96
  .boolean()
93
97
  .optional()
94
98
  .describe("Preview this action without executing it"),
95
99
  },
96
100
  annotations: safety_js_1.WRITE_NOT_IDEMPOTENT,
97
- }, async ({ siteId, name, management, enabled, vlanId, dryRun }) => {
101
+ }, async ({ siteId, name, management, enabled, vlanId, dhcpGuarding, dryRun }) => {
98
102
  try {
99
103
  const body = { name, management, enabled, vlanId };
104
+ if (dhcpGuarding !== undefined)
105
+ body.dhcpGuarding = dhcpGuarding;
100
106
  if (dryRun)
101
107
  return (0, safety_js_1.formatDryRun)("POST", `/sites/${siteId}/networks`, body);
102
108
  const data = await client.post(`/sites/${siteId}/networks`, body);
@@ -122,15 +128,21 @@ function registerNetworkTools(server, client, readOnly = false) {
122
128
  .min(1)
123
129
  .max(4009)
124
130
  .describe("VLAN ID (1-4009)"),
131
+ dhcpGuarding: zod_1.z
132
+ .record(zod_1.z.string(), zod_1.z.unknown())
133
+ .optional()
134
+ .describe("DHCP Guarding settings (trustedDhcpServerIpAddresses)"),
125
135
  dryRun: zod_1.z
126
136
  .boolean()
127
137
  .optional()
128
138
  .describe("Preview this action without executing it"),
129
139
  },
130
140
  annotations: safety_js_1.WRITE,
131
- }, async ({ siteId, networkId, name, management, enabled, vlanId, dryRun }) => {
141
+ }, async ({ siteId, networkId, name, management, enabled, vlanId, dhcpGuarding, dryRun }) => {
132
142
  try {
133
143
  const body = { name, management, enabled, vlanId };
144
+ if (dhcpGuarding !== undefined)
145
+ body.dhcpGuarding = dhcpGuarding;
134
146
  if (dryRun)
135
147
  return (0, safety_js_1.formatDryRun)("PUT", `/sites/${siteId}/networks/${networkId}`, body);
136
148
  const data = await client.put(`/sites/${siteId}/networks/${networkId}`, body);
@@ -0,0 +1,3 @@
1
+ import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
2
+ import { NetworkClient } from "../client.js";
3
+ export declare function registerSwitchingTools(server: McpServer, client: NetworkClient): void;
@@ -0,0 +1,156 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.registerSwitchingTools = registerSwitchingTools;
4
+ const zod_1 = require("zod");
5
+ const responses_js_1 = require("../utils/responses.js");
6
+ const query_js_1 = require("../utils/query.js");
7
+ const safety_js_1 = require("../utils/safety.js");
8
+ function registerSwitchingTools(server, client) {
9
+ server.registerTool("unifi_list_switch_stacks", {
10
+ description: "List all Switch Stacks at a site",
11
+ inputSchema: {
12
+ siteId: zod_1.z.string().describe("Site ID"),
13
+ offset: zod_1.z
14
+ .number()
15
+ .int()
16
+ .nonnegative()
17
+ .optional()
18
+ .describe("Number of records to skip (default: 0)"),
19
+ limit: zod_1.z
20
+ .number()
21
+ .int()
22
+ .min(1)
23
+ .max(200)
24
+ .optional()
25
+ .describe("Number of records to return (default: 25, max: 200)"),
26
+ filter: zod_1.z
27
+ .string()
28
+ .optional()
29
+ .describe("Filter expression"),
30
+ },
31
+ annotations: safety_js_1.READ_ONLY,
32
+ }, async ({ siteId, offset, limit, filter }) => {
33
+ try {
34
+ const query = (0, query_js_1.buildQuery)({ offset, limit, filter });
35
+ const data = await client.get(`/sites/${siteId}/switching/switch-stacks${query}`);
36
+ return (0, responses_js_1.formatSuccess)(data);
37
+ }
38
+ catch (err) {
39
+ return (0, responses_js_1.formatError)(err);
40
+ }
41
+ });
42
+ server.registerTool("unifi_get_switch_stack", {
43
+ description: "Get details of a specific Switch Stack",
44
+ inputSchema: {
45
+ siteId: zod_1.z.string().describe("Site ID"),
46
+ switchStackId: zod_1.z.string().describe("Switch Stack ID"),
47
+ },
48
+ annotations: safety_js_1.READ_ONLY,
49
+ }, async ({ siteId, switchStackId }) => {
50
+ try {
51
+ const data = await client.get(`/sites/${siteId}/switching/switch-stacks/${switchStackId}`);
52
+ return (0, responses_js_1.formatSuccess)(data);
53
+ }
54
+ catch (err) {
55
+ return (0, responses_js_1.formatError)(err);
56
+ }
57
+ });
58
+ server.registerTool("unifi_list_mc_lag_domains", {
59
+ description: "List all MC-LAG (Multi-Chassis LAG) Domains at a site",
60
+ inputSchema: {
61
+ siteId: zod_1.z.string().describe("Site ID"),
62
+ offset: zod_1.z
63
+ .number()
64
+ .int()
65
+ .nonnegative()
66
+ .optional()
67
+ .describe("Number of records to skip (default: 0)"),
68
+ limit: zod_1.z
69
+ .number()
70
+ .int()
71
+ .min(1)
72
+ .max(200)
73
+ .optional()
74
+ .describe("Number of records to return (default: 25, max: 200)"),
75
+ filter: zod_1.z
76
+ .string()
77
+ .optional()
78
+ .describe("Filter expression"),
79
+ },
80
+ annotations: safety_js_1.READ_ONLY,
81
+ }, async ({ siteId, offset, limit, filter }) => {
82
+ try {
83
+ const query = (0, query_js_1.buildQuery)({ offset, limit, filter });
84
+ const data = await client.get(`/sites/${siteId}/switching/mc-lag-domains${query}`);
85
+ return (0, responses_js_1.formatSuccess)(data);
86
+ }
87
+ catch (err) {
88
+ return (0, responses_js_1.formatError)(err);
89
+ }
90
+ });
91
+ server.registerTool("unifi_get_mc_lag_domain", {
92
+ description: "Get details of a specific MC-LAG (Multi-Chassis LAG) Domain",
93
+ inputSchema: {
94
+ siteId: zod_1.z.string().describe("Site ID"),
95
+ mcLagDomainId: zod_1.z.string().describe("MC-LAG Domain ID"),
96
+ },
97
+ annotations: safety_js_1.READ_ONLY,
98
+ }, async ({ siteId, mcLagDomainId }) => {
99
+ try {
100
+ const data = await client.get(`/sites/${siteId}/switching/mc-lag-domains/${mcLagDomainId}`);
101
+ return (0, responses_js_1.formatSuccess)(data);
102
+ }
103
+ catch (err) {
104
+ return (0, responses_js_1.formatError)(err);
105
+ }
106
+ });
107
+ server.registerTool("unifi_list_lags", {
108
+ description: "List all LAGs (Link Aggregation Groups) at a site",
109
+ inputSchema: {
110
+ siteId: zod_1.z.string().describe("Site ID"),
111
+ offset: zod_1.z
112
+ .number()
113
+ .int()
114
+ .nonnegative()
115
+ .optional()
116
+ .describe("Number of records to skip (default: 0)"),
117
+ limit: zod_1.z
118
+ .number()
119
+ .int()
120
+ .min(1)
121
+ .max(200)
122
+ .optional()
123
+ .describe("Number of records to return (default: 25, max: 200)"),
124
+ filter: zod_1.z
125
+ .string()
126
+ .optional()
127
+ .describe("Filter expression"),
128
+ },
129
+ annotations: safety_js_1.READ_ONLY,
130
+ }, async ({ siteId, offset, limit, filter }) => {
131
+ try {
132
+ const query = (0, query_js_1.buildQuery)({ offset, limit, filter });
133
+ const data = await client.get(`/sites/${siteId}/switching/lags${query}`);
134
+ return (0, responses_js_1.formatSuccess)(data);
135
+ }
136
+ catch (err) {
137
+ return (0, responses_js_1.formatError)(err);
138
+ }
139
+ });
140
+ server.registerTool("unifi_get_lag", {
141
+ description: "Get details of a specific LAG (Link Aggregation Group)",
142
+ inputSchema: {
143
+ siteId: zod_1.z.string().describe("Site ID"),
144
+ lagId: zod_1.z.string().describe("LAG ID"),
145
+ },
146
+ annotations: safety_js_1.READ_ONLY,
147
+ }, async ({ siteId, lagId }) => {
148
+ try {
149
+ const data = await client.get(`/sites/${siteId}/switching/lags/${lagId}`);
150
+ return (0, responses_js_1.formatSuccess)(data);
151
+ }
152
+ catch (err) {
153
+ return (0, responses_js_1.formatError)(err);
154
+ }
155
+ });
156
+ }
@@ -64,9 +64,10 @@ function registerTrafficMatchingTools(server, client, readOnly = false) {
64
64
  type: zod_1.z
65
65
  .enum(["PORTS", "IPV4_ADDRESSES", "IPV6_ADDRESSES"])
66
66
  .describe("List type"),
67
- name: zod_1.z.string().describe("List name"),
67
+ name: zod_1.z.string().min(1).describe("List name"),
68
68
  items: zod_1.z
69
69
  .array(zod_1.z.unknown())
70
+ .min(1)
70
71
  .describe("List items (ports or IP addresses)"),
71
72
  dryRun: zod_1.z
72
73
  .boolean()
@@ -94,8 +95,8 @@ function registerTrafficMatchingTools(server, client, readOnly = false) {
94
95
  type: zod_1.z
95
96
  .enum(["PORTS", "IPV4_ADDRESSES", "IPV6_ADDRESSES"])
96
97
  .describe("List type"),
97
- name: zod_1.z.string().describe("List name"),
98
- items: zod_1.z.array(zod_1.z.unknown()).describe("List items"),
98
+ name: zod_1.z.string().min(1).describe("List name"),
99
+ items: zod_1.z.array(zod_1.z.unknown()).min(1).describe("List items"),
99
100
  dryRun: zod_1.z
100
101
  .boolean()
101
102
  .optional()
@@ -63,24 +63,118 @@ function registerWifiTools(server, client, readOnly = false) {
63
63
  siteId: zod_1.z.string().describe("Site ID"),
64
64
  name: zod_1.z.string().describe("SSID name"),
65
65
  enabled: zod_1.z.boolean().describe("Enable the WiFi network"),
66
- type: zod_1.z.enum(["STANDARD"]).describe("WiFi type"),
66
+ type: zod_1.z.enum(["STANDARD", "IOT_OPTIMIZED"]).describe("WiFi type"),
67
67
  broadcastingFrequenciesGHz: zod_1.z
68
68
  .array(zod_1.z.string())
69
69
  .describe("Frequencies: 2.4, 5, 6"),
70
+ securityConfiguration: zod_1.z
71
+ .record(zod_1.z.string(), zod_1.z.unknown())
72
+ .describe("Security configuration object"),
73
+ multicastToUnicastConversionEnabled: zod_1.z
74
+ .boolean()
75
+ .describe("Enable multicast to unicast conversion"),
76
+ clientIsolationEnabled: zod_1.z
77
+ .boolean()
78
+ .describe("Enable client isolation"),
79
+ hideName: zod_1.z.boolean().describe("Hide SSID name"),
80
+ uapsdEnabled: zod_1.z
81
+ .boolean()
82
+ .describe("Enable Unscheduled Automatic Power Save Delivery"),
83
+ arpProxyEnabled: zod_1.z.boolean().describe("Enable ARP proxy"),
84
+ bssTransitionEnabled: zod_1.z
85
+ .boolean()
86
+ .describe("Enable BSS transition"),
87
+ advertiseDeviceName: zod_1.z
88
+ .boolean()
89
+ .describe("Advertise device name in beacon frames"),
90
+ network: zod_1.z
91
+ .record(zod_1.z.string(), zod_1.z.unknown())
92
+ .optional()
93
+ .describe("Network reference"),
94
+ broadcastingDeviceFilter: zod_1.z
95
+ .record(zod_1.z.string(), zod_1.z.unknown())
96
+ .optional()
97
+ .describe("Custom scope of broadcasting devices"),
98
+ mdnsProxyConfiguration: zod_1.z
99
+ .record(zod_1.z.string(), zod_1.z.unknown())
100
+ .optional()
101
+ .describe("mDNS proxy configuration"),
102
+ multicastFilteringPolicy: zod_1.z
103
+ .record(zod_1.z.string(), zod_1.z.unknown())
104
+ .optional()
105
+ .describe("Multicast filtering policy"),
106
+ basicDataRateKbpsByFrequencyGHz: zod_1.z
107
+ .record(zod_1.z.string(), zod_1.z.unknown())
108
+ .optional()
109
+ .describe("Basic data rate in kbps by frequency"),
110
+ clientFilteringPolicy: zod_1.z
111
+ .record(zod_1.z.string(), zod_1.z.unknown())
112
+ .optional()
113
+ .describe("Client MAC address filtering policy"),
114
+ blackoutScheduleConfiguration: zod_1.z
115
+ .record(zod_1.z.string(), zod_1.z.unknown())
116
+ .optional()
117
+ .describe("Blackout schedule configuration"),
118
+ hotspotConfiguration: zod_1.z
119
+ .record(zod_1.z.string(), zod_1.z.unknown())
120
+ .optional()
121
+ .describe("Hotspot configuration"),
122
+ mloEnabled: zod_1.z
123
+ .boolean()
124
+ .optional()
125
+ .describe("Enable MLO (Multi-Link Operation)"),
126
+ bandSteeringEnabled: zod_1.z
127
+ .boolean()
128
+ .optional()
129
+ .describe("Enable band steering"),
130
+ dtimPeriodByFrequencyGHzOverride: zod_1.z
131
+ .record(zod_1.z.string(), zod_1.z.unknown())
132
+ .optional()
133
+ .describe("DTIM period override by frequency"),
70
134
  dryRun: zod_1.z
71
135
  .boolean()
72
136
  .optional()
73
137
  .describe("Preview this action without executing it"),
74
138
  },
75
139
  annotations: safety_js_1.WRITE_NOT_IDEMPOTENT,
76
- }, async ({ siteId, name, enabled, type, broadcastingFrequenciesGHz, dryRun, }) => {
140
+ }, async ({ siteId, name, enabled, type, broadcastingFrequenciesGHz, securityConfiguration, multicastToUnicastConversionEnabled, clientIsolationEnabled, hideName, uapsdEnabled, arpProxyEnabled, bssTransitionEnabled, advertiseDeviceName, network, broadcastingDeviceFilter, mdnsProxyConfiguration, multicastFilteringPolicy, basicDataRateKbpsByFrequencyGHz, clientFilteringPolicy, blackoutScheduleConfiguration, hotspotConfiguration, mloEnabled, bandSteeringEnabled, dtimPeriodByFrequencyGHzOverride, dryRun, }) => {
77
141
  try {
78
142
  const body = {
79
143
  name,
80
144
  enabled,
81
145
  type,
82
146
  broadcastingFrequenciesGHz: broadcastingFrequenciesGHz.map(Number),
147
+ securityConfiguration,
148
+ multicastToUnicastConversionEnabled,
149
+ clientIsolationEnabled,
150
+ hideName,
151
+ uapsdEnabled,
152
+ arpProxyEnabled,
153
+ bssTransitionEnabled,
154
+ advertiseDeviceName,
83
155
  };
156
+ if (network !== undefined)
157
+ body.network = network;
158
+ if (broadcastingDeviceFilter !== undefined)
159
+ body.broadcastingDeviceFilter = broadcastingDeviceFilter;
160
+ if (mdnsProxyConfiguration !== undefined)
161
+ body.mdnsProxyConfiguration = mdnsProxyConfiguration;
162
+ if (multicastFilteringPolicy !== undefined)
163
+ body.multicastFilteringPolicy = multicastFilteringPolicy;
164
+ if (basicDataRateKbpsByFrequencyGHz !== undefined)
165
+ body.basicDataRateKbpsByFrequencyGHz = basicDataRateKbpsByFrequencyGHz;
166
+ if (clientFilteringPolicy !== undefined)
167
+ body.clientFilteringPolicy = clientFilteringPolicy;
168
+ if (blackoutScheduleConfiguration !== undefined)
169
+ body.blackoutScheduleConfiguration = blackoutScheduleConfiguration;
170
+ if (hotspotConfiguration !== undefined)
171
+ body.hotspotConfiguration = hotspotConfiguration;
172
+ if (mloEnabled !== undefined)
173
+ body.mloEnabled = mloEnabled;
174
+ if (bandSteeringEnabled !== undefined)
175
+ body.bandSteeringEnabled = bandSteeringEnabled;
176
+ if (dtimPeriodByFrequencyGHzOverride !== undefined)
177
+ body.dtimPeriodByFrequencyGHzOverride = dtimPeriodByFrequencyGHzOverride;
84
178
  if (dryRun)
85
179
  return (0, safety_js_1.formatDryRun)("POST", `/sites/${siteId}/wifi/broadcasts`, body);
86
180
  const data = await client.post(`/sites/${siteId}/wifi/broadcasts`, body);
@@ -97,19 +191,143 @@ function registerWifiTools(server, client, readOnly = false) {
97
191
  wifiBroadcastId: zod_1.z.string().describe("WiFi Broadcast ID"),
98
192
  name: zod_1.z.string().optional().describe("SSID name"),
99
193
  enabled: zod_1.z.boolean().optional().describe("Enable the WiFi network"),
194
+ type: zod_1.z
195
+ .enum(["STANDARD", "IOT_OPTIMIZED"])
196
+ .optional()
197
+ .describe("WiFi type"),
198
+ broadcastingFrequenciesGHz: zod_1.z
199
+ .array(zod_1.z.string())
200
+ .optional()
201
+ .describe("Frequencies: 2.4, 5, 6"),
202
+ securityConfiguration: zod_1.z
203
+ .record(zod_1.z.string(), zod_1.z.unknown())
204
+ .optional()
205
+ .describe("Security configuration object"),
206
+ multicastToUnicastConversionEnabled: zod_1.z
207
+ .boolean()
208
+ .optional()
209
+ .describe("Enable multicast to unicast conversion"),
210
+ clientIsolationEnabled: zod_1.z
211
+ .boolean()
212
+ .optional()
213
+ .describe("Enable client isolation"),
214
+ hideName: zod_1.z.boolean().optional().describe("Hide SSID name"),
215
+ uapsdEnabled: zod_1.z
216
+ .boolean()
217
+ .optional()
218
+ .describe("Enable Unscheduled Automatic Power Save Delivery"),
219
+ arpProxyEnabled: zod_1.z
220
+ .boolean()
221
+ .optional()
222
+ .describe("Enable ARP proxy"),
223
+ bssTransitionEnabled: zod_1.z
224
+ .boolean()
225
+ .optional()
226
+ .describe("Enable BSS transition"),
227
+ advertiseDeviceName: zod_1.z
228
+ .boolean()
229
+ .optional()
230
+ .describe("Advertise device name in beacon frames"),
231
+ network: zod_1.z
232
+ .record(zod_1.z.string(), zod_1.z.unknown())
233
+ .optional()
234
+ .describe("Network reference"),
235
+ broadcastingDeviceFilter: zod_1.z
236
+ .record(zod_1.z.string(), zod_1.z.unknown())
237
+ .optional()
238
+ .describe("Custom scope of broadcasting devices"),
239
+ mdnsProxyConfiguration: zod_1.z
240
+ .record(zod_1.z.string(), zod_1.z.unknown())
241
+ .optional()
242
+ .describe("mDNS proxy configuration"),
243
+ multicastFilteringPolicy: zod_1.z
244
+ .record(zod_1.z.string(), zod_1.z.unknown())
245
+ .optional()
246
+ .describe("Multicast filtering policy"),
247
+ basicDataRateKbpsByFrequencyGHz: zod_1.z
248
+ .record(zod_1.z.string(), zod_1.z.unknown())
249
+ .optional()
250
+ .describe("Basic data rate in kbps by frequency"),
251
+ clientFilteringPolicy: zod_1.z
252
+ .record(zod_1.z.string(), zod_1.z.unknown())
253
+ .optional()
254
+ .describe("Client MAC address filtering policy"),
255
+ blackoutScheduleConfiguration: zod_1.z
256
+ .record(zod_1.z.string(), zod_1.z.unknown())
257
+ .optional()
258
+ .describe("Blackout schedule configuration"),
259
+ hotspotConfiguration: zod_1.z
260
+ .record(zod_1.z.string(), zod_1.z.unknown())
261
+ .optional()
262
+ .describe("Hotspot configuration"),
263
+ mloEnabled: zod_1.z
264
+ .boolean()
265
+ .optional()
266
+ .describe("Enable MLO (Multi-Link Operation)"),
267
+ bandSteeringEnabled: zod_1.z
268
+ .boolean()
269
+ .optional()
270
+ .describe("Enable band steering"),
271
+ dtimPeriodByFrequencyGHzOverride: zod_1.z
272
+ .record(zod_1.z.string(), zod_1.z.unknown())
273
+ .optional()
274
+ .describe("DTIM period override by frequency"),
100
275
  dryRun: zod_1.z
101
276
  .boolean()
102
277
  .optional()
103
278
  .describe("Preview this action without executing it"),
104
279
  },
105
280
  annotations: safety_js_1.WRITE,
106
- }, async ({ siteId, wifiBroadcastId, name, enabled, dryRun }) => {
281
+ }, async ({ siteId, wifiBroadcastId, name, enabled, type, broadcastingFrequenciesGHz, securityConfiguration, multicastToUnicastConversionEnabled, clientIsolationEnabled, hideName, uapsdEnabled, arpProxyEnabled, bssTransitionEnabled, advertiseDeviceName, network, broadcastingDeviceFilter, mdnsProxyConfiguration, multicastFilteringPolicy, basicDataRateKbpsByFrequencyGHz, clientFilteringPolicy, blackoutScheduleConfiguration, hotspotConfiguration, mloEnabled, bandSteeringEnabled, dtimPeriodByFrequencyGHzOverride, dryRun, }) => {
107
282
  try {
108
283
  const body = {};
109
284
  if (name !== undefined)
110
285
  body.name = name;
111
286
  if (enabled !== undefined)
112
287
  body.enabled = enabled;
288
+ if (type !== undefined)
289
+ body.type = type;
290
+ if (broadcastingFrequenciesGHz !== undefined)
291
+ body.broadcastingFrequenciesGHz = broadcastingFrequenciesGHz.map(Number);
292
+ if (securityConfiguration !== undefined)
293
+ body.securityConfiguration = securityConfiguration;
294
+ if (multicastToUnicastConversionEnabled !== undefined)
295
+ body.multicastToUnicastConversionEnabled =
296
+ multicastToUnicastConversionEnabled;
297
+ if (clientIsolationEnabled !== undefined)
298
+ body.clientIsolationEnabled = clientIsolationEnabled;
299
+ if (hideName !== undefined)
300
+ body.hideName = hideName;
301
+ if (uapsdEnabled !== undefined)
302
+ body.uapsdEnabled = uapsdEnabled;
303
+ if (arpProxyEnabled !== undefined)
304
+ body.arpProxyEnabled = arpProxyEnabled;
305
+ if (bssTransitionEnabled !== undefined)
306
+ body.bssTransitionEnabled = bssTransitionEnabled;
307
+ if (advertiseDeviceName !== undefined)
308
+ body.advertiseDeviceName = advertiseDeviceName;
309
+ if (network !== undefined)
310
+ body.network = network;
311
+ if (broadcastingDeviceFilter !== undefined)
312
+ body.broadcastingDeviceFilter = broadcastingDeviceFilter;
313
+ if (mdnsProxyConfiguration !== undefined)
314
+ body.mdnsProxyConfiguration = mdnsProxyConfiguration;
315
+ if (multicastFilteringPolicy !== undefined)
316
+ body.multicastFilteringPolicy = multicastFilteringPolicy;
317
+ if (basicDataRateKbpsByFrequencyGHz !== undefined)
318
+ body.basicDataRateKbpsByFrequencyGHz = basicDataRateKbpsByFrequencyGHz;
319
+ if (clientFilteringPolicy !== undefined)
320
+ body.clientFilteringPolicy = clientFilteringPolicy;
321
+ if (blackoutScheduleConfiguration !== undefined)
322
+ body.blackoutScheduleConfiguration = blackoutScheduleConfiguration;
323
+ if (hotspotConfiguration !== undefined)
324
+ body.hotspotConfiguration = hotspotConfiguration;
325
+ if (mloEnabled !== undefined)
326
+ body.mloEnabled = mloEnabled;
327
+ if (bandSteeringEnabled !== undefined)
328
+ body.bandSteeringEnabled = bandSteeringEnabled;
329
+ if (dtimPeriodByFrequencyGHzOverride !== undefined)
330
+ body.dtimPeriodByFrequencyGHzOverride = dtimPeriodByFrequencyGHzOverride;
113
331
  if (dryRun)
114
332
  return (0, safety_js_1.formatDryRun)("PUT", `/sites/${siteId}/wifi/broadcasts/${wifiBroadcastId}`, body);
115
333
  const data = await client.put(`/sites/${siteId}/wifi/broadcasts/${wifiBroadcastId}`, body);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@owine/unifi-network-mcp",
3
- "version": "2.1.0",
3
+ "version": "2.2.1",
4
4
  "description": "MCP server for the UniFi Network API",
5
5
  "main": "dist/index.js",
6
6
  "bin": {
@@ -42,10 +42,10 @@
42
42
  "devDependencies": {
43
43
  "@eslint/js": "10.0.1",
44
44
  "@types/node": "24.12.0",
45
- "@vitest/coverage-v8": "4.0.18",
46
- "eslint": "10.0.3",
45
+ "@vitest/coverage-v8": "4.1.2",
46
+ "eslint": "10.1.0",
47
47
  "typescript": "5.9.3",
48
- "typescript-eslint": "8.56.1",
49
- "vitest": "4.0.18"
48
+ "typescript-eslint": "8.57.2",
49
+ "vitest": "4.1.2"
50
50
  }
51
51
  }