@owine/unifi-protect-mcp 2.1.0 → 2.2.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/dist/client.d.ts +2 -0
- package/dist/client.js +11 -0
- package/dist/tools/cameras.js +29 -12
- package/dist/tools/devices.js +2 -2
- package/dist/tools/files.js +5 -5
- package/dist/tools/index.js +2 -0
- package/dist/tools/liveviews.js +3 -3
- package/dist/tools/subscriptions.d.ts +3 -0
- package/dist/tools/subscriptions.js +71 -0
- package/package.json +3 -1
package/dist/client.d.ts
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import WebSocket from "ws";
|
|
1
2
|
import { Config } from "./config.js";
|
|
2
3
|
export declare class ProtectClient {
|
|
3
4
|
private baseUrl;
|
|
@@ -13,4 +14,5 @@ export declare class ProtectClient {
|
|
|
13
14
|
mimeType: string;
|
|
14
15
|
}>;
|
|
15
16
|
postBinary(path: string, data: Buffer, contentType: string): Promise<unknown>;
|
|
17
|
+
connectWebSocket(path: string): WebSocket;
|
|
16
18
|
}
|
package/dist/client.js
CHANGED
|
@@ -1,6 +1,10 @@
|
|
|
1
1
|
"use strict";
|
|
2
|
+
var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
3
|
+
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
4
|
+
};
|
|
2
5
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
6
|
exports.ProtectClient = void 0;
|
|
7
|
+
const ws_1 = __importDefault(require("ws"));
|
|
4
8
|
class ProtectClient {
|
|
5
9
|
baseUrl;
|
|
6
10
|
headers;
|
|
@@ -80,5 +84,12 @@ class ProtectClient {
|
|
|
80
84
|
}
|
|
81
85
|
return response.text();
|
|
82
86
|
}
|
|
87
|
+
connectWebSocket(path) {
|
|
88
|
+
const url = `wss://${this.baseUrl.replace(/^https?:\/\//, "")}${path}`;
|
|
89
|
+
return new ws_1.default(url, {
|
|
90
|
+
headers: { "X-API-KEY": this.headers["X-API-KEY"] },
|
|
91
|
+
rejectUnauthorized: process.env.NODE_TLS_REJECT_UNAUTHORIZED !== "0",
|
|
92
|
+
});
|
|
93
|
+
}
|
|
83
94
|
}
|
|
84
95
|
exports.ProtectClient = ProtectClient;
|
package/dist/tools/cameras.js
CHANGED
|
@@ -34,11 +34,20 @@ function registerCameraTools(server, client, readOnly) {
|
|
|
34
34
|
});
|
|
35
35
|
server.registerTool("protect_get_snapshot", {
|
|
36
36
|
description: "Get a JPEG snapshot from a camera (returns image)",
|
|
37
|
-
inputSchema: {
|
|
37
|
+
inputSchema: {
|
|
38
|
+
id: zod_1.z.string().describe("Camera ID"),
|
|
39
|
+
highQuality: zod_1.z
|
|
40
|
+
.boolean()
|
|
41
|
+
.optional()
|
|
42
|
+
.describe("If true, request a high-quality snapshot"),
|
|
43
|
+
},
|
|
38
44
|
annotations: safety_js_1.READ_ONLY,
|
|
39
|
-
}, async ({ id }) => {
|
|
45
|
+
}, async ({ id, highQuality }) => {
|
|
40
46
|
try {
|
|
41
|
-
const
|
|
47
|
+
const url = highQuality
|
|
48
|
+
? (0, url_js_1.safePath) `/cameras/${id}/snapshot?highQuality=true`
|
|
49
|
+
: (0, url_js_1.safePath) `/cameras/${id}/snapshot`;
|
|
50
|
+
const { data, mimeType } = await client.getBinary(url);
|
|
42
51
|
return {
|
|
43
52
|
content: [
|
|
44
53
|
{
|
|
@@ -75,7 +84,7 @@ function registerCameraTools(server, client, readOnly) {
|
|
|
75
84
|
id: zod_1.z.string().describe("Camera ID"),
|
|
76
85
|
settings: zod_1.z
|
|
77
86
|
.record(zod_1.z.string(), zod_1.z.unknown())
|
|
78
|
-
.describe("Partial camera settings to update. Known fields: name (string), osdSettings (object), ledSettings (object), lcdMessage (object), videoMode (string), hdrType (string), smartDetectSettings (object)"),
|
|
87
|
+
.describe("Partial camera settings to update. Known fields: name (string), osdSettings (object), ledSettings (object), lcdMessage (object), videoMode (string: \"default\" | \"highFps\" | \"slo-mo\"), hdrType (string: \"off\" | \"normal\" | \"always\"), micVolume (number 0-100), smartDetectSettings (object)"),
|
|
79
88
|
dryRun: zod_1.z
|
|
80
89
|
.boolean()
|
|
81
90
|
.optional()
|
|
@@ -98,18 +107,23 @@ function registerCameraTools(server, client, readOnly) {
|
|
|
98
107
|
description: "Create an RTSPS stream session for a camera",
|
|
99
108
|
inputSchema: {
|
|
100
109
|
id: zod_1.z.string().describe("Camera ID"),
|
|
110
|
+
qualities: zod_1.z
|
|
111
|
+
.array(zod_1.z.enum(["high", "medium", "low", "package"]))
|
|
112
|
+
.describe("Quality levels to include in the RTSPS stream"),
|
|
101
113
|
dryRun: zod_1.z
|
|
102
114
|
.boolean()
|
|
103
115
|
.optional()
|
|
104
116
|
.describe("If true, return what would happen without making changes"),
|
|
105
117
|
},
|
|
106
118
|
annotations: safety_js_1.WRITE,
|
|
107
|
-
}, async ({ id, dryRun }) => {
|
|
119
|
+
}, async ({ id, qualities, dryRun }) => {
|
|
108
120
|
try {
|
|
109
121
|
if (dryRun) {
|
|
110
|
-
return (0, safety_js_1.formatDryRun)("POST", (0, url_js_1.safePath) `/cameras/${id}/rtsps-stream
|
|
122
|
+
return (0, safety_js_1.formatDryRun)("POST", (0, url_js_1.safePath) `/cameras/${id}/rtsps-stream`, { qualities });
|
|
111
123
|
}
|
|
112
|
-
const data = await client.post((0, url_js_1.safePath) `/cameras/${id}/rtsps-stream
|
|
124
|
+
const data = await client.post((0, url_js_1.safePath) `/cameras/${id}/rtsps-stream`, {
|
|
125
|
+
qualities,
|
|
126
|
+
});
|
|
113
127
|
return (0, responses_js_1.formatSuccess)(data);
|
|
114
128
|
}
|
|
115
129
|
catch (err) {
|
|
@@ -120,18 +134,21 @@ function registerCameraTools(server, client, readOnly) {
|
|
|
120
134
|
description: "Stop and delete an active RTSPS stream session for a camera",
|
|
121
135
|
inputSchema: {
|
|
122
136
|
id: zod_1.z.string().describe("Camera ID"),
|
|
137
|
+
qualities: zod_1.z
|
|
138
|
+
.array(zod_1.z.enum(["high", "medium", "low", "package"]))
|
|
139
|
+
.describe("Quality levels to remove from the RTSPS stream"),
|
|
123
140
|
dryRun: zod_1.z
|
|
124
141
|
.boolean()
|
|
125
142
|
.optional()
|
|
126
143
|
.describe("If true, return what would happen without making changes"),
|
|
127
144
|
},
|
|
128
145
|
annotations: safety_js_1.DESTRUCTIVE,
|
|
129
|
-
}, async ({ id, dryRun }) => {
|
|
146
|
+
}, async ({ id, qualities, dryRun }) => {
|
|
130
147
|
try {
|
|
131
148
|
if (dryRun) {
|
|
132
|
-
return (0, safety_js_1.formatDryRun)("DELETE", (0, url_js_1.safePath) `/cameras/${id}/rtsps-stream`);
|
|
149
|
+
return (0, safety_js_1.formatDryRun)("DELETE", (0, url_js_1.safePath) `/cameras/${id}/rtsps-stream?qualities=${qualities.join(",")}`);
|
|
133
150
|
}
|
|
134
|
-
const data = await client.delete((0, url_js_1.safePath) `/cameras/${id}/rtsps-stream`);
|
|
151
|
+
const data = await client.delete((0, url_js_1.safePath) `/cameras/${id}/rtsps-stream?qualities=${qualities.join(",")}`);
|
|
135
152
|
return (0, responses_js_1.formatSuccess)(data);
|
|
136
153
|
}
|
|
137
154
|
catch (err) {
|
|
@@ -185,7 +202,7 @@ function registerCameraTools(server, client, readOnly) {
|
|
|
185
202
|
description: "Start PTZ patrol on a camera at a given slot",
|
|
186
203
|
inputSchema: {
|
|
187
204
|
id: zod_1.z.string().describe("Camera ID"),
|
|
188
|
-
slot: zod_1.z.number().int().describe("Patrol slot number"),
|
|
205
|
+
slot: zod_1.z.number().int().min(0).max(4).describe("Patrol slot number (0-4)"),
|
|
189
206
|
dryRun: zod_1.z
|
|
190
207
|
.boolean()
|
|
191
208
|
.optional()
|
|
@@ -230,7 +247,7 @@ function registerCameraTools(server, client, readOnly) {
|
|
|
230
247
|
description: "Move camera PTZ to a preset position",
|
|
231
248
|
inputSchema: {
|
|
232
249
|
id: zod_1.z.string().describe("Camera ID"),
|
|
233
|
-
slot: zod_1.z.number().int().describe("PTZ preset slot number"),
|
|
250
|
+
slot: zod_1.z.number().int().min(-1).max(4).describe("PTZ preset slot number (0-4, or -1 for home position)"),
|
|
234
251
|
dryRun: zod_1.z
|
|
235
252
|
.boolean()
|
|
236
253
|
.optional()
|
package/dist/tools/devices.js
CHANGED
|
@@ -7,8 +7,8 @@ const safety_js_1 = require("../utils/safety.js");
|
|
|
7
7
|
const url_js_1 = require("../utils/url.js");
|
|
8
8
|
const settingsHints = {
|
|
9
9
|
light: "Known fields: name (string), isLightForceEnabled (boolean), lightModeSettings (object with mode, enableAt), lightDeviceSettings (object with isIndicatorEnabled, pirDuration, pirSensitivity, ledLevel)",
|
|
10
|
-
sensor: "Known fields: name (string),
|
|
11
|
-
chime: "Known fields: name (string),
|
|
10
|
+
sensor: "Known fields: name (string), motionSettings (object), humiditySettings (object), temperatureSettings (object), lightSettings (object), alarmSettings (object)",
|
|
11
|
+
chime: "Known fields: name (string), cameraIds (array of camera IDs linked to the chime), ringSettings (array of ring tone configurations)",
|
|
12
12
|
viewer: "Known fields: name (string), liveview (string, liveview ID to display)",
|
|
13
13
|
};
|
|
14
14
|
function registerDeviceCrud(server, client, deviceType, readOnly) {
|
package/dist/tools/files.js
CHANGED
|
@@ -7,11 +7,11 @@ const safety_js_1 = require("../utils/safety.js");
|
|
|
7
7
|
const url_js_1 = require("../utils/url.js");
|
|
8
8
|
function registerFileTools(server, client, readOnly) {
|
|
9
9
|
server.registerTool("protect_list_files", {
|
|
10
|
-
description: "List files of a given type (
|
|
10
|
+
description: "List files of a given type (only 'animations' is supported)",
|
|
11
11
|
inputSchema: {
|
|
12
12
|
fileType: zod_1.z
|
|
13
13
|
.string()
|
|
14
|
-
.describe("File type to list (
|
|
14
|
+
.describe("File type to list (only 'animations' is currently supported)"),
|
|
15
15
|
},
|
|
16
16
|
annotations: safety_js_1.READ_ONLY,
|
|
17
17
|
}, async ({ fileType }) => {
|
|
@@ -28,7 +28,7 @@ function registerFileTools(server, client, readOnly) {
|
|
|
28
28
|
server.registerTool("protect_trigger_alarm_webhook", {
|
|
29
29
|
description: "Trigger an alarm manager webhook by ID. This fires an external alarm action.",
|
|
30
30
|
inputSchema: {
|
|
31
|
-
id: zod_1.z.string().describe("
|
|
31
|
+
id: zod_1.z.string().describe("User-defined webhook ID matching an alarm manager configuration"),
|
|
32
32
|
confirm: zod_1.z
|
|
33
33
|
.literal(true)
|
|
34
34
|
.describe("Must be true to confirm triggering the alarm webhook"),
|
|
@@ -53,8 +53,8 @@ function registerFileTools(server, client, readOnly) {
|
|
|
53
53
|
base64Data: zod_1.z.string().describe("Base64-encoded file content"),
|
|
54
54
|
contentType: zod_1.z
|
|
55
55
|
.string()
|
|
56
|
-
.default("
|
|
57
|
-
.describe("MIME type of the file"),
|
|
56
|
+
.default("image/gif")
|
|
57
|
+
.describe("MIME type of the file (accepted: image/gif, image/png, image/jpeg)"),
|
|
58
58
|
dryRun: zod_1.z
|
|
59
59
|
.boolean()
|
|
60
60
|
.optional()
|
package/dist/tools/index.js
CHANGED
|
@@ -6,8 +6,10 @@ const cameras_js_1 = require("./cameras.js");
|
|
|
6
6
|
const devices_js_1 = require("./devices.js");
|
|
7
7
|
const liveviews_js_1 = require("./liveviews.js");
|
|
8
8
|
const files_js_1 = require("./files.js");
|
|
9
|
+
const subscriptions_js_1 = require("./subscriptions.js");
|
|
9
10
|
function registerAllTools(server, client, readOnly) {
|
|
10
11
|
(0, system_js_1.registerSystemTools)(server, client);
|
|
12
|
+
(0, subscriptions_js_1.registerSubscriptionTools)(server, client);
|
|
11
13
|
(0, cameras_js_1.registerCameraTools)(server, client, readOnly);
|
|
12
14
|
(0, devices_js_1.registerDeviceTools)(server, client, readOnly);
|
|
13
15
|
(0, liveviews_js_1.registerLiveviewTools)(server, client, readOnly);
|
package/dist/tools/liveviews.js
CHANGED
|
@@ -38,7 +38,7 @@ function registerLiveviewTools(server, client, readOnly) {
|
|
|
38
38
|
inputSchema: {
|
|
39
39
|
settings: zod_1.z
|
|
40
40
|
.record(zod_1.z.string(), zod_1.z.unknown())
|
|
41
|
-
.describe("Liveview configuration. Required fields: name (string), layout (number of slots), slots (array of {cameras: string[], cycleMode: string, cycleInterval: number}), isDefault (boolean), isGlobal (boolean)"),
|
|
41
|
+
.describe("Liveview configuration. Required fields: id (string), modelKey (string, must be 'liveview'), owner (string, user ID), name (string), layout (number of slots), slots (array of {cameras: string[], cycleMode: string, cycleInterval: number}), isDefault (boolean), isGlobal (boolean)"),
|
|
42
42
|
dryRun: zod_1.z
|
|
43
43
|
.boolean()
|
|
44
44
|
.optional()
|
|
@@ -58,12 +58,12 @@ function registerLiveviewTools(server, client, readOnly) {
|
|
|
58
58
|
}
|
|
59
59
|
});
|
|
60
60
|
server.registerTool("protect_update_liveview", {
|
|
61
|
-
description: "Update an existing live view (
|
|
61
|
+
description: "Update an existing live view (full replacement via PUT-style PATCH — include all fields)",
|
|
62
62
|
inputSchema: {
|
|
63
63
|
id: zod_1.z.string().describe("Liveview ID"),
|
|
64
64
|
settings: zod_1.z
|
|
65
65
|
.record(zod_1.z.string(), zod_1.z.unknown())
|
|
66
|
-
.describe("
|
|
66
|
+
.describe("Full liveview object for update (all fields required). Known fields: id (string), modelKey (string, must be 'liveview'), owner (string, user ID), name (string), layout (number), slots (array of {cameras: string[], cycleMode: string, cycleInterval: number}), isDefault (boolean), isGlobal (boolean)"),
|
|
67
67
|
dryRun: zod_1.z
|
|
68
68
|
.boolean()
|
|
69
69
|
.optional()
|
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.registerSubscriptionTools = registerSubscriptionTools;
|
|
4
|
+
const zod_1 = require("zod");
|
|
5
|
+
const responses_js_1 = require("../utils/responses.js");
|
|
6
|
+
const safety_js_1 = require("../utils/safety.js");
|
|
7
|
+
const durationSchema = zod_1.z
|
|
8
|
+
.number()
|
|
9
|
+
.min(1)
|
|
10
|
+
.max(30)
|
|
11
|
+
.default(5)
|
|
12
|
+
.describe("Seconds to listen (1-30, default 5)");
|
|
13
|
+
function collectMessages(client, path, duration) {
|
|
14
|
+
return new Promise((resolve) => {
|
|
15
|
+
const messages = [];
|
|
16
|
+
let error;
|
|
17
|
+
const ws = client.connectWebSocket(path);
|
|
18
|
+
const timeout = setTimeout(() => {
|
|
19
|
+
ws.close();
|
|
20
|
+
}, duration * 1000);
|
|
21
|
+
ws.on("message", (data) => {
|
|
22
|
+
const text = String(data);
|
|
23
|
+
try {
|
|
24
|
+
messages.push(JSON.parse(text));
|
|
25
|
+
}
|
|
26
|
+
catch {
|
|
27
|
+
messages.push(text);
|
|
28
|
+
}
|
|
29
|
+
});
|
|
30
|
+
ws.on("error", (err) => {
|
|
31
|
+
error = err.message;
|
|
32
|
+
clearTimeout(timeout);
|
|
33
|
+
ws.close();
|
|
34
|
+
});
|
|
35
|
+
ws.on("close", () => {
|
|
36
|
+
clearTimeout(timeout);
|
|
37
|
+
const result = { messages, duration };
|
|
38
|
+
if (error)
|
|
39
|
+
result.error = error;
|
|
40
|
+
resolve(result);
|
|
41
|
+
});
|
|
42
|
+
});
|
|
43
|
+
}
|
|
44
|
+
function registerSubscriptionTools(server, client) {
|
|
45
|
+
server.registerTool("protect_subscribe_devices", {
|
|
46
|
+
description: "Connect to the device update WebSocket and collect messages for a specified duration. Returns real-time add/update/remove events for Protect-managed hardware devices.",
|
|
47
|
+
inputSchema: { duration: durationSchema },
|
|
48
|
+
annotations: safety_js_1.READ_ONLY,
|
|
49
|
+
}, async ({ duration }) => {
|
|
50
|
+
try {
|
|
51
|
+
const data = await collectMessages(client, "/subscribe/devices", duration);
|
|
52
|
+
return (0, responses_js_1.formatSuccess)(data);
|
|
53
|
+
}
|
|
54
|
+
catch (err) {
|
|
55
|
+
return (0, responses_js_1.formatError)(err);
|
|
56
|
+
}
|
|
57
|
+
});
|
|
58
|
+
server.registerTool("protect_subscribe_events", {
|
|
59
|
+
description: "Connect to the Protect event WebSocket and collect messages for a specified duration. Returns events including motion, ring (doorbell), and smartDetect (person/vehicle/animal/package).",
|
|
60
|
+
inputSchema: { duration: durationSchema },
|
|
61
|
+
annotations: safety_js_1.READ_ONLY,
|
|
62
|
+
}, async ({ duration }) => {
|
|
63
|
+
try {
|
|
64
|
+
const data = await collectMessages(client, "/subscribe/events", duration);
|
|
65
|
+
return (0, responses_js_1.formatSuccess)(data);
|
|
66
|
+
}
|
|
67
|
+
catch (err) {
|
|
68
|
+
return (0, responses_js_1.formatError)(err);
|
|
69
|
+
}
|
|
70
|
+
});
|
|
71
|
+
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@owine/unifi-protect-mcp",
|
|
3
|
-
"version": "2.
|
|
3
|
+
"version": "2.2.0",
|
|
4
4
|
"description": "MCP server for UniFi Protect Integration API",
|
|
5
5
|
"main": "dist/index.js",
|
|
6
6
|
"bin": {
|
|
@@ -37,11 +37,13 @@
|
|
|
37
37
|
"license": "MIT",
|
|
38
38
|
"dependencies": {
|
|
39
39
|
"@modelcontextprotocol/sdk": "^1.12.1",
|
|
40
|
+
"ws": "^8.19.0",
|
|
40
41
|
"zod": "^4.0.0"
|
|
41
42
|
},
|
|
42
43
|
"devDependencies": {
|
|
43
44
|
"@eslint/js": "10.0.1",
|
|
44
45
|
"@types/node": "24.12.0",
|
|
46
|
+
"@types/ws": "^8.18.0",
|
|
45
47
|
"@vitest/coverage-v8": "4.0.18",
|
|
46
48
|
"eslint": "10.0.3",
|
|
47
49
|
"typescript": "5.9.3",
|