@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.
- package/LICENSE +21 -0
- package/README.md +235 -80
- package/dist/client.d.ts +13 -1
- package/dist/client.js +142 -16
- package/dist/config.d.ts +11 -0
- package/dist/config.js +102 -3
- package/dist/guard.d.ts +46 -0
- package/dist/guard.js +171 -0
- package/dist/index.js +15 -4
- package/dist/instructions.d.ts +9 -0
- package/dist/instructions.js +32 -0
- package/dist/tls.d.ts +14 -0
- package/dist/tls.js +58 -0
- package/dist/tools/acl.js +126 -53
- package/dist/tools/client-history.js +20 -10
- package/dist/tools/clients.js +14 -13
- package/dist/tools/devices.js +30 -29
- package/dist/tools/dns-policies.js +116 -69
- package/dist/tools/firewall.d.ts +2 -1
- package/dist/tools/firewall.js +245 -80
- package/dist/tools/hotspot.js +35 -28
- package/dist/tools/index.d.ts +13 -1
- package/dist/tools/index.js +63 -4
- package/dist/tools/networks.d.ts +2 -1
- package/dist/tools/networks.js +152 -58
- package/dist/tools/sites.js +3 -3
- package/dist/tools/supporting.js +31 -30
- package/dist/tools/switching.js +22 -21
- package/dist/tools/system.js +1 -1
- package/dist/tools/traffic-matching.d.ts +2 -1
- package/dist/tools/traffic-matching.js +31 -28
- package/dist/tools/wifi.js +228 -311
- package/dist/utils/full-put.d.ts +32 -0
- package/dist/utils/full-put.js +74 -0
- package/dist/utils/output-schemas.d.ts +77 -9
- package/dist/utils/output-schemas.js +106 -54
- package/dist/utils/params.d.ts +7 -0
- package/dist/utils/params.js +12 -0
- package/dist/utils/responses.d.ts +2 -0
- package/dist/utils/responses.js +28 -2
- package/dist/utils/safety.d.ts +35 -1
- package/dist/utils/safety.js +53 -3
- package/dist/utils/secrets.d.ts +24 -0
- package/dist/utils/secrets.js +92 -0
- package/dist/utils/wan-exposure.d.ts +69 -0
- package/dist/utils/wan-exposure.js +245 -0
- package/dist/write-log.d.ts +52 -0
- package/dist/write-log.js +178 -0
- package/dist/write-tools.d.ts +27 -0
- package/dist/write-tools.js +50 -0
- package/package.json +19 -15
package/dist/tools/hotspot.js
CHANGED
|
@@ -6,26 +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");
|
|
10
|
+
const VOUCHER_BATCH_WITHOUT_CONFIRM = 10;
|
|
9
11
|
function registerHotspotTools(server, client, readOnly = false) {
|
|
10
12
|
server.registerTool("unifi_list_vouchers", {
|
|
11
13
|
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.",
|
|
12
14
|
inputSchema: {
|
|
13
|
-
siteId:
|
|
15
|
+
siteId: (0, params_js_1.resourceId)().describe("Site ID"),
|
|
14
16
|
offset: zod_1.z
|
|
15
17
|
.number()
|
|
16
18
|
.int()
|
|
17
19
|
.nonnegative()
|
|
18
20
|
.optional()
|
|
19
21
|
.default(0)
|
|
20
|
-
.describe("Number of records to skip
|
|
22
|
+
.describe("Number of records to skip"),
|
|
21
23
|
limit: zod_1.z
|
|
22
24
|
.number()
|
|
23
25
|
.int()
|
|
24
26
|
.min(0)
|
|
25
27
|
.max(1000)
|
|
26
28
|
.optional()
|
|
27
|
-
.default(
|
|
28
|
-
.describe("Number of records to return
|
|
29
|
+
.default(25)
|
|
30
|
+
.describe("Number of records to return"),
|
|
29
31
|
filter: zod_1.z
|
|
30
32
|
.string()
|
|
31
33
|
.optional()
|
|
@@ -46,8 +48,8 @@ function registerHotspotTools(server, client, readOnly = false) {
|
|
|
46
48
|
server.registerTool("unifi_get_voucher", {
|
|
47
49
|
description: "Get a specific hotspot voucher by ID (same fields as the list entry).",
|
|
48
50
|
inputSchema: {
|
|
49
|
-
siteId:
|
|
50
|
-
voucherId:
|
|
51
|
+
siteId: (0, params_js_1.resourceId)().describe("Site ID"),
|
|
52
|
+
voucherId: (0, params_js_1.resourceId)().describe("Voucher ID"),
|
|
51
53
|
},
|
|
52
54
|
outputSchema: output_schemas_js_1.voucherOutputSchema,
|
|
53
55
|
annotations: safety_js_1.READ_ONLY,
|
|
@@ -63,9 +65,9 @@ function registerHotspotTools(server, client, readOnly = false) {
|
|
|
63
65
|
if (readOnly)
|
|
64
66
|
return;
|
|
65
67
|
server.registerTool("unifi_create_voucher", {
|
|
66
|
-
description:
|
|
68
|
+
description: `Create one or more hotspot vouchers (use count for batch creation; more than ${VOUCHER_BATCH_WITHOUT_CONFIRM} at once needs confirm: true). Returns: vouchers[] — the created vouchers with their generated codes. Idempotency: not safe to retry — each call mints fresh codes.`,
|
|
67
69
|
inputSchema: {
|
|
68
|
-
siteId:
|
|
70
|
+
siteId: (0, params_js_1.resourceId)().describe("Site ID"),
|
|
69
71
|
name: zod_1.z
|
|
70
72
|
.string()
|
|
71
73
|
.describe("Voucher note/name (duplicated across all generated vouchers)"),
|
|
@@ -110,21 +112,30 @@ function registerHotspotTools(server, client, readOnly = false) {
|
|
|
110
112
|
.max(100000)
|
|
111
113
|
.optional()
|
|
112
114
|
.describe("Upload rate limit in kbps (2-100000)"),
|
|
115
|
+
confirm: zod_1.z
|
|
116
|
+
.boolean()
|
|
117
|
+
.optional()
|
|
118
|
+
.describe(`Set true to confirm creating more than ${VOUCHER_BATCH_WITHOUT_CONFIRM} vouchers at once; not needed with dryRun: true`),
|
|
113
119
|
dryRun: zod_1.z
|
|
114
120
|
.boolean()
|
|
115
121
|
.optional()
|
|
116
122
|
.describe("Preview this action without executing it"),
|
|
117
123
|
},
|
|
118
|
-
outputSchema: output_schemas_js_1.createVouchersOutputSchema,
|
|
124
|
+
outputSchema: (0, output_schemas_js_1.writeResult)(output_schemas_js_1.createVouchersOutputSchema),
|
|
119
125
|
annotations: safety_js_1.WRITE_NOT_IDEMPOTENT,
|
|
120
|
-
}, async ({ siteId, name, timeLimitMinutes, count, authorizedGuestLimit, dataUsageLimitMBytes, rxRateLimitKbps, txRateLimitKbps, dryRun, }) => {
|
|
126
|
+
}, async ({ siteId, name, timeLimitMinutes, count, authorizedGuestLimit, dataUsageLimitMBytes, rxRateLimitKbps, txRateLimitKbps, confirm, dryRun, }) => {
|
|
127
|
+
// Each voucher is a guest credential; a large batch is a lot of access
|
|
128
|
+
// to hand out on one unchecked call.
|
|
129
|
+
const opening = count > VOUCHER_BATCH_WITHOUT_CONFIRM ? `This creates ${count} guest access codes` : undefined;
|
|
130
|
+
const guard = (0, safety_js_1.requireConfirmationIf)(opening, confirm, dryRun);
|
|
131
|
+
if (guard)
|
|
132
|
+
return guard;
|
|
121
133
|
try {
|
|
122
134
|
const body = {
|
|
123
135
|
name,
|
|
124
136
|
timeLimitMinutes,
|
|
137
|
+
count,
|
|
125
138
|
};
|
|
126
|
-
if (count !== undefined)
|
|
127
|
-
body.count = count;
|
|
128
139
|
if (authorizedGuestLimit !== undefined)
|
|
129
140
|
body.authorizedGuestLimit = authorizedGuestLimit;
|
|
130
141
|
if (dataUsageLimitMBytes !== undefined)
|
|
@@ -134,7 +145,7 @@ function registerHotspotTools(server, client, readOnly = false) {
|
|
|
134
145
|
if (txRateLimitKbps !== undefined)
|
|
135
146
|
body.txRateLimitKbps = txRateLimitKbps;
|
|
136
147
|
if (dryRun)
|
|
137
|
-
return (0, safety_js_1.formatDryRun)("POST", `/sites/${siteId}/hotspot/vouchers`, body);
|
|
148
|
+
return (0, safety_js_1.formatDryRun)("POST", `/sites/${siteId}/hotspot/vouchers`, body, opening);
|
|
138
149
|
const data = await client.post(`/sites/${siteId}/hotspot/vouchers`, body);
|
|
139
150
|
return (0, responses_js_1.formatSuccess)(data, { structured: true });
|
|
140
151
|
}
|
|
@@ -145,12 +156,9 @@ function registerHotspotTools(server, client, readOnly = false) {
|
|
|
145
156
|
server.registerTool("unifi_delete_voucher", {
|
|
146
157
|
description: "DESTRUCTIVE: Delete a hotspot voucher. Returns: vouchersDeleted (count of vouchers removed).",
|
|
147
158
|
inputSchema: {
|
|
148
|
-
siteId:
|
|
149
|
-
voucherId:
|
|
150
|
-
confirm:
|
|
151
|
-
.boolean()
|
|
152
|
-
.optional()
|
|
153
|
-
.describe("Must be true to execute this destructive action"),
|
|
159
|
+
siteId: (0, params_js_1.resourceId)().describe("Site ID"),
|
|
160
|
+
voucherId: (0, params_js_1.resourceId)().describe("Voucher ID"),
|
|
161
|
+
confirm: (0, safety_js_1.confirmDestructive)(),
|
|
154
162
|
dryRun: zod_1.z
|
|
155
163
|
.boolean()
|
|
156
164
|
.optional()
|
|
@@ -158,7 +166,7 @@ function registerHotspotTools(server, client, readOnly = false) {
|
|
|
158
166
|
},
|
|
159
167
|
annotations: safety_js_1.DESTRUCTIVE,
|
|
160
168
|
}, async ({ siteId, voucherId, confirm, dryRun }) => {
|
|
161
|
-
const guard = (0, safety_js_1.requireConfirmation)(confirm, "This will permanently delete the voucher");
|
|
169
|
+
const guard = dryRun ? null : (0, safety_js_1.requireConfirmation)(confirm, "This will permanently delete the voucher");
|
|
162
170
|
if (guard)
|
|
163
171
|
return guard;
|
|
164
172
|
try {
|
|
@@ -173,16 +181,15 @@ function registerHotspotTools(server, client, readOnly = false) {
|
|
|
173
181
|
}
|
|
174
182
|
});
|
|
175
183
|
server.registerTool("unifi_bulk_delete_vouchers", {
|
|
176
|
-
description: "DESTRUCTIVE: Bulk delete hotspot vouchers
|
|
184
|
+
description: "DESTRUCTIVE: Bulk delete hotspot vouchers matching a filter. The dry run shows only the request, not which vouchers match: call unifi_list_vouchers with the same filter first. Returns: vouchersDeleted (count of vouchers removed).",
|
|
177
185
|
inputSchema: {
|
|
178
|
-
siteId:
|
|
186
|
+
siteId: (0, params_js_1.resourceId)().describe("Site ID"),
|
|
179
187
|
filter: zod_1.z
|
|
180
188
|
.string()
|
|
181
|
-
.
|
|
182
|
-
|
|
183
|
-
.
|
|
184
|
-
|
|
185
|
-
.describe("Must be true to execute this destructive action"),
|
|
189
|
+
.trim()
|
|
190
|
+
.min(1)
|
|
191
|
+
.describe("Required filter expression, e.g. expired.eq(true) or name.like('guest*')"),
|
|
192
|
+
confirm: (0, safety_js_1.confirmDestructive)(),
|
|
186
193
|
dryRun: zod_1.z
|
|
187
194
|
.boolean()
|
|
188
195
|
.optional()
|
|
@@ -191,7 +198,7 @@ function registerHotspotTools(server, client, readOnly = false) {
|
|
|
191
198
|
annotations: safety_js_1.DESTRUCTIVE,
|
|
192
199
|
}, async ({ siteId, filter, confirm, dryRun }) => {
|
|
193
200
|
try {
|
|
194
|
-
const guard = (0, safety_js_1.requireConfirmation)(confirm, "This will delete all vouchers matching the filter");
|
|
201
|
+
const guard = dryRun ? null : (0, safety_js_1.requireConfirmation)(confirm, "This will delete all vouchers matching the filter");
|
|
195
202
|
if (guard)
|
|
196
203
|
return guard;
|
|
197
204
|
const query = (0, query_js_1.buildQuery)({ filter });
|
package/dist/tools/index.d.ts
CHANGED
|
@@ -1,3 +1,15 @@
|
|
|
1
1
|
import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
|
|
2
2
|
import { NetworkClient } from "../client.js";
|
|
3
|
-
|
|
3
|
+
import { type WanPolicy } from "../utils/wan-exposure.js";
|
|
4
|
+
import type { WriteLog } from "../write-log.js";
|
|
5
|
+
export interface ToolOptions {
|
|
6
|
+
/** The write tools to register (UNIFI_NETWORK_WRITE_TOOLS); undefined registers them all. */
|
|
7
|
+
writeTools?: readonly string[];
|
|
8
|
+
/** WAN-exposure refusals (UNIFI_NETWORK_ALLOW_WAN_EXPOSURE, UNIFI_NETWORK_WAN_ZONE_IDS); on by default. */
|
|
9
|
+
wan?: WanPolicy;
|
|
10
|
+
/** UNIFI_NETWORK_WRITE_LOG; every write tool call appends a line. */
|
|
11
|
+
writeLog?: WriteLog;
|
|
12
|
+
}
|
|
13
|
+
export declare function registerAllTools(server: McpServer, client: NetworkClient, readOnly?: boolean, options?: ToolOptions): void;
|
|
14
|
+
/** "unifi_list_mc_lag_domains" → "List MC-LAG domains". */
|
|
15
|
+
export declare function toolTitle(name: string): string;
|
package/dist/tools/index.js
CHANGED
|
@@ -1,6 +1,11 @@
|
|
|
1
1
|
"use strict";
|
|
2
2
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
3
|
exports.registerAllTools = registerAllTools;
|
|
4
|
+
exports.toolTitle = toolTitle;
|
|
5
|
+
const client_js_1 = require("../client.js");
|
|
6
|
+
const output_schemas_js_1 = require("../utils/output-schemas.js");
|
|
7
|
+
const write_tools_js_1 = require("../write-tools.js");
|
|
8
|
+
const wan_exposure_js_1 = require("../utils/wan-exposure.js");
|
|
4
9
|
const system_js_1 = require("./system.js");
|
|
5
10
|
const sites_js_1 = require("./sites.js");
|
|
6
11
|
const devices_js_1 = require("./devices.js");
|
|
@@ -15,19 +20,73 @@ const dns_policies_js_1 = require("./dns-policies.js");
|
|
|
15
20
|
const traffic_matching_js_1 = require("./traffic-matching.js");
|
|
16
21
|
const switching_js_1 = require("./switching.js");
|
|
17
22
|
const supporting_js_1 = require("./supporting.js");
|
|
18
|
-
function registerAllTools(server, client, readOnly = false) {
|
|
23
|
+
function registerAllTools(server, client, readOnly = false, options = {}) {
|
|
24
|
+
server = instrumentTools(server, options);
|
|
25
|
+
const wan = options.wan ?? wan_exposure_js_1.DEFAULT_WAN_POLICY;
|
|
19
26
|
(0, system_js_1.registerSystemTools)(server, client);
|
|
20
27
|
(0, sites_js_1.registerSiteTools)(server, client);
|
|
21
28
|
(0, devices_js_1.registerDeviceTools)(server, client, readOnly);
|
|
22
29
|
(0, clients_js_1.registerClientTools)(server, client, readOnly);
|
|
23
30
|
(0, client_history_js_1.registerClientHistoryTools)(server, client);
|
|
24
|
-
(0, networks_js_1.registerNetworkTools)(server, client, readOnly);
|
|
31
|
+
(0, networks_js_1.registerNetworkTools)(server, client, readOnly, wan);
|
|
25
32
|
(0, wifi_js_1.registerWifiTools)(server, client, readOnly);
|
|
26
33
|
(0, hotspot_js_1.registerHotspotTools)(server, client, readOnly);
|
|
27
|
-
(0, firewall_js_1.registerFirewallTools)(server, client, readOnly);
|
|
34
|
+
(0, firewall_js_1.registerFirewallTools)(server, client, readOnly, wan);
|
|
28
35
|
(0, acl_js_1.registerAclTools)(server, client, readOnly);
|
|
29
36
|
(0, dns_policies_js_1.registerDnsPolicyTools)(server, client, readOnly);
|
|
30
|
-
(0, traffic_matching_js_1.registerTrafficMatchingTools)(server, client, readOnly);
|
|
37
|
+
(0, traffic_matching_js_1.registerTrafficMatchingTools)(server, client, readOnly, wan);
|
|
31
38
|
(0, switching_js_1.registerSwitchingTools)(server, client);
|
|
32
39
|
(0, supporting_js_1.registerSupportingTools)(server, client);
|
|
33
40
|
}
|
|
41
|
+
/**
|
|
42
|
+
* A view of server that applies these rules to every tool it registers, so no
|
|
43
|
+
* tool module can forget them:
|
|
44
|
+
*
|
|
45
|
+
* - Handlers run with their call's abort signal in scope, so NetworkClient
|
|
46
|
+
* aborts the controller request when the MCP client cancels the call. The
|
|
47
|
+
* SDK passes that signal in `extra`, the handler's last argument.
|
|
48
|
+
* - Each tool gets a human-readable title derived from its name.
|
|
49
|
+
* - A write tool the operator did not enable (UNIFI_NETWORK_WRITE_TOOLS) is
|
|
50
|
+
* not registered at all. A write tool missing from WRITE_TOOL_GROUPS, or a
|
|
51
|
+
* read tool listed there, is a bug: registration throws.
|
|
52
|
+
* - With a write log, every write tool call is recorded in it.
|
|
53
|
+
* - The outputSchema is published as a loose object. The SDK client validates
|
|
54
|
+
* structuredContent against the published JSON Schema, and tools pass the
|
|
55
|
+
* controller's response through; a raw shape publishes
|
|
56
|
+
* additionalProperties: false, so the first field a firmware update added
|
|
57
|
+
* would make the tool fail for every client that validates.
|
|
58
|
+
*/
|
|
59
|
+
function instrumentTools(server, { writeTools, writeLog }) {
|
|
60
|
+
const registerTool = (name, config, handler) => {
|
|
61
|
+
const write = config.annotations?.readOnlyHint === false;
|
|
62
|
+
if (write !== write_tools_js_1.WRITE_TOOLS.includes(name)) {
|
|
63
|
+
throw new Error(`${name} is ${write ? "a write tool missing from" : "a read tool listed in"} WRITE_TOOL_GROUPS (src/write-tools.ts)`);
|
|
64
|
+
}
|
|
65
|
+
if (write && writeTools !== undefined && !writeTools.includes(name))
|
|
66
|
+
return undefined;
|
|
67
|
+
return server.registerTool(name, {
|
|
68
|
+
...config,
|
|
69
|
+
title: config.title ?? toolTitle(name),
|
|
70
|
+
...(config.outputSchema ? { outputSchema: (0, output_schemas_js_1.looseOutput)(config.outputSchema) } : {}),
|
|
71
|
+
}, ((...args) => {
|
|
72
|
+
const extra = args.at(-1);
|
|
73
|
+
const signal = extra?.signal instanceof AbortSignal ? extra.signal : undefined;
|
|
74
|
+
const run = () => (0, client_js_1.withCallSignal)(signal, () => handler(...args));
|
|
75
|
+
return write && writeLog ? writeLog.record(name, args[0], run) : run();
|
|
76
|
+
}));
|
|
77
|
+
};
|
|
78
|
+
return new Proxy(server, {
|
|
79
|
+
get: (target, prop, receiver) => prop === "registerTool" ? registerTool : Reflect.get(target, prop, receiver),
|
|
80
|
+
});
|
|
81
|
+
}
|
|
82
|
+
// Words in tool names that read wrongly in plain sentence case.
|
|
83
|
+
const TITLE_WORDS = {
|
|
84
|
+
acl: "ACL", dns: "DNS", dpi: "DPI", vpn: "VPN", radius: "RADIUS",
|
|
85
|
+
wifi: "WiFi", wan: "WAN", wans: "WANs", lag: "LAG", lags: "LAGs", mc: "MC",
|
|
86
|
+
};
|
|
87
|
+
/** "unifi_list_mc_lag_domains" → "List MC-LAG domains". */
|
|
88
|
+
function toolTitle(name) {
|
|
89
|
+
const words = name.replace(/^unifi_/, "").split("_").map((word) => TITLE_WORDS[word] ?? word);
|
|
90
|
+
const title = words.join(" ").replace("MC LAG", "MC-LAG");
|
|
91
|
+
return title.charAt(0).toUpperCase() + title.slice(1);
|
|
92
|
+
}
|
package/dist/tools/networks.d.ts
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
1
|
import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
|
|
2
2
|
import { NetworkClient } from "../client.js";
|
|
3
|
-
|
|
3
|
+
import { type WanPolicy } from "../utils/wan-exposure.js";
|
|
4
|
+
export declare function registerNetworkTools(server: McpServer, client: NetworkClient, readOnly?: boolean, wan?: WanPolicy): void;
|
package/dist/tools/networks.js
CHANGED
|
@@ -5,29 +5,127 @@ 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");
|
|
9
|
-
|
|
10
|
+
const params_js_1 = require("../utils/params.js");
|
|
11
|
+
const wan_exposure_js_1 = require("../utils/wan-exposure.js");
|
|
12
|
+
// Request-body fields per management type, from the 11.0.83 POST/PUT schemas.
|
|
13
|
+
const COMMON_FIELDS = ["management", "name", "enabled", "vlanId", "dhcpGuarding"];
|
|
14
|
+
const COMMON_REQUIRED = ["management", "name", "enabled", "vlanId"];
|
|
15
|
+
const NETWORK_VARIANTS = {
|
|
16
|
+
UNMANAGED: { allowed: COMMON_FIELDS, required: COMMON_REQUIRED },
|
|
17
|
+
GATEWAY: {
|
|
18
|
+
allowed: [
|
|
19
|
+
...COMMON_FIELDS,
|
|
20
|
+
"isolationEnabled",
|
|
21
|
+
"cellularBackupEnabled",
|
|
22
|
+
"zoneId",
|
|
23
|
+
"internetAccessEnabled",
|
|
24
|
+
"mdnsForwardingEnabled",
|
|
25
|
+
"ipv4Configuration",
|
|
26
|
+
"ipv6Configuration",
|
|
27
|
+
],
|
|
28
|
+
required: [
|
|
29
|
+
...COMMON_REQUIRED,
|
|
30
|
+
"isolationEnabled",
|
|
31
|
+
"cellularBackupEnabled",
|
|
32
|
+
"internetAccessEnabled",
|
|
33
|
+
"ipv4Configuration",
|
|
34
|
+
],
|
|
35
|
+
},
|
|
36
|
+
SWITCH: {
|
|
37
|
+
allowed: [
|
|
38
|
+
...COMMON_FIELDS,
|
|
39
|
+
"isolationEnabled",
|
|
40
|
+
"cellularBackupEnabled",
|
|
41
|
+
"deviceId",
|
|
42
|
+
"ipv4Configuration",
|
|
43
|
+
],
|
|
44
|
+
required: [
|
|
45
|
+
...COMMON_REQUIRED,
|
|
46
|
+
"isolationEnabled",
|
|
47
|
+
"cellularBackupEnabled",
|
|
48
|
+
"deviceId",
|
|
49
|
+
"ipv4Configuration",
|
|
50
|
+
],
|
|
51
|
+
},
|
|
52
|
+
};
|
|
53
|
+
const NETWORK_VARIANTS_DOC = "all types take name, enabled, vlanId, optional dhcpGuarding (omitted or null = disabled). GATEWAY (routed by the UniFi gateway) also requires isolationEnabled, cellularBackupEnabled, internetAccessEnabled, ipv4Configuration (gateway-managed subnet + DHCP), optional zoneId, mdnsForwardingEnabled, ipv6Configuration. SWITCH (routed by an L3 switch) also requires isolationEnabled, cellularBackupEnabled, deviceId (the L3 switch), ipv4Configuration (switch-managed).";
|
|
54
|
+
function fieldsForManagement(management) {
|
|
55
|
+
if (management === "UNMANAGED" || management === "GATEWAY" || management === "SWITCH") {
|
|
56
|
+
return NETWORK_VARIANTS[management];
|
|
57
|
+
}
|
|
58
|
+
throw new Error(`Unsupported network management type: ${String(management)}`);
|
|
59
|
+
}
|
|
60
|
+
function checkNetworkBody(body, updates) {
|
|
61
|
+
const { allowed, required } = fieldsForManagement(body.management);
|
|
62
|
+
const rejected = (0, full_put_js_1.unsupportedFields)(updates, allowed);
|
|
63
|
+
if (rejected.length > 0) {
|
|
64
|
+
throw new Error(`Fields not accepted for management ${String(body.management)}: ${rejected.join(", ")}`);
|
|
65
|
+
}
|
|
66
|
+
const missing = (0, full_put_js_1.missingFields)(body, required);
|
|
67
|
+
if (missing.length > 0) {
|
|
68
|
+
throw new Error(`Missing required fields for management ${String(body.management)}: ${missing.join(", ")}`);
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
const config = (description) => zod_1.z.record(zod_1.z.string(), zod_1.z.unknown()).describe(description);
|
|
72
|
+
// Every body field as an optional input. Create tightens the common fields to
|
|
73
|
+
// required; update keeps them optional and fills gaps from the current network.
|
|
74
|
+
const networkFields = {
|
|
75
|
+
name: zod_1.z.string().optional().describe("Network name"),
|
|
76
|
+
management: zod_1.z
|
|
77
|
+
.enum(["UNMANAGED", "GATEWAY", "SWITCH"])
|
|
78
|
+
.optional()
|
|
79
|
+
.describe("Network management type"),
|
|
80
|
+
enabled: zod_1.z.boolean().optional().describe("Enable the network"),
|
|
81
|
+
vlanId: zod_1.z
|
|
82
|
+
.number()
|
|
83
|
+
.int()
|
|
84
|
+
.min(1)
|
|
85
|
+
.max(4009)
|
|
86
|
+
.optional()
|
|
87
|
+
.describe("VLAN ID (1 for the default network, 2-4009 for additional networks)"),
|
|
88
|
+
dhcpGuarding: config("DHCP Guarding settings (trustedDhcpServerIpAddresses). Omitted or null = disabled.")
|
|
89
|
+
.nullable()
|
|
90
|
+
.optional(),
|
|
91
|
+
isolationEnabled: zod_1.z.boolean().optional().describe("Isolate this network from all other networks. GATEWAY/SWITCH."),
|
|
92
|
+
cellularBackupEnabled: zod_1.z.boolean().optional().describe("Allow cellular data when WAN connection(s) are down. GATEWAY/SWITCH."),
|
|
93
|
+
internetAccessEnabled: zod_1.z.boolean().optional().describe("Allow internet access for devices on this network. GATEWAY only."),
|
|
94
|
+
mdnsForwardingEnabled: zod_1.z
|
|
95
|
+
.boolean()
|
|
96
|
+
.nullable()
|
|
97
|
+
.optional()
|
|
98
|
+
.describe("Participate in mDNS forwarding; null = use the site mDNS default. GATEWAY only."),
|
|
99
|
+
zoneId: (0, params_js_1.resourceId)().optional().describe("Firewall zone ID for this network. GATEWAY only."),
|
|
100
|
+
deviceId: (0, params_js_1.resourceId)().optional().describe("ID of the L3 switching capable device that manages this network. SWITCH only."),
|
|
101
|
+
ipv4Configuration: config("IPv4 configuration (hostIpAddress, prefixLength, dhcpConfiguration, …) — copy the shape from unifi_get_network. GATEWAY/SWITCH.")
|
|
102
|
+
.optional(),
|
|
103
|
+
ipv6Configuration: config("IPv6 configuration (interfaceType, clientAddressAssignment, routerAdvertisement, …); omitted or null = IPv6 not configured. GATEWAY only.")
|
|
104
|
+
.nullable()
|
|
105
|
+
.optional(),
|
|
106
|
+
};
|
|
107
|
+
function registerNetworkTools(server, client, readOnly = false, wan = wan_exposure_js_1.DEFAULT_WAN_POLICY) {
|
|
10
108
|
server.registerTool("unifi_list_networks", {
|
|
11
|
-
description: "List all networks (VLANs/LAN segments) at a site. Returns: id, name, management (UNMANAGED/GATEWAY/SWITCH), enabled, vlanId, default (true for the default network), zoneId (the firewall zone the network sits in), dhcpGuarding, metadata.origin. NOTE: the list view is sparse — for subnet/DHCP/NTP detail (ipv4Configuration/ipv6Configuration), call unifi_get_network on a specific id. Use for: VLAN inventory; pair with unifi_get_network_references to find what consumes a network.",
|
|
109
|
+
description: "List all networks (VLANs/LAN segments) at a site. Returns: id, name, management (UNMANAGED/GATEWAY/SWITCH), enabled, vlanId, default (true for the default network), zoneId (the firewall zone the network sits in), dhcpGuarding (only present when enabled), metadata.origin (+ metadata.configurable on the system-defined default network). NOTE: the list view is sparse — for subnet/DHCP/NTP detail (ipv4Configuration/ipv6Configuration), call unifi_get_network on a specific id. Use for: VLAN inventory; pair with unifi_get_network_references to find what consumes a network.",
|
|
12
110
|
inputSchema: {
|
|
13
|
-
siteId:
|
|
111
|
+
siteId: (0, params_js_1.resourceId)().describe("Site ID"),
|
|
14
112
|
offset: zod_1.z
|
|
15
113
|
.number()
|
|
16
114
|
.int()
|
|
17
115
|
.nonnegative()
|
|
18
116
|
.optional()
|
|
19
|
-
.describe("Number of records to skip
|
|
117
|
+
.describe("Number of records to skip"),
|
|
20
118
|
limit: zod_1.z
|
|
21
119
|
.number()
|
|
22
120
|
.int()
|
|
23
121
|
.min(0)
|
|
24
122
|
.max(200)
|
|
25
123
|
.optional()
|
|
26
|
-
.describe("Number of records to return (default: 25
|
|
124
|
+
.describe("Number of records to return (API default: 25)"),
|
|
27
125
|
filter: zod_1.z
|
|
28
126
|
.string()
|
|
29
127
|
.optional()
|
|
30
|
-
.describe("Filter expression"),
|
|
128
|
+
.describe("Filter expression, e.g. vlanId.eq(20)"),
|
|
31
129
|
},
|
|
32
130
|
outputSchema: output_schemas_js_1.listNetworksOutputSchema,
|
|
33
131
|
annotations: safety_js_1.READ_ONLY,
|
|
@@ -42,10 +140,10 @@ function registerNetworkTools(server, client, readOnly = false) {
|
|
|
42
140
|
}
|
|
43
141
|
});
|
|
44
142
|
server.registerTool("unifi_get_network", {
|
|
45
|
-
description: "Get a network/VLAN by ID. Returns the list fields PLUS (live-verified) isolationEnabled, internetAccessEnabled, mdnsForwardingEnabled, cellularBackupEnabled, a full ipv4Configuration object (autoScaleEnabled, hostIpAddress, prefixLength, dhcpConfiguration with mode/ipAddressRange/leaseTimeSeconds/domainName/pingConflictDetectionEnabled/ntpServerIpAddresses), and an ipv6Configuration object (interfaceType, clientAddressAssignment, routerAdvertisement, hostIpAddress, prefixLength). NOTE: subnet/DHCP detail appears here at get-by-id but NOT in unifi_list_networks (sparse list view).",
|
|
143
|
+
description: "Get a network/VLAN by ID. Returns the list fields PLUS (live-verified) isolationEnabled, internetAccessEnabled, mdnsForwardingEnabled, cellularBackupEnabled, a full ipv4Configuration object (autoScaleEnabled, hostIpAddress, prefixLength, dhcpConfiguration with mode/ipAddressRange/leaseTimeSeconds/domainName/pingConflictDetectionEnabled/ntpServerIpAddresses — which may contain empty-string entries), and an ipv6Configuration object when IPv6 is configured (interfaceType, clientAddressAssignment, routerAdvertisement, hostIpAddress, prefixLength). NOTE: subnet/DHCP detail appears here at get-by-id but NOT in unifi_list_networks (sparse list view).",
|
|
46
144
|
inputSchema: {
|
|
47
|
-
siteId:
|
|
48
|
-
networkId:
|
|
145
|
+
siteId: (0, params_js_1.resourceId)().describe("Site ID"),
|
|
146
|
+
networkId: (0, params_js_1.resourceId)().describe("Network ID"),
|
|
49
147
|
},
|
|
50
148
|
outputSchema: output_schemas_js_1.networkOutputSchema,
|
|
51
149
|
annotations: safety_js_1.READ_ONLY,
|
|
@@ -59,10 +157,10 @@ function registerNetworkTools(server, client, readOnly = false) {
|
|
|
59
157
|
}
|
|
60
158
|
});
|
|
61
159
|
server.registerTool("unifi_get_network_references", {
|
|
62
|
-
description: "Get all objects that reference this network
|
|
160
|
+
description: "Get all objects that reference this network. Returns: { referenceResources: [{ resourceType (e.g. WIFI), referenceCount, references: [{ referenceId }] }] }. Use before deleting a network to find dependencies that need to be re-pointed or removed. NOTE: on 11.0.83 the controller returns HTTP 500 (api.unexpected-error) for many networks — observed on the default network and on networks used by PPSK keys. On a 500, fall back to unifi_list_wifi + unifi_get_wifi (network.networkId and securityConfiguration.presharedKeys[].network.networkId) and unifi_list_firewall_zones (networkIds).",
|
|
63
161
|
inputSchema: {
|
|
64
|
-
siteId:
|
|
65
|
-
networkId:
|
|
162
|
+
siteId: (0, params_js_1.resourceId)().describe("Site ID"),
|
|
163
|
+
networkId: (0, params_js_1.resourceId)().describe("Network ID"),
|
|
66
164
|
},
|
|
67
165
|
outputSchema: output_schemas_js_1.getNetworkReferencesOutputSchema,
|
|
68
166
|
annotations: safety_js_1.READ_ONLY,
|
|
@@ -78,9 +176,10 @@ function registerNetworkTools(server, client, readOnly = false) {
|
|
|
78
176
|
if (readOnly)
|
|
79
177
|
return;
|
|
80
178
|
server.registerTool("unifi_create_network", {
|
|
81
|
-
description:
|
|
179
|
+
description: `Create a new VLAN/network. The body depends on management (per 11.0.83 docs): ${NETWORK_VARIANTS_DOC} Idempotency: not safe to retry — re-running creates duplicates.`,
|
|
82
180
|
inputSchema: {
|
|
83
|
-
siteId:
|
|
181
|
+
siteId: (0, params_js_1.resourceId)().describe("Site ID"),
|
|
182
|
+
...networkFields,
|
|
84
183
|
name: zod_1.z.string().describe("Network name"),
|
|
85
184
|
management: zod_1.z
|
|
86
185
|
.enum(["UNMANAGED", "GATEWAY", "SWITCH"])
|
|
@@ -92,22 +191,22 @@ function registerNetworkTools(server, client, readOnly = false) {
|
|
|
92
191
|
.min(1)
|
|
93
192
|
.max(4009)
|
|
94
193
|
.describe("VLAN ID (1 for default, 2+ for additional)"),
|
|
95
|
-
dhcpGuarding: zod_1.z
|
|
96
|
-
.record(zod_1.z.string(), zod_1.z.unknown())
|
|
97
|
-
.optional()
|
|
98
|
-
.describe("DHCP Guarding settings (trustedDhcpServerIpAddresses)"),
|
|
99
194
|
dryRun: zod_1.z
|
|
100
195
|
.boolean()
|
|
101
196
|
.optional()
|
|
102
197
|
.describe("Preview this action without executing it"),
|
|
103
198
|
},
|
|
104
|
-
outputSchema: output_schemas_js_1.networkOutputSchema,
|
|
199
|
+
outputSchema: (0, output_schemas_js_1.writeResult)(output_schemas_js_1.networkOutputSchema),
|
|
105
200
|
annotations: safety_js_1.WRITE_NOT_IDEMPOTENT,
|
|
106
|
-
}, async ({ siteId,
|
|
201
|
+
}, async ({ siteId, dryRun, ...fields }) => {
|
|
107
202
|
try {
|
|
108
|
-
const body =
|
|
109
|
-
|
|
110
|
-
|
|
203
|
+
const body = (0, full_put_js_1.definedEntries)(fields);
|
|
204
|
+
checkNetworkBody(body, body);
|
|
205
|
+
if (typeof body.zoneId === "string") {
|
|
206
|
+
const exposure = await new wan_exposure_js_1.WanExposure(client, wan, siteId).networkZone(body.zoneId);
|
|
207
|
+
if (exposure)
|
|
208
|
+
return (0, wan_exposure_js_1.wanRefusal)(exposure, "POST", `/sites/${siteId}/networks`);
|
|
209
|
+
}
|
|
111
210
|
if (dryRun)
|
|
112
211
|
return (0, safety_js_1.formatDryRun)("POST", `/sites/${siteId}/networks`, body);
|
|
113
212
|
const data = await client.post(`/sites/${siteId}/networks`, body);
|
|
@@ -118,41 +217,39 @@ function registerNetworkTools(server, client, readOnly = false) {
|
|
|
118
217
|
}
|
|
119
218
|
});
|
|
120
219
|
server.registerTool("unifi_update_network", {
|
|
121
|
-
description:
|
|
220
|
+
description: `Update an existing network. Pass only the fields to change. The API's PUT replaces the whole network — omitting dhcpGuarding disables DHCP guarding, and GATEWAY networks must resend their full ipv4Configuration — so this tool reads the current network, keeps every documented body field for its management type, overlays your changes and sends the full body. Pass null to clear an optional field (e.g. ipv6Configuration: null removes IPv6). Nested objects such as ipv4Configuration are replaced whole, not deep-merged: read them with unifi_get_network and send the complete object. Variants: ${NETWORK_VARIANTS_DOC} dryRun shows the merged body.`,
|
|
122
221
|
inputSchema: {
|
|
123
|
-
siteId:
|
|
124
|
-
networkId:
|
|
125
|
-
|
|
126
|
-
management: zod_1.z
|
|
127
|
-
.enum(["UNMANAGED", "GATEWAY", "SWITCH"])
|
|
128
|
-
.describe("Network management type"),
|
|
129
|
-
enabled: zod_1.z.boolean().describe("Enable the network"),
|
|
130
|
-
vlanId: zod_1.z
|
|
131
|
-
.number()
|
|
132
|
-
.int()
|
|
133
|
-
.min(1)
|
|
134
|
-
.max(4009)
|
|
135
|
-
.describe("VLAN ID (1-4009)"),
|
|
136
|
-
dhcpGuarding: zod_1.z
|
|
137
|
-
.record(zod_1.z.string(), zod_1.z.unknown())
|
|
138
|
-
.optional()
|
|
139
|
-
.describe("DHCP Guarding settings (trustedDhcpServerIpAddresses)"),
|
|
222
|
+
siteId: (0, params_js_1.resourceId)().describe("Site ID"),
|
|
223
|
+
networkId: (0, params_js_1.resourceId)().describe("Network ID"),
|
|
224
|
+
...networkFields,
|
|
140
225
|
dryRun: zod_1.z
|
|
141
226
|
.boolean()
|
|
142
227
|
.optional()
|
|
143
|
-
.describe("Preview
|
|
228
|
+
.describe("Preview the merged PUT body without executing it (still reads the current network)"),
|
|
144
229
|
},
|
|
145
|
-
outputSchema: output_schemas_js_1.networkOutputSchema,
|
|
230
|
+
outputSchema: (0, output_schemas_js_1.writeResult)(output_schemas_js_1.networkOutputSchema),
|
|
146
231
|
annotations: safety_js_1.WRITE,
|
|
147
|
-
}, async ({ siteId, networkId,
|
|
232
|
+
}, async ({ siteId, networkId, dryRun, ...fields }) => {
|
|
148
233
|
try {
|
|
149
|
-
const
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
234
|
+
const path = `/sites/${siteId}/networks/${networkId}`;
|
|
235
|
+
return await (0, full_put_js_1.withResourceLock)(path, async () => {
|
|
236
|
+
const current = (0, full_put_js_1.asObject)(await client.get(path), "the current network");
|
|
237
|
+
const updates = (0, full_put_js_1.definedEntries)(fields);
|
|
238
|
+
const { allowed } = fieldsForManagement(updates.management ?? current.management);
|
|
239
|
+
const body = (0, full_put_js_1.mergeForPut)(current, updates, allowed);
|
|
240
|
+
checkNetworkBody(body, updates);
|
|
241
|
+
const currentZoneId = typeof current.zoneId === "string" ? current.zoneId : undefined;
|
|
242
|
+
// Zone IDs are compared without case, as the WAN check compares them.
|
|
243
|
+
if (typeof body.zoneId === "string" && body.zoneId.toLowerCase() !== currentZoneId?.toLowerCase()) {
|
|
244
|
+
const exposure = await new wan_exposure_js_1.WanExposure(client, wan, siteId).networkZone(body.zoneId, currentZoneId);
|
|
245
|
+
if (exposure)
|
|
246
|
+
return (0, wan_exposure_js_1.wanRefusal)(exposure, "PUT", path);
|
|
247
|
+
}
|
|
248
|
+
if (dryRun)
|
|
249
|
+
return (0, safety_js_1.formatDryRun)("PUT", path, body);
|
|
250
|
+
const data = await client.put(path, body);
|
|
251
|
+
return (0, responses_js_1.formatSuccess)(data, { structured: true });
|
|
252
|
+
});
|
|
156
253
|
}
|
|
157
254
|
catch (err) {
|
|
158
255
|
return (0, responses_js_1.formatError)(err);
|
|
@@ -161,17 +258,14 @@ function registerNetworkTools(server, client, readOnly = false) {
|
|
|
161
258
|
server.registerTool("unifi_delete_network", {
|
|
162
259
|
description: "DESTRUCTIVE: Delete a network — all clients on this network will be disconnected",
|
|
163
260
|
inputSchema: {
|
|
164
|
-
siteId:
|
|
165
|
-
networkId:
|
|
261
|
+
siteId: (0, params_js_1.resourceId)().describe("Site ID"),
|
|
262
|
+
networkId: (0, params_js_1.resourceId)().describe("Network ID"),
|
|
166
263
|
force: zod_1.z
|
|
167
264
|
.boolean()
|
|
168
265
|
.optional()
|
|
169
266
|
.default(false)
|
|
170
267
|
.describe("Force delete (default: false)"),
|
|
171
|
-
confirm:
|
|
172
|
-
.boolean()
|
|
173
|
-
.optional()
|
|
174
|
-
.describe("Must be true to execute this destructive action"),
|
|
268
|
+
confirm: (0, safety_js_1.confirmDestructive)(),
|
|
175
269
|
dryRun: zod_1.z
|
|
176
270
|
.boolean()
|
|
177
271
|
.optional()
|
|
@@ -179,7 +273,7 @@ function registerNetworkTools(server, client, readOnly = false) {
|
|
|
179
273
|
},
|
|
180
274
|
annotations: safety_js_1.DESTRUCTIVE,
|
|
181
275
|
}, async ({ siteId, networkId, force, confirm, dryRun }) => {
|
|
182
|
-
const guard = (0, safety_js_1.requireConfirmation)(confirm, "This will delete the network and disconnect all clients on it");
|
|
276
|
+
const guard = dryRun ? null : (0, safety_js_1.requireConfirmation)(confirm, "This will delete the network and disconnect all clients on it");
|
|
183
277
|
if (guard)
|
|
184
278
|
return guard;
|
|
185
279
|
try {
|
package/dist/tools/sites.js
CHANGED
|
@@ -15,18 +15,18 @@ function registerSiteTools(server, client) {
|
|
|
15
15
|
.int()
|
|
16
16
|
.nonnegative()
|
|
17
17
|
.optional()
|
|
18
|
-
.describe("Number of records to skip
|
|
18
|
+
.describe("Number of records to skip"),
|
|
19
19
|
limit: zod_1.z
|
|
20
20
|
.number()
|
|
21
21
|
.int()
|
|
22
22
|
.min(0)
|
|
23
23
|
.max(200)
|
|
24
24
|
.optional()
|
|
25
|
-
.describe("Number of records to return (default: 25
|
|
25
|
+
.describe("Number of records to return (API default: 25)"),
|
|
26
26
|
filter: zod_1.z
|
|
27
27
|
.string()
|
|
28
28
|
.optional()
|
|
29
|
-
.describe("Filter expression (
|
|
29
|
+
.describe("Filter expression. Filterable: id, internalReference, name — eq/ne/in/notIn only (like is rejected). Example: name.eq('Default')"),
|
|
30
30
|
},
|
|
31
31
|
outputSchema: output_schemas_js_1.listSitesOutputSchema,
|
|
32
32
|
annotations: safety_js_1.READ_ONLY,
|