@owine/unifi-network-mcp 0.9.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/README.md ADDED
@@ -0,0 +1,241 @@
1
+ # UniFi Network MCP Server
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 67 tools for managing sites, devices, clients, networks, WiFi, firewalls, ACLs, DNS policies, hotspot vouchers, VPNs, and more.
4
+
5
+ ## Prerequisites
6
+
7
+ - Node.js 20+
8
+ - A UniFi Network console with the Integration API enabled
9
+ - An API key generated from your UniFi Network console
10
+
11
+ ## Setup
12
+
13
+ ### Quick start (npx)
14
+
15
+ Add to Claude Code with a single command — no clone or build needed:
16
+
17
+ ```bash
18
+ claude mcp add-json unifi-network '{
19
+ "command": "npx",
20
+ "args": ["-y", "@owine/unifi-network-mcp"],
21
+ "env": {
22
+ "UNIFI_NETWORK_HOST": "192.168.1.1",
23
+ "UNIFI_NETWORK_API_KEY": "your-api-key",
24
+ "UNIFI_NETWORK_VERIFY_SSL": "false"
25
+ }
26
+ }' -s user
27
+ ```
28
+
29
+ Use `-s user` for global availability across all projects, or `-s project` for the current project only.
30
+
31
+ ### From source
32
+
33
+ If you prefer to build locally:
34
+
35
+ ```bash
36
+ git clone https://github.com/owine/unifi-network-mcp.git
37
+ cd unifi-network-mcp
38
+ npm install
39
+ npm run build
40
+ ```
41
+
42
+ Then add to Claude Code:
43
+
44
+ ```bash
45
+ claude mcp add-json unifi-network '{
46
+ "command": "node",
47
+ "args": ["/path/to/unifi-network-mcp/dist/index.js"],
48
+ "env": {
49
+ "UNIFI_NETWORK_HOST": "192.168.1.1",
50
+ "UNIFI_NETWORK_API_KEY": "your-api-key",
51
+ "UNIFI_NETWORK_VERIFY_SSL": "false"
52
+ }
53
+ }' -s user
54
+ ```
55
+
56
+ ### Environment Variables
57
+
58
+ | Variable | Required | Default | Description |
59
+ |---|---|---|---|
60
+ | `UNIFI_NETWORK_HOST` | Yes | — | IP or hostname of your UniFi Network console |
61
+ | `UNIFI_NETWORK_API_KEY` | Yes | — | API key from Network integration settings |
62
+ | `UNIFI_NETWORK_VERIFY_SSL` | No | `true` | Set to `false` to skip TLS certificate verification (needed for self-signed certs) |
63
+ | `UNIFI_NETWORK_READ_ONLY` | No | `false` | Set to `true` to disable all write/mutating tools (monitoring-only mode) |
64
+
65
+ ### Manual Configuration
66
+
67
+ Alternatively, add to your `~/.claude.json` under the top-level `"mcpServers"` key:
68
+
69
+ ```json
70
+ {
71
+ "mcpServers": {
72
+ "unifi-network": {
73
+ "command": "npx",
74
+ "args": ["-y", "@owine/unifi-network-mcp"],
75
+ "env": {
76
+ "UNIFI_NETWORK_HOST": "192.168.1.1",
77
+ "UNIFI_NETWORK_API_KEY": "your-api-key",
78
+ "UNIFI_NETWORK_VERIFY_SSL": "false"
79
+ }
80
+ }
81
+ }
82
+ }
83
+ ```
84
+
85
+ ## Safety Features
86
+
87
+ This server provides layered safety controls for responsible operation:
88
+
89
+ - **Tool annotations** — Every tool declares `readOnlyHint`, `destructiveHint`, and `idempotentHint` so MCP clients (like Claude Code) can make informed confirmation decisions
90
+ - **Read-only mode** — Set `UNIFI_NETWORK_READ_ONLY=true` to completely hide all write/mutating tools. Only read operations (list, get) are registered. Ideal for monitoring-only deployments
91
+ - **Destructive tool warnings** — Tools that delete or irreversibly modify resources have descriptions prefixed with `DESTRUCTIVE:` to clearly signal risk
92
+ - **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
93
+ - **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
94
+
95
+ ## Tools (67 total)
96
+
97
+ ### System (1)
98
+ | Tool | Description |
99
+ |---|---|
100
+ | `unifi_get_info` | Get application information including version and whether it's a UniFi OS Console |
101
+
102
+ ### Sites (1)
103
+ | Tool | Description |
104
+ |---|---|
105
+ | `unifi_list_sites` | List all sites available to the API key |
106
+
107
+ ### Devices (8)
108
+ | Tool | Description |
109
+ |---|---|
110
+ | `unifi_list_devices` | List all adopted devices at a site |
111
+ | `unifi_get_device` | Get a specific device by ID |
112
+ | `unifi_get_device_statistics` | Get latest statistics for a device |
113
+ | `unifi_list_pending_devices` | List devices pending adoption (global) |
114
+ | `unifi_adopt_device` | Adopt a pending device |
115
+ | `unifi_remove_device` | **DESTRUCTIVE:** Remove (unadopt) a device — may factory reset |
116
+ | `unifi_restart_device` | Restart a device |
117
+ | `unifi_power_cycle_port` | Power cycle a specific port (PoE restart) |
118
+
119
+ ### Clients (4)
120
+ | Tool | Description |
121
+ |---|---|
122
+ | `unifi_list_clients` | List all connected clients (wired, wireless, VPN) at a site |
123
+ | `unifi_get_client` | Get a specific client by ID |
124
+ | `unifi_authorize_guest` | Authorize a guest client on a hotspot network |
125
+ | `unifi_unauthorize_guest` | Unauthorize a guest client |
126
+
127
+ ### Networks (6)
128
+ | Tool | Description |
129
+ |---|---|
130
+ | `unifi_list_networks` | List all networks at a site |
131
+ | `unifi_get_network` | Get a specific network by ID |
132
+ | `unifi_get_network_references` | Get references to a network (WiFi, firewall zones, etc.) |
133
+ | `unifi_create_network` | Create a new network |
134
+ | `unifi_update_network` | Update an existing network |
135
+ | `unifi_delete_network` | **DESTRUCTIVE:** Delete a network — disconnects all clients |
136
+
137
+ ### WiFi (5)
138
+ | Tool | Description |
139
+ |---|---|
140
+ | `unifi_list_wifi` | List all WiFi broadcasts (SSIDs) at a site |
141
+ | `unifi_get_wifi` | Get a specific WiFi network by ID |
142
+ | `unifi_create_wifi` | Create a new WiFi network (SSID) |
143
+ | `unifi_update_wifi` | Update an existing WiFi network |
144
+ | `unifi_delete_wifi` | **DESTRUCTIVE:** Delete a WiFi network — disconnects all clients |
145
+
146
+ ### Hotspot Vouchers (5)
147
+ | Tool | Description |
148
+ |---|---|
149
+ | `unifi_list_vouchers` | List all hotspot vouchers at a site |
150
+ | `unifi_get_voucher` | Get a specific hotspot voucher by ID |
151
+ | `unifi_create_voucher` | Create hotspot vouchers |
152
+ | `unifi_delete_voucher` | **DESTRUCTIVE:** Delete a hotspot voucher |
153
+ | `unifi_bulk_delete_vouchers` | **DESTRUCTIVE:** Bulk delete vouchers matching a filter |
154
+
155
+ ### Firewall Zones & Policies (12)
156
+ | Tool | Description |
157
+ |---|---|
158
+ | `unifi_list_firewall_zones` | List all firewall zones at a site |
159
+ | `unifi_get_firewall_zone` | Get a specific firewall zone by ID |
160
+ | `unifi_create_firewall_zone` | Create a new custom firewall zone |
161
+ | `unifi_update_firewall_zone` | Update a firewall zone |
162
+ | `unifi_delete_firewall_zone` | **DESTRUCTIVE:** Delete a custom firewall zone |
163
+ | `unifi_list_firewall_policies` | List all firewall policies at a site |
164
+ | `unifi_get_firewall_policy` | Get a specific firewall policy by ID |
165
+ | `unifi_create_firewall_policy` | Create a new firewall policy |
166
+ | `unifi_update_firewall_policy` | Update a firewall policy |
167
+ | `unifi_delete_firewall_policy` | **DESTRUCTIVE:** Delete a firewall policy |
168
+ | `unifi_get_firewall_policy_ordering` | Get user-defined firewall policy ordering for a zone pair |
169
+ | `unifi_reorder_firewall_policies` | Reorder user-defined firewall policies for a zone pair |
170
+
171
+ ### ACL Rules (7)
172
+ | Tool | Description |
173
+ |---|---|
174
+ | `unifi_list_acl_rules` | List all ACL (firewall) rules at a site |
175
+ | `unifi_get_acl_rule` | Get a specific ACL rule by ID |
176
+ | `unifi_get_acl_rule_ordering` | Get user-defined ACL rule ordering |
177
+ | `unifi_create_acl_rule` | Create a new ACL rule |
178
+ | `unifi_update_acl_rule` | Update an ACL rule |
179
+ | `unifi_delete_acl_rule` | **DESTRUCTIVE:** Delete an ACL rule |
180
+ | `unifi_reorder_acl_rules` | Reorder user-defined ACL rules |
181
+
182
+ ### DNS Policies (5)
183
+ | Tool | Description |
184
+ |---|---|
185
+ | `unifi_list_dns_policies` | List all DNS policies at a site |
186
+ | `unifi_get_dns_policy` | Get a specific DNS policy by ID |
187
+ | `unifi_create_dns_policy` | Create a new DNS policy |
188
+ | `unifi_update_dns_policy` | Update a DNS policy |
189
+ | `unifi_delete_dns_policy` | **DESTRUCTIVE:** Delete a DNS policy |
190
+
191
+ ### Traffic Matching (5)
192
+ | Tool | Description |
193
+ |---|---|
194
+ | `unifi_list_traffic_matching_lists` | List all traffic matching lists (port groups, IP groups) |
195
+ | `unifi_get_traffic_matching_list` | Get a specific traffic matching list by ID |
196
+ | `unifi_create_traffic_matching_list` | Create a new traffic matching list |
197
+ | `unifi_update_traffic_matching_list` | Update a traffic matching list |
198
+ | `unifi_delete_traffic_matching_list` | **DESTRUCTIVE:** Delete a traffic matching list |
199
+
200
+ ### Supporting (8)
201
+ | Tool | Description |
202
+ |---|---|
203
+ | `unifi_list_wans` | List all WAN interfaces at a site |
204
+ | `unifi_list_vpn_tunnels` | List all site-to-site VPN tunnels at a site |
205
+ | `unifi_list_vpn_servers` | List all VPN servers at a site |
206
+ | `unifi_list_radius_profiles` | List all RADIUS profiles at a site |
207
+ | `unifi_list_device_tags` | List all device tags at a site |
208
+ | `unifi_list_dpi_categories` | List all DPI categories for traffic identification |
209
+ | `unifi_list_dpi_applications` | List all DPI applications for traffic identification |
210
+ | `unifi_list_countries` | List all countries/regions for geo-based rules |
211
+
212
+ ## Development
213
+
214
+ ```bash
215
+ npm run build # Compile TypeScript
216
+ npm start # Run the server
217
+ npm run typecheck # Type-check without emitting
218
+ npm run lint # ESLint
219
+ npm test # Run all tests (vitest)
220
+ ```
221
+
222
+ ### Commit conventions
223
+
224
+ This project uses [conventional commits](https://www.conventionalcommits.org/) and [release-please](https://github.com/googleapis/release-please) for automated releases:
225
+
226
+ - `feat: ...` — new feature (minor version bump)
227
+ - `fix: ...` — bug fix (patch version bump)
228
+ - `feat!: ...` or `BREAKING CHANGE:` footer — breaking change (major version bump)
229
+ - `chore:`, `docs:`, `ci:`, etc. — no version bump
230
+
231
+ On push to `main`, release-please opens a Release PR that bumps the version and updates `CHANGELOG.md`. Merging that PR publishes to npm automatically.
232
+
233
+ To override the version number, add `Release-As: x.x.x` in the commit body:
234
+
235
+ ```bash
236
+ git commit --allow-empty -m "chore: release 2.0.0" -m "Release-As: 2.0.0"
237
+ ```
238
+
239
+ ## License
240
+
241
+ MIT
@@ -0,0 +1,12 @@
1
+ import { Config } from "./config.js";
2
+ export declare class NetworkClient {
3
+ private baseUrl;
4
+ private headers;
5
+ constructor(config: Config);
6
+ private request;
7
+ get(path: string): Promise<unknown>;
8
+ post(path: string, body?: unknown): Promise<unknown>;
9
+ put(path: string, body: unknown): Promise<unknown>;
10
+ patch(path: string, body: unknown): Promise<unknown>;
11
+ delete(path: string): Promise<unknown>;
12
+ }
package/dist/client.js ADDED
@@ -0,0 +1,53 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.NetworkClient = void 0;
4
+ class NetworkClient {
5
+ baseUrl;
6
+ headers;
7
+ constructor(config) {
8
+ this.baseUrl = `https://${config.host}/proxy/network/integration/v1`;
9
+ this.headers = {
10
+ "X-API-KEY": config.apiKey,
11
+ "Content-Type": "application/json",
12
+ };
13
+ if (!config.verifySsl) {
14
+ process.env.NODE_TLS_REJECT_UNAUTHORIZED = "0";
15
+ }
16
+ }
17
+ async request(method, path, body) {
18
+ const url = `${this.baseUrl}${path}`;
19
+ const options = {
20
+ method,
21
+ headers: this.headers,
22
+ };
23
+ if (body !== undefined) {
24
+ options.body = JSON.stringify(body);
25
+ }
26
+ const response = await fetch(url, options);
27
+ if (!response.ok) {
28
+ const text = await response.text();
29
+ throw new Error(`HTTP ${response.status}: ${text}`);
30
+ }
31
+ const contentType = response.headers.get("content-type") ?? "";
32
+ if (contentType.includes("application/json")) {
33
+ return response.json();
34
+ }
35
+ return response.text();
36
+ }
37
+ async get(path) {
38
+ return this.request("GET", path);
39
+ }
40
+ async post(path, body) {
41
+ return this.request("POST", path, body);
42
+ }
43
+ async put(path, body) {
44
+ return this.request("PUT", path, body);
45
+ }
46
+ async patch(path, body) {
47
+ return this.request("PATCH", path, body);
48
+ }
49
+ async delete(path) {
50
+ return this.request("DELETE", path);
51
+ }
52
+ }
53
+ exports.NetworkClient = NetworkClient;
@@ -0,0 +1,10 @@
1
+ import { z } from "zod";
2
+ declare const configSchema: z.ZodObject<{
3
+ host: z.ZodString;
4
+ apiKey: z.ZodString;
5
+ verifySsl: z.ZodDefault<z.ZodBoolean>;
6
+ readOnly: z.ZodDefault<z.ZodBoolean>;
7
+ }, z.core.$strip>;
8
+ export type Config = z.infer<typeof configSchema>;
9
+ export declare function loadConfig(): Config;
10
+ export {};
package/dist/config.js ADDED
@@ -0,0 +1,29 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.loadConfig = loadConfig;
4
+ const zod_1 = require("zod");
5
+ const configSchema = zod_1.z.object({
6
+ host: zod_1.z.string().min(1).describe("UniFi Network host (IP or hostname)"),
7
+ apiKey: zod_1.z.string().min(1).describe("UniFi Network API key"),
8
+ verifySsl: zod_1.z
9
+ .boolean()
10
+ .default(true)
11
+ .describe("Verify SSL certificates"),
12
+ readOnly: zod_1.z
13
+ .boolean()
14
+ .default(false)
15
+ .describe("When true, only read-only tools are registered"),
16
+ });
17
+ function loadConfig() {
18
+ const result = configSchema.safeParse({
19
+ host: process.env.UNIFI_NETWORK_HOST,
20
+ apiKey: process.env.UNIFI_NETWORK_API_KEY,
21
+ verifySsl: process.env.UNIFI_NETWORK_VERIFY_SSL?.toLowerCase() !== "false",
22
+ readOnly: process.env.UNIFI_NETWORK_READ_ONLY?.toLowerCase() === "true",
23
+ });
24
+ if (!result.success) {
25
+ console.error("Configuration error:", result.error.format());
26
+ process.exit(1);
27
+ }
28
+ return result.data;
29
+ }
@@ -0,0 +1,2 @@
1
+ #!/usr/bin/env node
2
+ export {};
package/dist/index.js ADDED
@@ -0,0 +1,29 @@
1
+ #!/usr/bin/env node
2
+ "use strict";
3
+ Object.defineProperty(exports, "__esModule", { value: true });
4
+ const mcp_js_1 = require("@modelcontextprotocol/sdk/server/mcp.js");
5
+ const stdio_js_1 = require("@modelcontextprotocol/sdk/server/stdio.js");
6
+ const config_js_1 = require("./config.js");
7
+ const client_js_1 = require("./client.js");
8
+ const index_js_1 = require("./tools/index.js");
9
+ async function main() {
10
+ const config = (0, config_js_1.loadConfig)();
11
+ const client = new client_js_1.NetworkClient(config);
12
+ const server = new mcp_js_1.McpServer({
13
+ name: "unifi-network",
14
+ version: "1.0.0",
15
+ });
16
+ (0, index_js_1.registerAllTools)(server, client, config.readOnly);
17
+ if (config.readOnly) {
18
+ console.error("UniFi Network MCP server running on stdio (READ-ONLY mode)");
19
+ }
20
+ else {
21
+ console.error("UniFi Network MCP server running on stdio");
22
+ }
23
+ const transport = new stdio_js_1.StdioServerTransport();
24
+ await server.connect(transport);
25
+ }
26
+ main().catch((err) => {
27
+ console.error("Fatal error:", err);
28
+ process.exit(1);
29
+ });
@@ -0,0 +1,3 @@
1
+ import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
2
+ import { NetworkClient } from "../client.js";
3
+ export declare function registerAclTools(server: McpServer, client: NetworkClient, readOnly?: boolean): void;
@@ -0,0 +1,164 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.registerAclTools = registerAclTools;
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 registerAclTools(server, client, readOnly = false) {
9
+ server.tool("unifi_list_acl_rules", "List all ACL (firewall) rules at a site", {
10
+ siteId: zod_1.z.string().describe("Site ID"),
11
+ offset: zod_1.z
12
+ .number()
13
+ .int()
14
+ .nonnegative()
15
+ .optional()
16
+ .describe("Number of records to skip (default: 0)"),
17
+ limit: zod_1.z
18
+ .number()
19
+ .int()
20
+ .min(1)
21
+ .max(200)
22
+ .optional()
23
+ .describe("Number of records to return (default: 25, max: 200)"),
24
+ filter: zod_1.z
25
+ .string()
26
+ .optional()
27
+ .describe("Filter expression"),
28
+ }, safety_js_1.READ_ONLY, async ({ siteId, offset, limit, filter }) => {
29
+ try {
30
+ const query = (0, query_js_1.buildQuery)({ offset, limit, filter });
31
+ const data = await client.get(`/sites/${siteId}/acl-rules${query}`);
32
+ return (0, responses_js_1.formatSuccess)(data);
33
+ }
34
+ catch (err) {
35
+ return (0, responses_js_1.formatError)(err);
36
+ }
37
+ });
38
+ server.tool("unifi_get_acl_rule", "Get a specific ACL rule by ID", {
39
+ siteId: zod_1.z.string().describe("Site ID"),
40
+ aclRuleId: zod_1.z.string().describe("ACL rule ID"),
41
+ }, safety_js_1.READ_ONLY, async ({ siteId, aclRuleId }) => {
42
+ try {
43
+ const data = await client.get(`/sites/${siteId}/acl-rules/${aclRuleId}`);
44
+ return (0, responses_js_1.formatSuccess)(data);
45
+ }
46
+ catch (err) {
47
+ return (0, responses_js_1.formatError)(err);
48
+ }
49
+ });
50
+ server.tool("unifi_get_acl_rule_ordering", "Get user-defined ACL rule ordering", {
51
+ siteId: zod_1.z.string().describe("Site ID"),
52
+ }, safety_js_1.READ_ONLY, async ({ siteId }) => {
53
+ try {
54
+ const data = await client.get(`/sites/${siteId}/acl-rules/ordering`);
55
+ return (0, responses_js_1.formatSuccess)(data);
56
+ }
57
+ catch (err) {
58
+ return (0, responses_js_1.formatError)(err);
59
+ }
60
+ });
61
+ if (readOnly)
62
+ return;
63
+ server.tool("unifi_create_acl_rule", "Create a new ACL (firewall) rule", {
64
+ siteId: zod_1.z.string().describe("Site ID"),
65
+ type: zod_1.z.enum(["IPV4", "MAC"]).describe("Rule type"),
66
+ name: zod_1.z.string().describe("Rule name"),
67
+ enabled: zod_1.z.boolean().describe("Enable the rule"),
68
+ action: zod_1.z.enum(["ALLOW", "BLOCK"]).describe("Rule action"),
69
+ description: zod_1.z
70
+ .string()
71
+ .optional()
72
+ .describe("Rule description"),
73
+ protocolFilter: zod_1.z
74
+ .array(zod_1.z.string())
75
+ .optional()
76
+ .describe("Protocols: TCP, UDP"),
77
+ dryRun: zod_1.z
78
+ .boolean()
79
+ .optional()
80
+ .describe("Preview this action without executing it"),
81
+ }, safety_js_1.WRITE_NOT_IDEMPOTENT, async ({ siteId, type, name, enabled, action, description, protocolFilter, dryRun }) => {
82
+ try {
83
+ const body = { type, name, enabled, action };
84
+ if (description !== undefined)
85
+ body.description = description;
86
+ if (protocolFilter !== undefined)
87
+ body.protocolFilter = protocolFilter;
88
+ if (dryRun)
89
+ return (0, safety_js_1.formatDryRun)("POST", `/sites/${siteId}/acl-rules`, body);
90
+ const data = await client.post(`/sites/${siteId}/acl-rules`, body);
91
+ return (0, responses_js_1.formatSuccess)(data);
92
+ }
93
+ catch (err) {
94
+ return (0, responses_js_1.formatError)(err);
95
+ }
96
+ });
97
+ server.tool("unifi_update_acl_rule", "Update an ACL rule", {
98
+ siteId: zod_1.z.string().describe("Site ID"),
99
+ aclRuleId: zod_1.z.string().describe("ACL rule ID"),
100
+ rule: zod_1.z
101
+ .record(zod_1.z.string(), zod_1.z.unknown())
102
+ .describe("ACL rule configuration (JSON object)"),
103
+ dryRun: zod_1.z
104
+ .boolean()
105
+ .optional()
106
+ .describe("Preview this action without executing it"),
107
+ }, safety_js_1.WRITE, async ({ siteId, aclRuleId, rule, dryRun }) => {
108
+ try {
109
+ if (dryRun)
110
+ return (0, safety_js_1.formatDryRun)("PUT", `/sites/${siteId}/acl-rules/${aclRuleId}`, rule);
111
+ const data = await client.put(`/sites/${siteId}/acl-rules/${aclRuleId}`, rule);
112
+ return (0, responses_js_1.formatSuccess)(data);
113
+ }
114
+ catch (err) {
115
+ return (0, responses_js_1.formatError)(err);
116
+ }
117
+ });
118
+ server.tool("unifi_delete_acl_rule", "DESTRUCTIVE: Delete an ACL rule", {
119
+ siteId: zod_1.z.string().describe("Site ID"),
120
+ aclRuleId: zod_1.z.string().describe("ACL rule ID"),
121
+ confirm: zod_1.z
122
+ .boolean()
123
+ .optional()
124
+ .describe("Must be true to execute this destructive action"),
125
+ dryRun: zod_1.z
126
+ .boolean()
127
+ .optional()
128
+ .describe("Preview this action without executing it"),
129
+ }, safety_js_1.DESTRUCTIVE, async ({ siteId, aclRuleId, confirm, dryRun }) => {
130
+ const guard = (0, safety_js_1.requireConfirmation)(confirm, "This will delete the ACL rule");
131
+ if (guard)
132
+ return guard;
133
+ try {
134
+ if (dryRun)
135
+ return (0, safety_js_1.formatDryRun)("DELETE", `/sites/${siteId}/acl-rules/${aclRuleId}`, {});
136
+ const data = await client.delete(`/sites/${siteId}/acl-rules/${aclRuleId}`);
137
+ return (0, responses_js_1.formatSuccess)(data);
138
+ }
139
+ catch (err) {
140
+ return (0, responses_js_1.formatError)(err);
141
+ }
142
+ });
143
+ server.tool("unifi_reorder_acl_rules", "Reorder user-defined ACL rules", {
144
+ siteId: zod_1.z.string().describe("Site ID"),
145
+ orderedAclRuleIds: zod_1.z
146
+ .array(zod_1.z.string())
147
+ .describe("Ordered ACL rule IDs"),
148
+ dryRun: zod_1.z
149
+ .boolean()
150
+ .optional()
151
+ .describe("Preview this action without executing it"),
152
+ }, safety_js_1.WRITE, async ({ siteId, orderedAclRuleIds, dryRun }) => {
153
+ try {
154
+ const body = { orderedAclRuleIds };
155
+ if (dryRun)
156
+ return (0, safety_js_1.formatDryRun)("PUT", `/sites/${siteId}/acl-rules/ordering`, body);
157
+ const data = await client.put(`/sites/${siteId}/acl-rules/ordering`, body);
158
+ return (0, responses_js_1.formatSuccess)(data);
159
+ }
160
+ catch (err) {
161
+ return (0, responses_js_1.formatError)(err);
162
+ }
163
+ });
164
+ }
@@ -0,0 +1,3 @@
1
+ import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
2
+ import { NetworkClient } from "../client.js";
3
+ export declare function registerClientTools(server: McpServer, client: NetworkClient, readOnly?: boolean): void;
@@ -0,0 +1,119 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.registerClientTools = registerClientTools;
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 registerClientTools(server, client, readOnly = false) {
9
+ server.tool("unifi_list_clients", "List all connected clients (wired, wireless, VPN) at a site", {
10
+ siteId: zod_1.z.string().describe("Site ID"),
11
+ offset: zod_1.z
12
+ .number()
13
+ .int()
14
+ .nonnegative()
15
+ .optional()
16
+ .describe("Number of records to skip (default: 0)"),
17
+ limit: zod_1.z
18
+ .number()
19
+ .int()
20
+ .min(1)
21
+ .max(200)
22
+ .optional()
23
+ .describe("Number of records to return (default: 25, max: 200)"),
24
+ filter: zod_1.z.string().optional().describe("Filter expression"),
25
+ }, safety_js_1.READ_ONLY, async ({ siteId, offset, limit, filter }) => {
26
+ try {
27
+ const query = (0, query_js_1.buildQuery)({ offset, limit, filter });
28
+ const data = await client.get(`/sites/${siteId}/clients${query}`);
29
+ return (0, responses_js_1.formatSuccess)(data);
30
+ }
31
+ catch (err) {
32
+ return (0, responses_js_1.formatError)(err);
33
+ }
34
+ });
35
+ server.tool("unifi_get_client", "Get a specific client by ID", {
36
+ siteId: zod_1.z.string().describe("Site ID"),
37
+ clientId: zod_1.z.string().describe("Client ID"),
38
+ }, safety_js_1.READ_ONLY, async ({ siteId, clientId }) => {
39
+ try {
40
+ const data = await client.get(`/sites/${siteId}/clients/${clientId}`);
41
+ return (0, responses_js_1.formatSuccess)(data);
42
+ }
43
+ catch (err) {
44
+ return (0, responses_js_1.formatError)(err);
45
+ }
46
+ });
47
+ if (readOnly)
48
+ return;
49
+ server.tool("unifi_authorize_guest", "Authorize a guest client on a hotspot network", {
50
+ siteId: zod_1.z.string().describe("Site ID"),
51
+ clientId: zod_1.z.string().describe("Client ID"),
52
+ timeLimitMinutes: zod_1.z
53
+ .number()
54
+ .int()
55
+ .min(1)
56
+ .max(1000000)
57
+ .optional()
58
+ .describe("How long (in minutes) the guest will be authorized (1-1000000)"),
59
+ dataUsageLimitMBytes: zod_1.z
60
+ .number()
61
+ .int()
62
+ .min(1)
63
+ .max(1048576)
64
+ .optional()
65
+ .describe("Data usage limit in megabytes (1-1048576)"),
66
+ rxRateLimitKbps: zod_1.z
67
+ .number()
68
+ .int()
69
+ .min(2)
70
+ .max(100000)
71
+ .optional()
72
+ .describe("Download rate limit in kilobits per second (2-100000)"),
73
+ txRateLimitKbps: zod_1.z
74
+ .number()
75
+ .int()
76
+ .min(2)
77
+ .max(100000)
78
+ .optional()
79
+ .describe("Upload rate limit in kilobits per second (2-100000)"),
80
+ dryRun: zod_1.z.boolean().optional().describe("Preview this action without executing it"),
81
+ }, safety_js_1.WRITE_NOT_IDEMPOTENT, async ({ siteId, clientId, timeLimitMinutes, dataUsageLimitMBytes, rxRateLimitKbps, txRateLimitKbps, dryRun, }) => {
82
+ const body = {
83
+ action: "AUTHORIZE_GUEST_ACCESS",
84
+ };
85
+ if (timeLimitMinutes !== undefined)
86
+ body.timeLimitMinutes = timeLimitMinutes;
87
+ if (dataUsageLimitMBytes !== undefined)
88
+ body.dataUsageLimitMBytes = dataUsageLimitMBytes;
89
+ if (rxRateLimitKbps !== undefined)
90
+ body.rxRateLimitKbps = rxRateLimitKbps;
91
+ if (txRateLimitKbps !== undefined)
92
+ body.txRateLimitKbps = txRateLimitKbps;
93
+ if (dryRun)
94
+ return (0, safety_js_1.formatDryRun)("POST", `/sites/${siteId}/clients/${clientId}/actions`, body);
95
+ try {
96
+ const data = await client.post(`/sites/${siteId}/clients/${clientId}/actions`, body);
97
+ return (0, responses_js_1.formatSuccess)(data);
98
+ }
99
+ catch (err) {
100
+ return (0, responses_js_1.formatError)(err);
101
+ }
102
+ });
103
+ server.tool("unifi_unauthorize_guest", "Unauthorize a guest client", {
104
+ siteId: zod_1.z.string().describe("Site ID"),
105
+ clientId: zod_1.z.string().describe("Client ID"),
106
+ dryRun: zod_1.z.boolean().optional().describe("Preview this action without executing it"),
107
+ }, safety_js_1.WRITE, async ({ siteId, clientId, dryRun }) => {
108
+ const body = { action: "UNAUTHORIZE_GUEST_ACCESS" };
109
+ if (dryRun)
110
+ return (0, safety_js_1.formatDryRun)("POST", `/sites/${siteId}/clients/${clientId}/actions`, body);
111
+ try {
112
+ const data = await client.post(`/sites/${siteId}/clients/${clientId}/actions`, body);
113
+ return (0, responses_js_1.formatSuccess)(data);
114
+ }
115
+ catch (err) {
116
+ return (0, responses_js_1.formatError)(err);
117
+ }
118
+ });
119
+ }
@@ -0,0 +1,3 @@
1
+ import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
2
+ import { NetworkClient } from "../client.js";
3
+ export declare function registerDeviceTools(server: McpServer, client: NetworkClient, readOnly?: boolean): void;