@owine/unifi-network-mcp 2.15.0 → 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 +226 -73
- 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 +123 -50
- package/dist/tools/client-history.js +20 -10
- package/dist/tools/clients.js +10 -9
- package/dist/tools/devices.js +21 -20
- package/dist/tools/dns-policies.js +27 -27
- package/dist/tools/firewall.d.ts +2 -1
- package/dist/tools/firewall.js +200 -75
- 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 +44 -31
- package/dist/tools/sites.js +2 -2
- package/dist/tools/supporting.js +22 -21
- package/dist/tools/switching.js +16 -15
- package/dist/tools/system.js +1 -1
- package/dist/tools/traffic-matching.d.ts +2 -1
- package/dist/tools/traffic-matching.js +22 -20
- package/dist/tools/wifi.js +76 -30
- package/dist/utils/full-put.d.ts +11 -2
- package/dist/utils/full-put.js +25 -2
- package/dist/utils/output-schemas.d.ts +26 -4
- package/dist/utils/output-schemas.js +80 -48
- 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.js +3 -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 +17 -13
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
|
@@ -7,6 +7,8 @@ const query_js_1 = require("../utils/query.js");
|
|
|
7
7
|
const safety_js_1 = require("../utils/safety.js");
|
|
8
8
|
const full_put_js_1 = require("../utils/full-put.js");
|
|
9
9
|
const output_schemas_js_1 = require("../utils/output-schemas.js");
|
|
10
|
+
const params_js_1 = require("../utils/params.js");
|
|
11
|
+
const wan_exposure_js_1 = require("../utils/wan-exposure.js");
|
|
10
12
|
// Request-body fields per management type, from the 11.0.83 POST/PUT schemas.
|
|
11
13
|
const COMMON_FIELDS = ["management", "name", "enabled", "vlanId", "dhcpGuarding"];
|
|
12
14
|
const COMMON_REQUIRED = ["management", "name", "enabled", "vlanId"];
|
|
@@ -94,32 +96,32 @@ const networkFields = {
|
|
|
94
96
|
.nullable()
|
|
95
97
|
.optional()
|
|
96
98
|
.describe("Participate in mDNS forwarding; null = use the site mDNS default. GATEWAY only."),
|
|
97
|
-
zoneId:
|
|
98
|
-
deviceId:
|
|
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."),
|
|
99
101
|
ipv4Configuration: config("IPv4 configuration (hostIpAddress, prefixLength, dhcpConfiguration, …) — copy the shape from unifi_get_network. GATEWAY/SWITCH.")
|
|
100
102
|
.optional(),
|
|
101
103
|
ipv6Configuration: config("IPv6 configuration (interfaceType, clientAddressAssignment, routerAdvertisement, …); omitted or null = IPv6 not configured. GATEWAY only.")
|
|
102
104
|
.nullable()
|
|
103
105
|
.optional(),
|
|
104
106
|
};
|
|
105
|
-
function registerNetworkTools(server, client, readOnly = false) {
|
|
107
|
+
function registerNetworkTools(server, client, readOnly = false, wan = wan_exposure_js_1.DEFAULT_WAN_POLICY) {
|
|
106
108
|
server.registerTool("unifi_list_networks", {
|
|
107
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.",
|
|
108
110
|
inputSchema: {
|
|
109
|
-
siteId:
|
|
111
|
+
siteId: (0, params_js_1.resourceId)().describe("Site ID"),
|
|
110
112
|
offset: zod_1.z
|
|
111
113
|
.number()
|
|
112
114
|
.int()
|
|
113
115
|
.nonnegative()
|
|
114
116
|
.optional()
|
|
115
|
-
.describe("Number of records to skip
|
|
117
|
+
.describe("Number of records to skip"),
|
|
116
118
|
limit: zod_1.z
|
|
117
119
|
.number()
|
|
118
120
|
.int()
|
|
119
121
|
.min(0)
|
|
120
122
|
.max(200)
|
|
121
123
|
.optional()
|
|
122
|
-
.describe("Number of records to return (default: 25
|
|
124
|
+
.describe("Number of records to return (API default: 25)"),
|
|
123
125
|
filter: zod_1.z
|
|
124
126
|
.string()
|
|
125
127
|
.optional()
|
|
@@ -140,8 +142,8 @@ function registerNetworkTools(server, client, readOnly = false) {
|
|
|
140
142
|
server.registerTool("unifi_get_network", {
|
|
141
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).",
|
|
142
144
|
inputSchema: {
|
|
143
|
-
siteId:
|
|
144
|
-
networkId:
|
|
145
|
+
siteId: (0, params_js_1.resourceId)().describe("Site ID"),
|
|
146
|
+
networkId: (0, params_js_1.resourceId)().describe("Network ID"),
|
|
145
147
|
},
|
|
146
148
|
outputSchema: output_schemas_js_1.networkOutputSchema,
|
|
147
149
|
annotations: safety_js_1.READ_ONLY,
|
|
@@ -157,8 +159,8 @@ function registerNetworkTools(server, client, readOnly = false) {
|
|
|
157
159
|
server.registerTool("unifi_get_network_references", {
|
|
158
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).",
|
|
159
161
|
inputSchema: {
|
|
160
|
-
siteId:
|
|
161
|
-
networkId:
|
|
162
|
+
siteId: (0, params_js_1.resourceId)().describe("Site ID"),
|
|
163
|
+
networkId: (0, params_js_1.resourceId)().describe("Network ID"),
|
|
162
164
|
},
|
|
163
165
|
outputSchema: output_schemas_js_1.getNetworkReferencesOutputSchema,
|
|
164
166
|
annotations: safety_js_1.READ_ONLY,
|
|
@@ -176,7 +178,7 @@ function registerNetworkTools(server, client, readOnly = false) {
|
|
|
176
178
|
server.registerTool("unifi_create_network", {
|
|
177
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.`,
|
|
178
180
|
inputSchema: {
|
|
179
|
-
siteId:
|
|
181
|
+
siteId: (0, params_js_1.resourceId)().describe("Site ID"),
|
|
180
182
|
...networkFields,
|
|
181
183
|
name: zod_1.z.string().describe("Network name"),
|
|
182
184
|
management: zod_1.z
|
|
@@ -194,12 +196,17 @@ function registerNetworkTools(server, client, readOnly = false) {
|
|
|
194
196
|
.optional()
|
|
195
197
|
.describe("Preview this action without executing it"),
|
|
196
198
|
},
|
|
197
|
-
outputSchema: output_schemas_js_1.networkOutputSchema,
|
|
199
|
+
outputSchema: (0, output_schemas_js_1.writeResult)(output_schemas_js_1.networkOutputSchema),
|
|
198
200
|
annotations: safety_js_1.WRITE_NOT_IDEMPOTENT,
|
|
199
201
|
}, async ({ siteId, dryRun, ...fields }) => {
|
|
200
202
|
try {
|
|
201
203
|
const body = (0, full_put_js_1.definedEntries)(fields);
|
|
202
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
|
+
}
|
|
203
210
|
if (dryRun)
|
|
204
211
|
return (0, safety_js_1.formatDryRun)("POST", `/sites/${siteId}/networks`, body);
|
|
205
212
|
const data = await client.post(`/sites/${siteId}/networks`, body);
|
|
@@ -212,28 +219,37 @@ function registerNetworkTools(server, client, readOnly = false) {
|
|
|
212
219
|
server.registerTool("unifi_update_network", {
|
|
213
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.`,
|
|
214
221
|
inputSchema: {
|
|
215
|
-
siteId:
|
|
216
|
-
networkId:
|
|
222
|
+
siteId: (0, params_js_1.resourceId)().describe("Site ID"),
|
|
223
|
+
networkId: (0, params_js_1.resourceId)().describe("Network ID"),
|
|
217
224
|
...networkFields,
|
|
218
225
|
dryRun: zod_1.z
|
|
219
226
|
.boolean()
|
|
220
227
|
.optional()
|
|
221
228
|
.describe("Preview the merged PUT body without executing it (still reads the current network)"),
|
|
222
229
|
},
|
|
223
|
-
outputSchema: output_schemas_js_1.networkOutputSchema,
|
|
230
|
+
outputSchema: (0, output_schemas_js_1.writeResult)(output_schemas_js_1.networkOutputSchema),
|
|
224
231
|
annotations: safety_js_1.WRITE,
|
|
225
232
|
}, async ({ siteId, networkId, dryRun, ...fields }) => {
|
|
226
233
|
try {
|
|
227
234
|
const path = `/sites/${siteId}/networks/${networkId}`;
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
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
|
+
});
|
|
237
253
|
}
|
|
238
254
|
catch (err) {
|
|
239
255
|
return (0, responses_js_1.formatError)(err);
|
|
@@ -242,17 +258,14 @@ function registerNetworkTools(server, client, readOnly = false) {
|
|
|
242
258
|
server.registerTool("unifi_delete_network", {
|
|
243
259
|
description: "DESTRUCTIVE: Delete a network — all clients on this network will be disconnected",
|
|
244
260
|
inputSchema: {
|
|
245
|
-
siteId:
|
|
246
|
-
networkId:
|
|
261
|
+
siteId: (0, params_js_1.resourceId)().describe("Site ID"),
|
|
262
|
+
networkId: (0, params_js_1.resourceId)().describe("Network ID"),
|
|
247
263
|
force: zod_1.z
|
|
248
264
|
.boolean()
|
|
249
265
|
.optional()
|
|
250
266
|
.default(false)
|
|
251
267
|
.describe("Force delete (default: false)"),
|
|
252
|
-
confirm:
|
|
253
|
-
.boolean()
|
|
254
|
-
.optional()
|
|
255
|
-
.describe("Must be true to execute this destructive action"),
|
|
268
|
+
confirm: (0, safety_js_1.confirmDestructive)(),
|
|
256
269
|
dryRun: zod_1.z
|
|
257
270
|
.boolean()
|
|
258
271
|
.optional()
|
|
@@ -260,7 +273,7 @@ function registerNetworkTools(server, client, readOnly = false) {
|
|
|
260
273
|
},
|
|
261
274
|
annotations: safety_js_1.DESTRUCTIVE,
|
|
262
275
|
}, async ({ siteId, networkId, force, confirm, dryRun }) => {
|
|
263
|
-
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");
|
|
264
277
|
if (guard)
|
|
265
278
|
return guard;
|
|
266
279
|
try {
|
package/dist/tools/sites.js
CHANGED
|
@@ -15,14 +15,14 @@ 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()
|
package/dist/tools/supporting.js
CHANGED
|
@@ -6,24 +6,25 @@ const responses_js_1 = require("../utils/responses.js");
|
|
|
6
6
|
const query_js_1 = require("../utils/query.js");
|
|
7
7
|
const safety_js_1 = require("../utils/safety.js");
|
|
8
8
|
const output_schemas_js_1 = require("../utils/output-schemas.js");
|
|
9
|
+
const params_js_1 = require("../utils/params.js");
|
|
9
10
|
function registerSupportingTools(server, client) {
|
|
10
11
|
server.registerTool("unifi_list_wans", {
|
|
11
12
|
description: "List WAN interface definitions at a site. Returns: id, name only (verified against 11.0.83 — the Integration API exposes no live link status or throughput rates here). Use for: WAN inventory, multi-WAN topology.",
|
|
12
13
|
inputSchema: {
|
|
13
|
-
siteId:
|
|
14
|
+
siteId: (0, params_js_1.resourceId)().describe("Site ID"),
|
|
14
15
|
offset: zod_1.z
|
|
15
16
|
.number()
|
|
16
17
|
.int()
|
|
17
18
|
.nonnegative()
|
|
18
19
|
.optional()
|
|
19
|
-
.describe("Number of records to skip
|
|
20
|
+
.describe("Number of records to skip"),
|
|
20
21
|
limit: zod_1.z
|
|
21
22
|
.number()
|
|
22
23
|
.int()
|
|
23
24
|
.min(0)
|
|
24
25
|
.max(200)
|
|
25
26
|
.optional()
|
|
26
|
-
.describe("Number of records to return (default: 25
|
|
27
|
+
.describe("Number of records to return (API default: 25)"),
|
|
27
28
|
},
|
|
28
29
|
outputSchema: output_schemas_js_1.listWansOutputSchema,
|
|
29
30
|
annotations: safety_js_1.READ_ONLY,
|
|
@@ -40,20 +41,20 @@ function registerSupportingTools(server, client) {
|
|
|
40
41
|
server.registerTool("unifi_list_vpn_tunnels", {
|
|
41
42
|
description: "List site-to-site VPN tunnels (IPsec, WireGuard, OpenVPN site-to-site) at a site. Returns: tunnel definitions per row (per-row schema not rendered in 11.0.83 docs and no tunnels to sample live; the older 10.4.57 spec lists id, type (IPSEC/OPENVPN/WIREGUARD), name, metadata.origin/source — call to inspect). For roaming client VPN servers, see unifi_list_vpn_servers.",
|
|
42
43
|
inputSchema: {
|
|
43
|
-
siteId:
|
|
44
|
+
siteId: (0, params_js_1.resourceId)().describe("Site ID"),
|
|
44
45
|
offset: zod_1.z
|
|
45
46
|
.number()
|
|
46
47
|
.int()
|
|
47
48
|
.nonnegative()
|
|
48
49
|
.optional()
|
|
49
|
-
.describe("Number of records to skip
|
|
50
|
+
.describe("Number of records to skip"),
|
|
50
51
|
limit: zod_1.z
|
|
51
52
|
.number()
|
|
52
53
|
.int()
|
|
53
54
|
.min(0)
|
|
54
55
|
.max(200)
|
|
55
56
|
.optional()
|
|
56
|
-
.describe("Number of records to return (default: 25
|
|
57
|
+
.describe("Number of records to return (API default: 25)"),
|
|
57
58
|
filter: zod_1.z
|
|
58
59
|
.string()
|
|
59
60
|
.optional()
|
|
@@ -74,20 +75,20 @@ function registerSupportingTools(server, client) {
|
|
|
74
75
|
server.registerTool("unifi_list_vpn_servers", {
|
|
75
76
|
description: "List VPN servers (roaming/client-access VPNs) at a site. Returns: id, type (L2TP, OPENVPN, PPTP, UID, WIREGUARD), name, enabled, metadata.origin.",
|
|
76
77
|
inputSchema: {
|
|
77
|
-
siteId:
|
|
78
|
+
siteId: (0, params_js_1.resourceId)().describe("Site ID"),
|
|
78
79
|
offset: zod_1.z
|
|
79
80
|
.number()
|
|
80
81
|
.int()
|
|
81
82
|
.nonnegative()
|
|
82
83
|
.optional()
|
|
83
|
-
.describe("Number of records to skip
|
|
84
|
+
.describe("Number of records to skip"),
|
|
84
85
|
limit: zod_1.z
|
|
85
86
|
.number()
|
|
86
87
|
.int()
|
|
87
88
|
.min(0)
|
|
88
89
|
.max(200)
|
|
89
90
|
.optional()
|
|
90
|
-
.describe("Number of records to return (default: 25
|
|
91
|
+
.describe("Number of records to return (API default: 25)"),
|
|
91
92
|
filter: zod_1.z
|
|
92
93
|
.string()
|
|
93
94
|
.optional()
|
|
@@ -108,20 +109,20 @@ function registerSupportingTools(server, client) {
|
|
|
108
109
|
server.registerTool("unifi_list_radius_profiles", {
|
|
109
110
|
description: "List RADIUS profiles (auth/accounting server configurations referenced by WiFi WPA-Enterprise, switch 802.1X port auth, VPN). Returns: id, name, metadata (origin, configurable).",
|
|
110
111
|
inputSchema: {
|
|
111
|
-
siteId:
|
|
112
|
+
siteId: (0, params_js_1.resourceId)().describe("Site ID"),
|
|
112
113
|
offset: zod_1.z
|
|
113
114
|
.number()
|
|
114
115
|
.int()
|
|
115
116
|
.nonnegative()
|
|
116
117
|
.optional()
|
|
117
|
-
.describe("Number of records to skip
|
|
118
|
+
.describe("Number of records to skip"),
|
|
118
119
|
limit: zod_1.z
|
|
119
120
|
.number()
|
|
120
121
|
.int()
|
|
121
122
|
.min(0)
|
|
122
123
|
.max(200)
|
|
123
124
|
.optional()
|
|
124
|
-
.describe("Number of records to return (default: 25
|
|
125
|
+
.describe("Number of records to return (API default: 25)"),
|
|
125
126
|
filter: zod_1.z
|
|
126
127
|
.string()
|
|
127
128
|
.optional()
|
|
@@ -142,20 +143,20 @@ function registerSupportingTools(server, client) {
|
|
|
142
143
|
server.registerTool("unifi_list_device_tags", {
|
|
143
144
|
description: "List device tags at a site. Tags group APs/switches for selective WiFi broadcast (via broadcastingDeviceFilter on a WiFi network). Returns: id, name, deviceIds[], metadata.",
|
|
144
145
|
inputSchema: {
|
|
145
|
-
siteId:
|
|
146
|
+
siteId: (0, params_js_1.resourceId)().describe("Site ID"),
|
|
146
147
|
offset: zod_1.z
|
|
147
148
|
.number()
|
|
148
149
|
.int()
|
|
149
150
|
.nonnegative()
|
|
150
151
|
.optional()
|
|
151
|
-
.describe("Number of records to skip
|
|
152
|
+
.describe("Number of records to skip"),
|
|
152
153
|
limit: zod_1.z
|
|
153
154
|
.number()
|
|
154
155
|
.int()
|
|
155
156
|
.min(0)
|
|
156
157
|
.max(200)
|
|
157
158
|
.optional()
|
|
158
|
-
.describe("Number of records to return (default: 25
|
|
159
|
+
.describe("Number of records to return (API default: 25)"),
|
|
159
160
|
filter: zod_1.z
|
|
160
161
|
.string()
|
|
161
162
|
.optional()
|
|
@@ -181,14 +182,14 @@ function registerSupportingTools(server, client) {
|
|
|
181
182
|
.int()
|
|
182
183
|
.nonnegative()
|
|
183
184
|
.optional()
|
|
184
|
-
.describe("Number of records to skip
|
|
185
|
+
.describe("Number of records to skip"),
|
|
185
186
|
limit: zod_1.z
|
|
186
187
|
.number()
|
|
187
188
|
.int()
|
|
188
189
|
.min(0)
|
|
189
190
|
.max(200)
|
|
190
191
|
.optional()
|
|
191
|
-
.describe("Number of records to return (default: 25
|
|
192
|
+
.describe("Number of records to return (API default: 25)"),
|
|
192
193
|
filter: zod_1.z
|
|
193
194
|
.string()
|
|
194
195
|
.optional()
|
|
@@ -214,14 +215,14 @@ function registerSupportingTools(server, client) {
|
|
|
214
215
|
.int()
|
|
215
216
|
.nonnegative()
|
|
216
217
|
.optional()
|
|
217
|
-
.describe("Number of records to skip
|
|
218
|
+
.describe("Number of records to skip"),
|
|
218
219
|
limit: zod_1.z
|
|
219
220
|
.number()
|
|
220
221
|
.int()
|
|
221
222
|
.min(0)
|
|
222
223
|
.max(200)
|
|
223
224
|
.optional()
|
|
224
|
-
.describe("Number of records to return (default: 25
|
|
225
|
+
.describe("Number of records to return (API default: 25)"),
|
|
225
226
|
filter: zod_1.z
|
|
226
227
|
.string()
|
|
227
228
|
.optional()
|
|
@@ -247,14 +248,14 @@ function registerSupportingTools(server, client) {
|
|
|
247
248
|
.int()
|
|
248
249
|
.nonnegative()
|
|
249
250
|
.optional()
|
|
250
|
-
.describe("Number of records to skip
|
|
251
|
+
.describe("Number of records to skip"),
|
|
251
252
|
limit: zod_1.z
|
|
252
253
|
.number()
|
|
253
254
|
.int()
|
|
254
255
|
.min(0)
|
|
255
256
|
.max(200)
|
|
256
257
|
.optional()
|
|
257
|
-
.describe("Number of records to return (default: 25
|
|
258
|
+
.describe("Number of records to return (API default: 25)"),
|
|
258
259
|
filter: zod_1.z
|
|
259
260
|
.string()
|
|
260
261
|
.optional()
|