@owine/unifi-protect-mcp 1.1.0 → 2.0.1
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 +4 -20
- package/dist/config.js +1 -1
- package/dist/tools/cameras.js +26 -25
- package/dist/tools/devices.js +12 -7
- package/dist/tools/files.js +11 -13
- package/dist/tools/liveviews.js +9 -10
- package/dist/tools/system.js +3 -3
- package/dist/utils/safety.d.ts +34 -0
- package/dist/utils/safety.js +39 -0
- package/package.json +6 -5
package/README.md
CHANGED
|
@@ -15,15 +15,7 @@ An MCP (Model Context Protocol) server that exposes UniFi Protect's Integration
|
|
|
15
15
|
Add to Claude Code with a single command — no clone or build needed:
|
|
16
16
|
|
|
17
17
|
```bash
|
|
18
|
-
claude mcp add-json unifi-protect '{
|
|
19
|
-
"command": "npx",
|
|
20
|
-
"args": ["-y", "@owine/unifi-protect-mcp@latest"],
|
|
21
|
-
"env": {
|
|
22
|
-
"UNIFI_PROTECT_HOST": "192.168.1.1",
|
|
23
|
-
"UNIFI_PROTECT_API_KEY": "your-api-key",
|
|
24
|
-
"UNIFI_PROTECT_VERIFY_SSL": "false"
|
|
25
|
-
}
|
|
26
|
-
}' -s user
|
|
18
|
+
claude mcp add-json unifi-protect '{"command":"npx","args":["-y","@owine/unifi-protect-mcp@latest"],"env":{"UNIFI_PROTECT_HOST":"192.168.1.1","UNIFI_PROTECT_API_KEY":"your-api-key","UNIFI_PROTECT_VERIFY_SSL":"false"}}' -s user
|
|
27
19
|
```
|
|
28
20
|
|
|
29
21
|
Use `-s user` for global availability across all projects, or `-s project` for the current project only.
|
|
@@ -42,15 +34,7 @@ npm run build
|
|
|
42
34
|
Then add to Claude Code:
|
|
43
35
|
|
|
44
36
|
```bash
|
|
45
|
-
claude mcp add-json unifi-protect '{
|
|
46
|
-
"command": "node",
|
|
47
|
-
"args": ["/path/to/unifi-protect-mcp/dist/index.js"],
|
|
48
|
-
"env": {
|
|
49
|
-
"UNIFI_PROTECT_HOST": "192.168.1.1",
|
|
50
|
-
"UNIFI_PROTECT_API_KEY": "your-api-key",
|
|
51
|
-
"UNIFI_PROTECT_VERIFY_SSL": "false"
|
|
52
|
-
}
|
|
53
|
-
}' -s user
|
|
37
|
+
claude mcp add-json unifi-protect '{"command":"node","args":["/path/to/unifi-protect-mcp/dist/index.js"],"env":{"UNIFI_PROTECT_HOST":"192.168.1.1","UNIFI_PROTECT_API_KEY":"your-api-key","UNIFI_PROTECT_VERIFY_SSL":"false"}}' -s user
|
|
54
38
|
```
|
|
55
39
|
|
|
56
40
|
### Environment Variables
|
|
@@ -60,7 +44,7 @@ claude mcp add-json unifi-protect '{
|
|
|
60
44
|
| `UNIFI_PROTECT_HOST` | Yes | — | IP or hostname of your UniFi Protect console |
|
|
61
45
|
| `UNIFI_PROTECT_API_KEY` | Yes | — | API key from Protect integration settings |
|
|
62
46
|
| `UNIFI_PROTECT_VERIFY_SSL` | No | `true` | Set to `false` to skip TLS certificate verification (needed for self-signed certs) |
|
|
63
|
-
| `UNIFI_PROTECT_READ_ONLY` | No | `
|
|
47
|
+
| `UNIFI_PROTECT_READ_ONLY` | No | `true` | Set to `false` to enable write/mutating tools (default is monitoring-only mode) |
|
|
64
48
|
|
|
65
49
|
### Manual Configuration
|
|
66
50
|
|
|
@@ -87,7 +71,7 @@ Alternatively, add to your `~/.claude.json` under the top-level `"mcpServers"` k
|
|
|
87
71
|
This server provides layered safety controls for responsible operation:
|
|
88
72
|
|
|
89
73
|
- **Tool annotations** — Every tool declares `readOnlyHint` and `destructiveHint` so MCP clients (like Claude Code) can make informed confirmation decisions
|
|
90
|
-
- **Read-only mode** —
|
|
74
|
+
- **Read-only mode** — Enabled by default. Only read operations (list, get, snapshot) are registered. Set `UNIFI_PROTECT_READ_ONLY=false` to enable write/mutating tools
|
|
91
75
|
- **Confirmation parameter** — The most dangerous tools (`protect_disable_mic`, `protect_trigger_alarm_webhook`) require an explicit `confirm: true` parameter that must be present for the call to succeed
|
|
92
76
|
- **Dry-run support** — All write tools (except those with `confirm`) accept an optional `dryRun: true` parameter that returns a preview of what would happen without making any changes
|
|
93
77
|
|
package/dist/config.js
CHANGED
|
@@ -13,7 +13,7 @@ function loadConfig() {
|
|
|
13
13
|
host: process.env.UNIFI_PROTECT_HOST,
|
|
14
14
|
apiKey: process.env.UNIFI_PROTECT_API_KEY,
|
|
15
15
|
verifySsl: process.env.UNIFI_PROTECT_VERIFY_SSL !== "false",
|
|
16
|
-
readOnly: process.env.UNIFI_PROTECT_READ_ONLY
|
|
16
|
+
readOnly: process.env.UNIFI_PROTECT_READ_ONLY !== "false",
|
|
17
17
|
});
|
|
18
18
|
if (!result.success) {
|
|
19
19
|
const errors = result.error.issues.map((i) => i.message).join(", ");
|
package/dist/tools/cameras.js
CHANGED
|
@@ -3,14 +3,12 @@ Object.defineProperty(exports, "__esModule", { value: true });
|
|
|
3
3
|
exports.registerCameraTools = registerCameraTools;
|
|
4
4
|
const zod_1 = require("zod");
|
|
5
5
|
const responses_js_1 = require("../utils/responses.js");
|
|
6
|
-
const
|
|
7
|
-
const WRITE_ANNOTATIONS = { readOnlyHint: false, destructiveHint: false };
|
|
8
|
-
const DESTRUCTIVE_ANNOTATIONS = { readOnlyHint: false, destructiveHint: true };
|
|
6
|
+
const safety_js_1 = require("../utils/safety.js");
|
|
9
7
|
function registerCameraTools(server, client, readOnly) {
|
|
10
8
|
// --- Read-only tools (always registered) ---
|
|
11
9
|
server.registerTool("protect_list_cameras", {
|
|
12
10
|
description: "List all cameras managed by UniFi Protect",
|
|
13
|
-
annotations:
|
|
11
|
+
annotations: safety_js_1.READ_ONLY,
|
|
14
12
|
}, async () => {
|
|
15
13
|
try {
|
|
16
14
|
const data = await client.get("/cameras");
|
|
@@ -23,7 +21,7 @@ function registerCameraTools(server, client, readOnly) {
|
|
|
23
21
|
server.registerTool("protect_get_camera", {
|
|
24
22
|
description: "Get details for a specific camera by ID",
|
|
25
23
|
inputSchema: { id: zod_1.z.string().describe("Camera ID") },
|
|
26
|
-
annotations:
|
|
24
|
+
annotations: safety_js_1.READ_ONLY,
|
|
27
25
|
}, async ({ id }) => {
|
|
28
26
|
try {
|
|
29
27
|
const data = await client.get(`/cameras/${id}`);
|
|
@@ -36,7 +34,7 @@ function registerCameraTools(server, client, readOnly) {
|
|
|
36
34
|
server.registerTool("protect_get_snapshot", {
|
|
37
35
|
description: "Get a JPEG snapshot from a camera (returns image)",
|
|
38
36
|
inputSchema: { id: zod_1.z.string().describe("Camera ID") },
|
|
39
|
-
annotations:
|
|
37
|
+
annotations: safety_js_1.READ_ONLY,
|
|
40
38
|
}, async ({ id }) => {
|
|
41
39
|
try {
|
|
42
40
|
const { data, mimeType } = await client.getBinary(`/cameras/${id}/snapshot`);
|
|
@@ -57,7 +55,7 @@ function registerCameraTools(server, client, readOnly) {
|
|
|
57
55
|
server.registerTool("protect_get_rtsp_streams", {
|
|
58
56
|
description: "Get active RTSPS stream sessions for a camera",
|
|
59
57
|
inputSchema: { id: zod_1.z.string().describe("Camera ID") },
|
|
60
|
-
annotations:
|
|
58
|
+
annotations: safety_js_1.READ_ONLY,
|
|
61
59
|
}, async ({ id }) => {
|
|
62
60
|
try {
|
|
63
61
|
const data = await client.get(`/cameras/${id}/rtsps-stream`);
|
|
@@ -76,17 +74,17 @@ function registerCameraTools(server, client, readOnly) {
|
|
|
76
74
|
id: zod_1.z.string().describe("Camera ID"),
|
|
77
75
|
settings: zod_1.z
|
|
78
76
|
.record(zod_1.z.string(), zod_1.z.unknown())
|
|
79
|
-
.describe("Partial camera settings to update (
|
|
77
|
+
.describe("Partial camera settings to update. Known fields: name (string), osdSettings (object), ledSettings (object), lcdMessage (object), videoMode (string), hdrType (string), smartDetectSettings (object)"),
|
|
80
78
|
dryRun: zod_1.z
|
|
81
79
|
.boolean()
|
|
82
80
|
.optional()
|
|
83
81
|
.describe("If true, return what would happen without making changes"),
|
|
84
82
|
},
|
|
85
|
-
annotations:
|
|
83
|
+
annotations: safety_js_1.WRITE,
|
|
86
84
|
}, async ({ id, settings, dryRun }) => {
|
|
87
85
|
try {
|
|
88
86
|
if (dryRun) {
|
|
89
|
-
return (0,
|
|
87
|
+
return (0, safety_js_1.formatDryRun)("PATCH", `/cameras/${id}`, settings);
|
|
90
88
|
}
|
|
91
89
|
const data = await client.patch(`/cameras/${id}`, settings);
|
|
92
90
|
return (0, responses_js_1.formatSuccess)(data);
|
|
@@ -104,11 +102,11 @@ function registerCameraTools(server, client, readOnly) {
|
|
|
104
102
|
.optional()
|
|
105
103
|
.describe("If true, return what would happen without making changes"),
|
|
106
104
|
},
|
|
107
|
-
annotations:
|
|
105
|
+
annotations: safety_js_1.WRITE,
|
|
108
106
|
}, async ({ id, dryRun }) => {
|
|
109
107
|
try {
|
|
110
108
|
if (dryRun) {
|
|
111
|
-
return (0,
|
|
109
|
+
return (0, safety_js_1.formatDryRun)("POST", `/cameras/${id}/rtsps-stream`);
|
|
112
110
|
}
|
|
113
111
|
const data = await client.post(`/cameras/${id}/rtsps-stream`);
|
|
114
112
|
return (0, responses_js_1.formatSuccess)(data);
|
|
@@ -126,11 +124,11 @@ function registerCameraTools(server, client, readOnly) {
|
|
|
126
124
|
.optional()
|
|
127
125
|
.describe("If true, return what would happen without making changes"),
|
|
128
126
|
},
|
|
129
|
-
annotations:
|
|
127
|
+
annotations: safety_js_1.DESTRUCTIVE,
|
|
130
128
|
}, async ({ id, dryRun }) => {
|
|
131
129
|
try {
|
|
132
130
|
if (dryRun) {
|
|
133
|
-
return (0,
|
|
131
|
+
return (0, safety_js_1.formatDryRun)("DELETE", `/cameras/${id}/rtsps-stream`);
|
|
134
132
|
}
|
|
135
133
|
const data = await client.delete(`/cameras/${id}/rtsps-stream`);
|
|
136
134
|
return (0, responses_js_1.formatSuccess)(data);
|
|
@@ -148,11 +146,11 @@ function registerCameraTools(server, client, readOnly) {
|
|
|
148
146
|
.optional()
|
|
149
147
|
.describe("If true, return what would happen without making changes"),
|
|
150
148
|
},
|
|
151
|
-
annotations:
|
|
149
|
+
annotations: safety_js_1.WRITE,
|
|
152
150
|
}, async ({ id, dryRun }) => {
|
|
153
151
|
try {
|
|
154
152
|
if (dryRun) {
|
|
155
|
-
return (0,
|
|
153
|
+
return (0, safety_js_1.formatDryRun)("POST", `/cameras/${id}/talkback-session`);
|
|
156
154
|
}
|
|
157
155
|
const data = await client.post(`/cameras/${id}/talkback-session`);
|
|
158
156
|
return (0, responses_js_1.formatSuccess)(data);
|
|
@@ -162,15 +160,18 @@ function registerCameraTools(server, client, readOnly) {
|
|
|
162
160
|
}
|
|
163
161
|
});
|
|
164
162
|
server.registerTool("protect_disable_mic", {
|
|
165
|
-
description: "IRREVERSIBLE: Permanently disable the microphone on a camera.
|
|
163
|
+
description: "IRREVERSIBLE: Permanently disable the microphone on a camera. Can only be re-enabled by factory resetting the camera.",
|
|
166
164
|
inputSchema: {
|
|
167
165
|
id: zod_1.z.string().describe("Camera ID"),
|
|
168
166
|
confirm: zod_1.z
|
|
169
167
|
.literal(true)
|
|
170
168
|
.describe("Must be true to confirm this irreversible action"),
|
|
171
169
|
},
|
|
172
|
-
annotations:
|
|
173
|
-
}, async ({ id }) => {
|
|
170
|
+
annotations: safety_js_1.DESTRUCTIVE,
|
|
171
|
+
}, async ({ id, confirm }) => {
|
|
172
|
+
const denied = (0, safety_js_1.requireConfirmation)(confirm, "permanently disable the microphone");
|
|
173
|
+
if (denied)
|
|
174
|
+
return denied;
|
|
174
175
|
try {
|
|
175
176
|
const data = await client.post(`/cameras/${id}/disable-mic-permanently`);
|
|
176
177
|
return (0, responses_js_1.formatSuccess)(data);
|
|
@@ -189,11 +190,11 @@ function registerCameraTools(server, client, readOnly) {
|
|
|
189
190
|
.optional()
|
|
190
191
|
.describe("If true, return what would happen without making changes"),
|
|
191
192
|
},
|
|
192
|
-
annotations:
|
|
193
|
+
annotations: safety_js_1.WRITE,
|
|
193
194
|
}, async ({ id, slot, dryRun }) => {
|
|
194
195
|
try {
|
|
195
196
|
if (dryRun) {
|
|
196
|
-
return (0,
|
|
197
|
+
return (0, safety_js_1.formatDryRun)("POST", `/cameras/${id}/ptz/patrol/start/${slot}`);
|
|
197
198
|
}
|
|
198
199
|
const data = await client.post(`/cameras/${id}/ptz/patrol/start/${slot}`);
|
|
199
200
|
return (0, responses_js_1.formatSuccess)(data);
|
|
@@ -211,11 +212,11 @@ function registerCameraTools(server, client, readOnly) {
|
|
|
211
212
|
.optional()
|
|
212
213
|
.describe("If true, return what would happen without making changes"),
|
|
213
214
|
},
|
|
214
|
-
annotations:
|
|
215
|
+
annotations: safety_js_1.WRITE,
|
|
215
216
|
}, async ({ id, dryRun }) => {
|
|
216
217
|
try {
|
|
217
218
|
if (dryRun) {
|
|
218
|
-
return (0,
|
|
219
|
+
return (0, safety_js_1.formatDryRun)("POST", `/cameras/${id}/ptz/patrol/stop`);
|
|
219
220
|
}
|
|
220
221
|
const data = await client.post(`/cameras/${id}/ptz/patrol/stop`);
|
|
221
222
|
return (0, responses_js_1.formatSuccess)(data);
|
|
@@ -234,11 +235,11 @@ function registerCameraTools(server, client, readOnly) {
|
|
|
234
235
|
.optional()
|
|
235
236
|
.describe("If true, return what would happen without making changes"),
|
|
236
237
|
},
|
|
237
|
-
annotations:
|
|
238
|
+
annotations: safety_js_1.WRITE,
|
|
238
239
|
}, async ({ id, slot, dryRun }) => {
|
|
239
240
|
try {
|
|
240
241
|
if (dryRun) {
|
|
241
|
-
return (0,
|
|
242
|
+
return (0, safety_js_1.formatDryRun)("POST", `/cameras/${id}/ptz/goto/${slot}`);
|
|
242
243
|
}
|
|
243
244
|
const data = await client.post(`/cameras/${id}/ptz/goto/${slot}`);
|
|
244
245
|
return (0, responses_js_1.formatSuccess)(data);
|
package/dist/tools/devices.js
CHANGED
|
@@ -3,14 +3,19 @@ Object.defineProperty(exports, "__esModule", { value: true });
|
|
|
3
3
|
exports.registerDeviceTools = registerDeviceTools;
|
|
4
4
|
const zod_1 = require("zod");
|
|
5
5
|
const responses_js_1 = require("../utils/responses.js");
|
|
6
|
-
const
|
|
7
|
-
const
|
|
6
|
+
const safety_js_1 = require("../utils/safety.js");
|
|
7
|
+
const settingsHints = {
|
|
8
|
+
light: "Known fields: name (string), isLightForceEnabled (boolean), lightModeSettings (object with mode, enableAt), lightDeviceSettings (object with isIndicatorEnabled, pirDuration, pirSensitivity, ledLevel)",
|
|
9
|
+
sensor: "Known fields: name (string), mountType (string), motionSettings (object), humiditySettings (object), temperatureSettings (object), lightSettings (object), alarmSettings (object)",
|
|
10
|
+
chime: "Known fields: name (string), volume (number), ringSettings (array of ring tone configurations)",
|
|
11
|
+
viewer: "Known fields: name (string), liveview (string, liveview ID to display)",
|
|
12
|
+
};
|
|
8
13
|
function registerDeviceCrud(server, client, deviceType, readOnly) {
|
|
9
14
|
const plural = `${deviceType}s`;
|
|
10
15
|
const label = deviceType.charAt(0).toUpperCase() + deviceType.slice(1);
|
|
11
16
|
server.registerTool(`protect_list_${plural}`, {
|
|
12
17
|
description: `List all ${plural} managed by UniFi Protect`,
|
|
13
|
-
annotations:
|
|
18
|
+
annotations: safety_js_1.READ_ONLY,
|
|
14
19
|
}, async () => {
|
|
15
20
|
try {
|
|
16
21
|
const data = await client.get(`/${plural}`);
|
|
@@ -23,7 +28,7 @@ function registerDeviceCrud(server, client, deviceType, readOnly) {
|
|
|
23
28
|
server.registerTool(`protect_get_${deviceType}`, {
|
|
24
29
|
description: `Get details for a specific ${deviceType} by ID`,
|
|
25
30
|
inputSchema: { id: zod_1.z.string().describe(`${label} ID`) },
|
|
26
|
-
annotations:
|
|
31
|
+
annotations: safety_js_1.READ_ONLY,
|
|
27
32
|
}, async ({ id }) => {
|
|
28
33
|
try {
|
|
29
34
|
const data = await client.get(`/${plural}/${id}`);
|
|
@@ -40,17 +45,17 @@ function registerDeviceCrud(server, client, deviceType, readOnly) {
|
|
|
40
45
|
id: zod_1.z.string().describe(`${label} ID`),
|
|
41
46
|
settings: zod_1.z
|
|
42
47
|
.record(zod_1.z.string(), zod_1.z.unknown())
|
|
43
|
-
.describe(`Partial ${deviceType} settings to update
|
|
48
|
+
.describe(`Partial ${deviceType} settings to update. ${settingsHints[deviceType]}`),
|
|
44
49
|
dryRun: zod_1.z
|
|
45
50
|
.boolean()
|
|
46
51
|
.optional()
|
|
47
52
|
.describe("If true, return what would happen without making changes"),
|
|
48
53
|
},
|
|
49
|
-
annotations:
|
|
54
|
+
annotations: safety_js_1.WRITE,
|
|
50
55
|
}, async ({ id, settings, dryRun }) => {
|
|
51
56
|
try {
|
|
52
57
|
if (dryRun) {
|
|
53
|
-
return (0,
|
|
58
|
+
return (0, safety_js_1.formatDryRun)("PATCH", `/${plural}/${id}`, settings);
|
|
54
59
|
}
|
|
55
60
|
const data = await client.patch(`/${plural}/${id}`, settings);
|
|
56
61
|
return (0, responses_js_1.formatSuccess)(data);
|
package/dist/tools/files.js
CHANGED
|
@@ -3,9 +3,7 @@ Object.defineProperty(exports, "__esModule", { value: true });
|
|
|
3
3
|
exports.registerFileTools = registerFileTools;
|
|
4
4
|
const zod_1 = require("zod");
|
|
5
5
|
const responses_js_1 = require("../utils/responses.js");
|
|
6
|
-
const
|
|
7
|
-
const WRITE_ANNOTATIONS = { readOnlyHint: false, destructiveHint: false };
|
|
8
|
-
const DESTRUCTIVE_ANNOTATIONS = { readOnlyHint: false, destructiveHint: true };
|
|
6
|
+
const safety_js_1 = require("../utils/safety.js");
|
|
9
7
|
function registerFileTools(server, client, readOnly) {
|
|
10
8
|
server.registerTool("protect_list_files", {
|
|
11
9
|
description: "List files of a given type (e.g. 'video', 'timelapse')",
|
|
@@ -14,7 +12,7 @@ function registerFileTools(server, client, readOnly) {
|
|
|
14
12
|
.string()
|
|
15
13
|
.describe("File type to list (e.g. 'video', 'timelapse')"),
|
|
16
14
|
},
|
|
17
|
-
annotations:
|
|
15
|
+
annotations: safety_js_1.READ_ONLY,
|
|
18
16
|
}, async ({ fileType }) => {
|
|
19
17
|
try {
|
|
20
18
|
const data = await client.get(`/files/${fileType}`);
|
|
@@ -34,11 +32,14 @@ function registerFileTools(server, client, readOnly) {
|
|
|
34
32
|
.literal(true)
|
|
35
33
|
.describe("Must be true to confirm triggering the alarm webhook"),
|
|
36
34
|
},
|
|
37
|
-
annotations:
|
|
38
|
-
}, async ({ id }) => {
|
|
35
|
+
annotations: safety_js_1.DESTRUCTIVE,
|
|
36
|
+
}, async ({ id, confirm }) => {
|
|
37
|
+
const denied = (0, safety_js_1.requireConfirmation)(confirm, "trigger the alarm webhook");
|
|
38
|
+
if (denied)
|
|
39
|
+
return denied;
|
|
39
40
|
try {
|
|
40
|
-
|
|
41
|
-
return (0, responses_js_1.formatSuccess)(
|
|
41
|
+
await client.post(`/alarm-manager/webhook/${id}`);
|
|
42
|
+
return (0, responses_js_1.formatSuccess)({ triggered: true, webhookId: id });
|
|
42
43
|
}
|
|
43
44
|
catch (err) {
|
|
44
45
|
return (0, responses_js_1.formatError)(err);
|
|
@@ -58,14 +59,11 @@ function registerFileTools(server, client, readOnly) {
|
|
|
58
59
|
.optional()
|
|
59
60
|
.describe("If true, return what would happen without making changes"),
|
|
60
61
|
},
|
|
61
|
-
annotations:
|
|
62
|
+
annotations: safety_js_1.WRITE,
|
|
62
63
|
}, async ({ fileType, base64Data, contentType, dryRun }) => {
|
|
63
64
|
try {
|
|
64
65
|
if (dryRun) {
|
|
65
|
-
return (0,
|
|
66
|
-
dryRun: true,
|
|
67
|
-
action: "POST",
|
|
68
|
-
path: `/files/${fileType}`,
|
|
66
|
+
return (0, safety_js_1.formatDryRun)("POST", `/files/${fileType}`, {
|
|
69
67
|
contentType,
|
|
70
68
|
dataLength: base64Data.length,
|
|
71
69
|
});
|
package/dist/tools/liveviews.js
CHANGED
|
@@ -3,12 +3,11 @@ Object.defineProperty(exports, "__esModule", { value: true });
|
|
|
3
3
|
exports.registerLiveviewTools = registerLiveviewTools;
|
|
4
4
|
const zod_1 = require("zod");
|
|
5
5
|
const responses_js_1 = require("../utils/responses.js");
|
|
6
|
-
const
|
|
7
|
-
const WRITE_ANNOTATIONS = { readOnlyHint: false, destructiveHint: false };
|
|
6
|
+
const safety_js_1 = require("../utils/safety.js");
|
|
8
7
|
function registerLiveviewTools(server, client, readOnly) {
|
|
9
8
|
server.registerTool("protect_list_liveviews", {
|
|
10
9
|
description: "List all live views configured in UniFi Protect",
|
|
11
|
-
annotations:
|
|
10
|
+
annotations: safety_js_1.READ_ONLY,
|
|
12
11
|
}, async () => {
|
|
13
12
|
try {
|
|
14
13
|
const data = await client.get("/liveviews");
|
|
@@ -21,7 +20,7 @@ function registerLiveviewTools(server, client, readOnly) {
|
|
|
21
20
|
server.registerTool("protect_get_liveview", {
|
|
22
21
|
description: "Get details for a specific live view by ID",
|
|
23
22
|
inputSchema: { id: zod_1.z.string().describe("Liveview ID") },
|
|
24
|
-
annotations:
|
|
23
|
+
annotations: safety_js_1.READ_ONLY,
|
|
25
24
|
}, async ({ id }) => {
|
|
26
25
|
try {
|
|
27
26
|
const data = await client.get(`/liveviews/${id}`);
|
|
@@ -38,17 +37,17 @@ function registerLiveviewTools(server, client, readOnly) {
|
|
|
38
37
|
inputSchema: {
|
|
39
38
|
settings: zod_1.z
|
|
40
39
|
.record(zod_1.z.string(), zod_1.z.unknown())
|
|
41
|
-
.describe("Liveview configuration (
|
|
40
|
+
.describe("Liveview configuration. Required fields: name (string), layout (number of slots), slots (array of {cameras: string[], cycleMode: string, cycleInterval: number}), isDefault (boolean), isGlobal (boolean)"),
|
|
42
41
|
dryRun: zod_1.z
|
|
43
42
|
.boolean()
|
|
44
43
|
.optional()
|
|
45
44
|
.describe("If true, return what would happen without making changes"),
|
|
46
45
|
},
|
|
47
|
-
annotations:
|
|
46
|
+
annotations: safety_js_1.WRITE,
|
|
48
47
|
}, async ({ settings, dryRun }) => {
|
|
49
48
|
try {
|
|
50
49
|
if (dryRun) {
|
|
51
|
-
return (0,
|
|
50
|
+
return (0, safety_js_1.formatDryRun)("POST", "/liveviews", settings);
|
|
52
51
|
}
|
|
53
52
|
const data = await client.post("/liveviews", settings);
|
|
54
53
|
return (0, responses_js_1.formatSuccess)(data);
|
|
@@ -63,17 +62,17 @@ function registerLiveviewTools(server, client, readOnly) {
|
|
|
63
62
|
id: zod_1.z.string().describe("Liveview ID"),
|
|
64
63
|
settings: zod_1.z
|
|
65
64
|
.record(zod_1.z.string(), zod_1.z.unknown())
|
|
66
|
-
.describe("Partial liveview settings to update (
|
|
65
|
+
.describe("Partial liveview settings to update. Known fields: name (string), layout (number), slots (array of {cameras: string[], cycleMode: string, cycleInterval: number}), isDefault (boolean), isGlobal (boolean)"),
|
|
67
66
|
dryRun: zod_1.z
|
|
68
67
|
.boolean()
|
|
69
68
|
.optional()
|
|
70
69
|
.describe("If true, return what would happen without making changes"),
|
|
71
70
|
},
|
|
72
|
-
annotations:
|
|
71
|
+
annotations: safety_js_1.WRITE,
|
|
73
72
|
}, async ({ id, settings, dryRun }) => {
|
|
74
73
|
try {
|
|
75
74
|
if (dryRun) {
|
|
76
|
-
return (0,
|
|
75
|
+
return (0, safety_js_1.formatDryRun)("PATCH", `/liveviews/${id}`, settings);
|
|
77
76
|
}
|
|
78
77
|
const data = await client.patch(`/liveviews/${id}`, settings);
|
|
79
78
|
return (0, responses_js_1.formatSuccess)(data);
|
package/dist/tools/system.js
CHANGED
|
@@ -2,11 +2,11 @@
|
|
|
2
2
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
3
|
exports.registerSystemTools = registerSystemTools;
|
|
4
4
|
const responses_js_1 = require("../utils/responses.js");
|
|
5
|
-
const
|
|
5
|
+
const safety_js_1 = require("../utils/safety.js");
|
|
6
6
|
function registerSystemTools(server, client) {
|
|
7
7
|
server.registerTool("protect_get_info", {
|
|
8
8
|
description: "Get UniFi Protect system information and version details",
|
|
9
|
-
annotations:
|
|
9
|
+
annotations: safety_js_1.READ_ONLY,
|
|
10
10
|
}, async () => {
|
|
11
11
|
try {
|
|
12
12
|
const data = await client.get("/meta/info");
|
|
@@ -18,7 +18,7 @@ function registerSystemTools(server, client) {
|
|
|
18
18
|
});
|
|
19
19
|
server.registerTool("protect_list_nvrs", {
|
|
20
20
|
description: "List all NVR (Network Video Recorder) devices",
|
|
21
|
-
annotations:
|
|
21
|
+
annotations: safety_js_1.READ_ONLY,
|
|
22
22
|
}, async () => {
|
|
23
23
|
try {
|
|
24
24
|
const data = await client.get("/nvrs");
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
/** Annotation constants for tool safety classification */
|
|
2
|
+
export declare const READ_ONLY: {
|
|
3
|
+
readonly readOnlyHint: true;
|
|
4
|
+
readonly destructiveHint: false;
|
|
5
|
+
};
|
|
6
|
+
export declare const WRITE: {
|
|
7
|
+
readonly readOnlyHint: false;
|
|
8
|
+
readonly destructiveHint: false;
|
|
9
|
+
};
|
|
10
|
+
export declare const DESTRUCTIVE: {
|
|
11
|
+
readonly readOnlyHint: false;
|
|
12
|
+
readonly destructiveHint: true;
|
|
13
|
+
};
|
|
14
|
+
/**
|
|
15
|
+
* Build a standard dry-run preview response.
|
|
16
|
+
* Returns the MCP tool result that describes what *would* happen.
|
|
17
|
+
*/
|
|
18
|
+
export declare function formatDryRun(method: string, path: string, body?: unknown): {
|
|
19
|
+
content: {
|
|
20
|
+
type: "text";
|
|
21
|
+
text: string;
|
|
22
|
+
}[];
|
|
23
|
+
};
|
|
24
|
+
/**
|
|
25
|
+
* Guard for destructive actions that require explicit confirmation.
|
|
26
|
+
* Returns an error result if `confirm` is not `true`, otherwise `null`.
|
|
27
|
+
*/
|
|
28
|
+
export declare function requireConfirmation(confirm: boolean, action: string): {
|
|
29
|
+
content: {
|
|
30
|
+
type: "text";
|
|
31
|
+
text: string;
|
|
32
|
+
}[];
|
|
33
|
+
isError: boolean;
|
|
34
|
+
} | null;
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.DESTRUCTIVE = exports.WRITE = exports.READ_ONLY = void 0;
|
|
4
|
+
exports.formatDryRun = formatDryRun;
|
|
5
|
+
exports.requireConfirmation = requireConfirmation;
|
|
6
|
+
const responses_js_1 = require("./responses.js");
|
|
7
|
+
/** Annotation constants for tool safety classification */
|
|
8
|
+
exports.READ_ONLY = { readOnlyHint: true, destructiveHint: false };
|
|
9
|
+
exports.WRITE = { readOnlyHint: false, destructiveHint: false };
|
|
10
|
+
exports.DESTRUCTIVE = { readOnlyHint: false, destructiveHint: true };
|
|
11
|
+
/**
|
|
12
|
+
* Build a standard dry-run preview response.
|
|
13
|
+
* Returns the MCP tool result that describes what *would* happen.
|
|
14
|
+
*/
|
|
15
|
+
function formatDryRun(method, path, body) {
|
|
16
|
+
const preview = { dryRun: true, action: method, path };
|
|
17
|
+
if (body !== undefined) {
|
|
18
|
+
preview.body = body;
|
|
19
|
+
}
|
|
20
|
+
return (0, responses_js_1.formatSuccess)(preview);
|
|
21
|
+
}
|
|
22
|
+
/**
|
|
23
|
+
* Guard for destructive actions that require explicit confirmation.
|
|
24
|
+
* Returns an error result if `confirm` is not `true`, otherwise `null`.
|
|
25
|
+
*/
|
|
26
|
+
function requireConfirmation(confirm, action) {
|
|
27
|
+
if (confirm !== true) {
|
|
28
|
+
return {
|
|
29
|
+
content: [
|
|
30
|
+
{
|
|
31
|
+
type: "text",
|
|
32
|
+
text: `Error: You must set confirm to true to ${action}`,
|
|
33
|
+
},
|
|
34
|
+
],
|
|
35
|
+
isError: true,
|
|
36
|
+
};
|
|
37
|
+
}
|
|
38
|
+
return null;
|
|
39
|
+
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@owine/unifi-protect-mcp",
|
|
3
|
-
"version": "
|
|
3
|
+
"version": "2.0.1",
|
|
4
4
|
"description": "MCP server for UniFi Protect Integration API",
|
|
5
5
|
"main": "dist/index.js",
|
|
6
6
|
"bin": {
|
|
@@ -20,7 +20,7 @@
|
|
|
20
20
|
"lint:fix": "eslint . --fix"
|
|
21
21
|
},
|
|
22
22
|
"engines": {
|
|
23
|
-
"node": "
|
|
23
|
+
"node": "^20.19.0 || ^22.13.0 || >=24"
|
|
24
24
|
},
|
|
25
25
|
"repository": {
|
|
26
26
|
"type": "git",
|
|
@@ -40,11 +40,12 @@
|
|
|
40
40
|
"zod": "^4.0.0"
|
|
41
41
|
},
|
|
42
42
|
"devDependencies": {
|
|
43
|
-
"@eslint/js": "
|
|
43
|
+
"@eslint/js": "10.0.1",
|
|
44
44
|
"@types/node": "24.10.13",
|
|
45
|
-
"
|
|
45
|
+
"@vitest/coverage-v8": "4.0.18",
|
|
46
|
+
"eslint": "10.0.0",
|
|
46
47
|
"typescript": "5.9.3",
|
|
47
|
-
"typescript-eslint": "8.
|
|
48
|
+
"typescript-eslint": "8.56.0",
|
|
48
49
|
"vitest": "4.0.18"
|
|
49
50
|
}
|
|
50
51
|
}
|