@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.
@@ -0,0 +1,3 @@
1
+ import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
2
+ import { NetworkClient } from "../client.js";
3
+ export declare function registerTrafficMatchingTools(server: McpServer, client: NetworkClient, readOnly?: boolean): void;
@@ -0,0 +1,126 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.registerTrafficMatchingTools = registerTrafficMatchingTools;
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 registerTrafficMatchingTools(server, client, readOnly = false) {
9
+ server.tool("unifi_list_traffic_matching_lists", "List all traffic matching lists at a site (port groups, IP groups)", {
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}/traffic-matching-lists${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_traffic_matching_list", "Get a specific traffic matching list by ID", {
39
+ siteId: zod_1.z.string().describe("Site ID"),
40
+ trafficMatchingListId: zod_1.z.string().describe("Traffic matching list ID"),
41
+ }, safety_js_1.READ_ONLY, async ({ siteId, trafficMatchingListId }) => {
42
+ try {
43
+ const data = await client.get(`/sites/${siteId}/traffic-matching-lists/${trafficMatchingListId}`);
44
+ return (0, responses_js_1.formatSuccess)(data);
45
+ }
46
+ catch (err) {
47
+ return (0, responses_js_1.formatError)(err);
48
+ }
49
+ });
50
+ if (readOnly)
51
+ return;
52
+ server.tool("unifi_create_traffic_matching_list", "Create a new traffic matching list", {
53
+ siteId: zod_1.z.string().describe("Site ID"),
54
+ type: zod_1.z
55
+ .enum(["PORTS", "IPV4_ADDRESSES", "IPV6_ADDRESSES"])
56
+ .describe("List type"),
57
+ name: zod_1.z.string().describe("List name"),
58
+ items: zod_1.z
59
+ .array(zod_1.z.unknown())
60
+ .describe("List items (ports or IP addresses)"),
61
+ dryRun: zod_1.z
62
+ .boolean()
63
+ .optional()
64
+ .describe("Preview this action without executing it"),
65
+ }, safety_js_1.WRITE_NOT_IDEMPOTENT, async ({ siteId, type, name, items, dryRun }) => {
66
+ try {
67
+ const body = { type, name, items };
68
+ if (dryRun)
69
+ return (0, safety_js_1.formatDryRun)("POST", `/sites/${siteId}/traffic-matching-lists`, body);
70
+ const data = await client.post(`/sites/${siteId}/traffic-matching-lists`, body);
71
+ return (0, responses_js_1.formatSuccess)(data);
72
+ }
73
+ catch (err) {
74
+ return (0, responses_js_1.formatError)(err);
75
+ }
76
+ });
77
+ server.tool("unifi_update_traffic_matching_list", "Update a traffic matching list", {
78
+ siteId: zod_1.z.string().describe("Site ID"),
79
+ trafficMatchingListId: zod_1.z.string().describe("Traffic matching list ID"),
80
+ type: zod_1.z
81
+ .enum(["PORTS", "IPV4_ADDRESSES", "IPV6_ADDRESSES"])
82
+ .describe("List type"),
83
+ name: zod_1.z.string().describe("List name"),
84
+ items: zod_1.z.array(zod_1.z.unknown()).describe("List items"),
85
+ dryRun: zod_1.z
86
+ .boolean()
87
+ .optional()
88
+ .describe("Preview this action without executing it"),
89
+ }, safety_js_1.WRITE, async ({ siteId, trafficMatchingListId, type, name, items, dryRun }) => {
90
+ try {
91
+ const body = { type, name, items };
92
+ if (dryRun)
93
+ return (0, safety_js_1.formatDryRun)("PUT", `/sites/${siteId}/traffic-matching-lists/${trafficMatchingListId}`, body);
94
+ const data = await client.put(`/sites/${siteId}/traffic-matching-lists/${trafficMatchingListId}`, body);
95
+ return (0, responses_js_1.formatSuccess)(data);
96
+ }
97
+ catch (err) {
98
+ return (0, responses_js_1.formatError)(err);
99
+ }
100
+ });
101
+ server.tool("unifi_delete_traffic_matching_list", "DESTRUCTIVE: Delete a traffic matching list", {
102
+ siteId: zod_1.z.string().describe("Site ID"),
103
+ trafficMatchingListId: zod_1.z.string().describe("Traffic matching list ID"),
104
+ confirm: zod_1.z
105
+ .boolean()
106
+ .optional()
107
+ .describe("Must be true to execute this destructive action"),
108
+ dryRun: zod_1.z
109
+ .boolean()
110
+ .optional()
111
+ .describe("Preview this action without executing it"),
112
+ }, safety_js_1.DESTRUCTIVE, async ({ siteId, trafficMatchingListId, confirm, dryRun }) => {
113
+ const guard = (0, safety_js_1.requireConfirmation)(confirm, "This will delete the traffic matching list");
114
+ if (guard)
115
+ return guard;
116
+ try {
117
+ if (dryRun)
118
+ return (0, safety_js_1.formatDryRun)("DELETE", `/sites/${siteId}/traffic-matching-lists/${trafficMatchingListId}`, {});
119
+ const data = await client.delete(`/sites/${siteId}/traffic-matching-lists/${trafficMatchingListId}`);
120
+ return (0, responses_js_1.formatSuccess)(data);
121
+ }
122
+ catch (err) {
123
+ return (0, responses_js_1.formatError)(err);
124
+ }
125
+ });
126
+ }
@@ -0,0 +1,3 @@
1
+ import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
2
+ import { NetworkClient } from "../client.js";
3
+ export declare function registerWifiTools(server: McpServer, client: NetworkClient, readOnly?: boolean): void;
@@ -0,0 +1,140 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.registerWifiTools = registerWifiTools;
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 registerWifiTools(server, client, readOnly = false) {
9
+ server.tool("unifi_list_wifi", "List all WiFi broadcasts (SSIDs) 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}/wifi${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_wifi", "Get a specific WiFi network by ID", {
39
+ siteId: zod_1.z.string().describe("Site ID"),
40
+ wifiBroadcastId: zod_1.z.string().describe("WiFi Broadcast ID"),
41
+ }, safety_js_1.READ_ONLY, async ({ siteId, wifiBroadcastId }) => {
42
+ try {
43
+ const data = await client.get(`/sites/${siteId}/wifi/${wifiBroadcastId}`);
44
+ return (0, responses_js_1.formatSuccess)(data);
45
+ }
46
+ catch (err) {
47
+ return (0, responses_js_1.formatError)(err);
48
+ }
49
+ });
50
+ if (readOnly)
51
+ return;
52
+ server.tool("unifi_create_wifi", "Create a new WiFi network (SSID)", {
53
+ siteId: zod_1.z.string().describe("Site ID"),
54
+ name: zod_1.z.string().describe("SSID name"),
55
+ enabled: zod_1.z.boolean().describe("Enable the WiFi network"),
56
+ type: zod_1.z.enum(["STANDARD"]).describe("WiFi type"),
57
+ broadcastingFrequenciesGHz: zod_1.z
58
+ .array(zod_1.z.string())
59
+ .describe("Frequencies: 2.4, 5, 6"),
60
+ dryRun: zod_1.z
61
+ .boolean()
62
+ .optional()
63
+ .describe("Preview this action without executing it"),
64
+ }, safety_js_1.WRITE_NOT_IDEMPOTENT, async ({ siteId, name, enabled, type, broadcastingFrequenciesGHz, dryRun, }) => {
65
+ try {
66
+ const body = {
67
+ name,
68
+ enabled,
69
+ type,
70
+ broadcastingFrequenciesGHz: broadcastingFrequenciesGHz.map(Number),
71
+ };
72
+ if (dryRun)
73
+ return (0, safety_js_1.formatDryRun)("POST", `/sites/${siteId}/wifi`, body);
74
+ const data = await client.post(`/sites/${siteId}/wifi`, body);
75
+ return (0, responses_js_1.formatSuccess)(data);
76
+ }
77
+ catch (err) {
78
+ return (0, responses_js_1.formatError)(err);
79
+ }
80
+ });
81
+ server.tool("unifi_update_wifi", "Update an existing WiFi network", {
82
+ siteId: zod_1.z.string().describe("Site ID"),
83
+ wifiBroadcastId: zod_1.z.string().describe("WiFi Broadcast ID"),
84
+ name: zod_1.z.string().optional().describe("SSID name"),
85
+ enabled: zod_1.z.boolean().optional().describe("Enable the WiFi network"),
86
+ dryRun: zod_1.z
87
+ .boolean()
88
+ .optional()
89
+ .describe("Preview this action without executing it"),
90
+ }, safety_js_1.WRITE, async ({ siteId, wifiBroadcastId, name, enabled, dryRun }) => {
91
+ try {
92
+ const body = {};
93
+ if (name !== undefined)
94
+ body.name = name;
95
+ if (enabled !== undefined)
96
+ body.enabled = enabled;
97
+ if (dryRun)
98
+ return (0, safety_js_1.formatDryRun)("PUT", `/sites/${siteId}/wifi/${wifiBroadcastId}`, body);
99
+ const data = await client.put(`/sites/${siteId}/wifi/${wifiBroadcastId}`, body);
100
+ return (0, responses_js_1.formatSuccess)(data);
101
+ }
102
+ catch (err) {
103
+ return (0, responses_js_1.formatError)(err);
104
+ }
105
+ });
106
+ server.tool("unifi_delete_wifi", "DESTRUCTIVE: Delete a WiFi network — all clients on this SSID will be disconnected", {
107
+ siteId: zod_1.z.string().describe("Site ID"),
108
+ wifiBroadcastId: zod_1.z.string().describe("WiFi Broadcast ID"),
109
+ force: zod_1.z
110
+ .boolean()
111
+ .optional()
112
+ .default(false)
113
+ .describe("Force delete (default: false)"),
114
+ confirm: zod_1.z
115
+ .boolean()
116
+ .optional()
117
+ .describe("Must be true to execute this destructive action"),
118
+ dryRun: zod_1.z
119
+ .boolean()
120
+ .optional()
121
+ .describe("Preview this action without executing it"),
122
+ }, safety_js_1.DESTRUCTIVE, async ({ siteId, wifiBroadcastId, force, confirm, dryRun }) => {
123
+ const guard = (0, safety_js_1.requireConfirmation)(confirm, "This will delete the WiFi network and disconnect all clients on this SSID");
124
+ if (guard)
125
+ return guard;
126
+ try {
127
+ let path = `/sites/${siteId}/wifi/${wifiBroadcastId}`;
128
+ if (force) {
129
+ path += "?force=true";
130
+ }
131
+ if (dryRun)
132
+ return (0, safety_js_1.formatDryRun)("DELETE", path);
133
+ const data = await client.delete(path);
134
+ return (0, responses_js_1.formatSuccess)(data);
135
+ }
136
+ catch (err) {
137
+ return (0, responses_js_1.formatError)(err);
138
+ }
139
+ });
140
+ }
@@ -0,0 +1,5 @@
1
+ export declare function buildQuery(params: {
2
+ offset?: number;
3
+ limit?: number;
4
+ filter?: string;
5
+ }): string;
@@ -0,0 +1,14 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.buildQuery = buildQuery;
4
+ function buildQuery(params) {
5
+ const searchParams = new URLSearchParams();
6
+ if (params.offset !== undefined)
7
+ searchParams.set("offset", String(params.offset));
8
+ if (params.limit !== undefined)
9
+ searchParams.set("limit", String(params.limit));
10
+ if (params.filter !== undefined)
11
+ searchParams.set("filter", params.filter);
12
+ const qs = searchParams.toString();
13
+ return qs ? `?${qs}` : "";
14
+ }
@@ -0,0 +1,13 @@
1
+ export declare function formatSuccess(data: unknown): {
2
+ content: {
3
+ type: "text";
4
+ text: string;
5
+ }[];
6
+ };
7
+ export declare function formatError(err: unknown): {
8
+ content: {
9
+ type: "text";
10
+ text: string;
11
+ }[];
12
+ isError: boolean;
13
+ };
@@ -0,0 +1,18 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.formatSuccess = formatSuccess;
4
+ exports.formatError = formatError;
5
+ function formatSuccess(data) {
6
+ return {
7
+ content: [
8
+ { type: "text", text: JSON.stringify(data, null, 2) },
9
+ ],
10
+ };
11
+ }
12
+ function formatError(err) {
13
+ const message = err instanceof Error ? err.message : String(err);
14
+ return {
15
+ content: [{ type: "text", text: `Error: ${message}` }],
16
+ isError: true,
17
+ };
18
+ }
@@ -0,0 +1,18 @@
1
+ import type { ToolAnnotations } from "@modelcontextprotocol/sdk/types.js";
2
+ export declare const READ_ONLY: ToolAnnotations;
3
+ export declare const WRITE: ToolAnnotations;
4
+ export declare const WRITE_NOT_IDEMPOTENT: ToolAnnotations;
5
+ export declare const DESTRUCTIVE: ToolAnnotations;
6
+ export declare function formatDryRun(method: string, path: string, body?: unknown): {
7
+ content: {
8
+ type: "text";
9
+ text: string;
10
+ }[];
11
+ };
12
+ export declare function requireConfirmation(confirm: boolean | undefined, action: string): {
13
+ content: {
14
+ type: "text";
15
+ text: string;
16
+ }[];
17
+ isError: true;
18
+ } | null;
@@ -0,0 +1,47 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.DESTRUCTIVE = exports.WRITE_NOT_IDEMPOTENT = exports.WRITE = exports.READ_ONLY = void 0;
4
+ exports.formatDryRun = formatDryRun;
5
+ exports.requireConfirmation = requireConfirmation;
6
+ exports.READ_ONLY = {
7
+ readOnlyHint: true,
8
+ destructiveHint: false,
9
+ };
10
+ exports.WRITE = {
11
+ readOnlyHint: false,
12
+ destructiveHint: false,
13
+ idempotentHint: true,
14
+ };
15
+ exports.WRITE_NOT_IDEMPOTENT = {
16
+ readOnlyHint: false,
17
+ destructiveHint: false,
18
+ idempotentHint: false,
19
+ };
20
+ exports.DESTRUCTIVE = {
21
+ readOnlyHint: false,
22
+ destructiveHint: true,
23
+ idempotentHint: false,
24
+ };
25
+ function formatDryRun(method, path, body) {
26
+ const preview = {
27
+ dryRun: true,
28
+ wouldExecute: { method, path, ...(body !== undefined ? { body } : {}) },
29
+ };
30
+ return {
31
+ content: [{ type: "text", text: JSON.stringify(preview, null, 2) }],
32
+ };
33
+ }
34
+ function requireConfirmation(confirm, action) {
35
+ if (confirm !== true) {
36
+ return {
37
+ content: [
38
+ {
39
+ type: "text",
40
+ text: `This action requires explicit confirmation: ${action}. Re-invoke with confirm: true to proceed.`,
41
+ },
42
+ ],
43
+ isError: true,
44
+ };
45
+ }
46
+ return null;
47
+ }
package/package.json ADDED
@@ -0,0 +1,50 @@
1
+ {
2
+ "name": "@owine/unifi-network-mcp",
3
+ "version": "0.9.0",
4
+ "description": "MCP server for the UniFi Network API",
5
+ "main": "dist/index.js",
6
+ "bin": {
7
+ "unifi-network-mcp": "dist/index.js"
8
+ },
9
+ "files": [
10
+ "dist"
11
+ ],
12
+ "scripts": {
13
+ "build": "tsc",
14
+ "start": "node dist/index.js",
15
+ "typecheck": "tsc --noEmit",
16
+ "test": "vitest run",
17
+ "test:watch": "vitest",
18
+ "test:coverage": "vitest run --coverage",
19
+ "lint": "eslint .",
20
+ "lint:fix": "eslint . --fix"
21
+ },
22
+ "engines": {
23
+ "node": ">=20.0.0"
24
+ },
25
+ "repository": {
26
+ "type": "git",
27
+ "url": "https://github.com/owine/unifi-network-mcp.git"
28
+ },
29
+ "keywords": [
30
+ "unifi",
31
+ "network",
32
+ "mcp",
33
+ "model-context-protocol",
34
+ "ubiquiti"
35
+ ],
36
+ "author": "owine",
37
+ "license": "MIT",
38
+ "dependencies": {
39
+ "@modelcontextprotocol/sdk": "^1.12.1",
40
+ "zod": "^4.0.0"
41
+ },
42
+ "devDependencies": {
43
+ "@eslint/js": "9.39.2",
44
+ "@types/node": "24.10.13",
45
+ "eslint": "9.39.2",
46
+ "typescript": "5.9.3",
47
+ "typescript-eslint": "8.55.0",
48
+ "vitest": "4.0.18"
49
+ }
50
+ }