@owine/unifi-protect-mcp 2.14.2 → 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/README.md +248 -92
- package/dist/client.d.ts +43 -3
- package/dist/client.js +255 -85
- package/dist/config.d.ts +7 -1
- package/dist/config.js +114 -6
- package/dist/index.js +14 -6
- package/dist/instructions.d.ts +6 -0
- package/dist/instructions.js +24 -0
- package/dist/schemas/cameras.d.ts +32 -30
- package/dist/schemas/cameras.js +7 -6
- package/dist/schemas/common.d.ts +31 -6
- package/dist/schemas/common.js +45 -6
- package/dist/schemas/devices.d.ts +2 -10
- package/dist/schemas/devices.js +35 -26
- package/dist/schemas/misc.d.ts +50 -18
- package/dist/schemas/misc.js +23 -15
- package/dist/schemas/pos.d.ts +1 -1
- package/dist/schemas/pos.js +1 -1
- package/dist/tls.d.ts +41 -0
- package/dist/tls.js +75 -0
- package/dist/tools/alarm-manager.d.ts +3 -0
- package/dist/tools/alarm-manager.js +32 -0
- package/dist/tools/arm-profiles.d.ts +8 -2
- package/dist/tools/arm-profiles.js +98 -95
- package/dist/tools/cameras.d.ts +2 -2
- package/dist/tools/cameras.js +156 -181
- package/dist/tools/device-actions.d.ts +2 -2
- package/dist/tools/device-actions.js +84 -97
- package/dist/tools/devices.d.ts +6 -3
- package/dist/tools/devices.js +50 -51
- package/dist/tools/files.d.ts +2 -2
- package/dist/tools/files.js +82 -57
- package/dist/tools/index.d.ts +2 -2
- package/dist/tools/index.js +15 -10
- package/dist/tools/liveviews.d.ts +2 -2
- package/dist/tools/liveviews.js +52 -52
- package/dist/tools/pos.d.ts +6 -4
- package/dist/tools/pos.js +15 -20
- package/dist/tools/registrar.d.ts +34 -0
- package/dist/tools/registrar.js +103 -0
- package/dist/tools/subscriptions.d.ts +13 -2
- package/dist/tools/subscriptions.js +148 -27
- package/dist/tools/system.d.ts +2 -2
- package/dist/tools/system.js +5 -15
- package/dist/tools/users.d.ts +2 -2
- package/dist/tools/users.js +18 -35
- package/dist/utils/limits.d.ts +33 -0
- package/dist/utils/limits.js +74 -0
- package/dist/utils/paging.d.ts +15 -0
- package/dist/utils/paging.js +28 -0
- package/dist/utils/params.d.ts +13 -0
- package/dist/utils/params.js +40 -0
- package/dist/utils/responses.js +2 -3
- package/dist/utils/safety.d.ts +39 -3
- package/dist/utils/safety.js +45 -3
- package/dist/utils/url.d.ts +4 -1
- package/dist/utils/url.js +4 -1
- package/dist/version.d.ts +6 -0
- package/dist/version.js +11 -0
- package/package.json +14 -12
package/dist/tools/files.js
CHANGED
|
@@ -5,78 +5,103 @@ const zod_1 = require("zod");
|
|
|
5
5
|
const responses_js_1 = require("../utils/responses.js");
|
|
6
6
|
const safety_js_1 = require("../utils/safety.js");
|
|
7
7
|
const url_js_1 = require("../utils/url.js");
|
|
8
|
+
const paging_js_1 = require("../utils/paging.js");
|
|
9
|
+
const limits_js_1 = require("../utils/limits.js");
|
|
8
10
|
const misc_js_1 = require("../schemas/misc.js");
|
|
11
|
+
// The only asset file type the API accepts (path param `assetFileType`).
|
|
12
|
+
const FILE_TYPE = zod_1.z
|
|
13
|
+
.literal("animations")
|
|
14
|
+
.describe("Asset file type (only 'animations' is supported)");
|
|
15
|
+
const UPLOAD_CONTENT_TYPES = [
|
|
16
|
+
"image/gif",
|
|
17
|
+
"image/jpeg",
|
|
18
|
+
"image/png",
|
|
19
|
+
"audio/mpeg",
|
|
20
|
+
"audio/mp4",
|
|
21
|
+
"audio/wave",
|
|
22
|
+
"audio/x-caf",
|
|
23
|
+
];
|
|
9
24
|
function registerFileTools(server, client, readOnly) {
|
|
10
25
|
server.registerTool("protect_list_files", {
|
|
11
|
-
description: "List
|
|
12
|
-
inputSchema: {
|
|
13
|
-
fileType: zod_1.z
|
|
14
|
-
.string()
|
|
15
|
-
.describe("File type to list (only 'animations' is currently supported)"),
|
|
16
|
-
},
|
|
26
|
+
description: "List device asset files (only 'animations' is supported — clips uploadable to G4 Doorbell screens). Returns array; each record (verified live 7.3.70): id, name (server-generated), type, originalName, path, size, createdAt, updatedAt, metadata. An image upload (verified for GIF) later gains a derived <name>.gif preview record, created asynchronously, so a listing taken right after the upload may show only the stored image." + paging_js_1.PAGING_NOTE,
|
|
27
|
+
inputSchema: { fileType: FILE_TYPE, ...paging_js_1.pagingInput },
|
|
17
28
|
outputSchema: misc_js_1.fileListOutputSchema,
|
|
18
29
|
annotations: safety_js_1.READ_ONLY,
|
|
19
|
-
}, async ({ fileType }) => {
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
return (0, responses_js_1.formatSuccess)(data);
|
|
23
|
-
}
|
|
24
|
-
catch (err) {
|
|
25
|
-
return (0, responses_js_1.formatError)(err);
|
|
26
|
-
}
|
|
30
|
+
}, async ({ fileType, limit, offset }) => {
|
|
31
|
+
const data = await client.get((0, url_js_1.safePath) `/files/${fileType}`);
|
|
32
|
+
return (0, responses_js_1.formatSuccess)((0, paging_js_1.pageOf)(data, limit, offset));
|
|
27
33
|
});
|
|
28
34
|
if (readOnly)
|
|
29
35
|
return;
|
|
30
|
-
server.registerTool("protect_trigger_alarm_webhook", {
|
|
31
|
-
description: "Trigger an alarm manager webhook by ID. This fires an external alarm action.",
|
|
32
|
-
inputSchema: {
|
|
33
|
-
id: zod_1.z.string().describe("User-defined webhook ID matching an alarm manager configuration"),
|
|
34
|
-
confirm: zod_1.z
|
|
35
|
-
.literal(true)
|
|
36
|
-
.describe("Must be true to confirm triggering the alarm webhook"),
|
|
37
|
-
},
|
|
38
|
-
annotations: safety_js_1.DESTRUCTIVE,
|
|
39
|
-
}, async ({ id, confirm }) => {
|
|
40
|
-
const denied = (0, safety_js_1.requireConfirmation)(confirm, "trigger the alarm webhook");
|
|
41
|
-
if (denied)
|
|
42
|
-
return denied;
|
|
43
|
-
try {
|
|
44
|
-
await client.post((0, url_js_1.safePath) `/alarm-manager/webhook/${id}`);
|
|
45
|
-
return (0, responses_js_1.formatSuccess)({ triggered: true, webhookId: id });
|
|
46
|
-
}
|
|
47
|
-
catch (err) {
|
|
48
|
-
return (0, responses_js_1.formatError)(err);
|
|
49
|
-
}
|
|
50
|
-
});
|
|
51
36
|
server.registerTool("protect_upload_file", {
|
|
52
|
-
description: "Upload a base64-encoded file to
|
|
37
|
+
description: "Upload a base64-encoded device asset file (sent as multipart/form-data). Observed for GIF image uploads: Protect stores the image as PNG (appending the stored extension to originalName) and a few seconds later creates a separate <name>.gif preview asset, which protect_list_files shows once it exists. Audio uploads have not been observed. Returns: id, name (server-generated), type, originalName, path, size, createdAt, updatedAt, metadata.",
|
|
53
38
|
inputSchema: {
|
|
54
|
-
fileType:
|
|
55
|
-
base64Data: zod_1.z
|
|
56
|
-
contentType: zod_1.z
|
|
39
|
+
fileType: FILE_TYPE,
|
|
40
|
+
base64Data: zod_1.z
|
|
57
41
|
.string()
|
|
42
|
+
.min(1)
|
|
43
|
+
.max(limits_js_1.MAX_UPLOAD_BASE64_CHARS, "File too large to send in one MCP message")
|
|
44
|
+
.describe("Base64-encoded file content (a data: URL prefix is accepted and stripped)"),
|
|
45
|
+
contentType: zod_1.z
|
|
46
|
+
.enum(UPLOAD_CONTENT_TYPES)
|
|
58
47
|
.default("image/gif")
|
|
59
|
-
.describe("MIME type of the file
|
|
60
|
-
|
|
61
|
-
.
|
|
48
|
+
.describe("MIME type of the file"),
|
|
49
|
+
fileName: zod_1.z
|
|
50
|
+
.string()
|
|
51
|
+
.min(1)
|
|
62
52
|
.optional()
|
|
63
|
-
.describe("
|
|
53
|
+
.describe("Original file name to record. Reported back as originalName, with the stored extension appended when Protect converts the file (e.g. welcome.gif → welcome.gif.png)"),
|
|
54
|
+
dryRun: (0, safety_js_1.dryRunInput)(),
|
|
64
55
|
},
|
|
56
|
+
outputSchema: misc_js_1.fileOutputSchema,
|
|
65
57
|
annotations: safety_js_1.WRITE,
|
|
66
|
-
}, async ({ fileType, base64Data, contentType, dryRun }) => {
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
const data = await client.postBinary((0, url_js_1.safePath) `/files/${fileType}`, buffer, contentType);
|
|
76
|
-
return (0, responses_js_1.formatSuccess)(data);
|
|
77
|
-
}
|
|
78
|
-
catch (err) {
|
|
79
|
-
return (0, responses_js_1.formatError)(err);
|
|
58
|
+
}, async ({ fileType, base64Data, contentType, fileName, dryRun }) => {
|
|
59
|
+
const name = fileName ?? defaultFileName(contentType);
|
|
60
|
+
const base64 = normalizeBase64(base64Data);
|
|
61
|
+
if (dryRun) {
|
|
62
|
+
return (0, safety_js_1.formatDryRun)("POST", (0, url_js_1.safePath) `/files/${fileType}`, {
|
|
63
|
+
contentType,
|
|
64
|
+
fileName: name,
|
|
65
|
+
byteLength: Buffer.byteLength(base64, "base64"),
|
|
66
|
+
});
|
|
80
67
|
}
|
|
68
|
+
const data = await client.postFile((0, url_js_1.safePath) `/files/${fileType}`, Buffer.from(base64, "base64"), contentType, name);
|
|
69
|
+
return (0, responses_js_1.formatSuccess)(data);
|
|
81
70
|
});
|
|
82
71
|
}
|
|
72
|
+
const EXTENSIONS = {
|
|
73
|
+
"image/gif": "gif",
|
|
74
|
+
"image/jpeg": "jpg",
|
|
75
|
+
"image/png": "png",
|
|
76
|
+
"audio/mpeg": "mp3",
|
|
77
|
+
"audio/mp4": "m4a",
|
|
78
|
+
"audio/wave": "wav",
|
|
79
|
+
"audio/x-caf": "caf",
|
|
80
|
+
};
|
|
81
|
+
function defaultFileName(contentType) {
|
|
82
|
+
return `upload.${EXTENSIONS[contentType]}`;
|
|
83
|
+
}
|
|
84
|
+
const DATA_URL_PREFIX = /^data:[^,]*;base64,/i;
|
|
85
|
+
/** Base64 encodes every 3 bytes as a group of 4 characters. */
|
|
86
|
+
const BASE64_GROUP_LENGTH = 4;
|
|
87
|
+
/** Standard or URL-safe base64 (one alphabet, not mixed), padded or not. */
|
|
88
|
+
const BASE64 = /^[A-Za-z0-9+/]+={0,2}$|^[A-Za-z0-9_-]+={0,2}$/;
|
|
89
|
+
/**
|
|
90
|
+
* Strip whitespace and a data: URL prefix, then reject anything that is not
|
|
91
|
+
* well-formed base64. Buffer.from(x, "base64") skips characters it cannot
|
|
92
|
+
* decode and ignores a dangling final character, so malformed input would
|
|
93
|
+
* otherwise upload as corrupt bytes with no error. Whitespace goes first, so
|
|
94
|
+
* a prefix after a leading line break is still found.
|
|
95
|
+
*/
|
|
96
|
+
function normalizeBase64(input) {
|
|
97
|
+
const cleaned = input.replace(/\s+/g, "").replace(DATA_URL_PREFIX, "");
|
|
98
|
+
// Padded input comes in whole groups; unpadded input can end in a partial
|
|
99
|
+
// group of 2 or 3 characters, but never a lone character (remainder 1).
|
|
100
|
+
const remainder = cleaned.length % BASE64_GROUP_LENGTH;
|
|
101
|
+
const wellFormed = BASE64.test(cleaned) &&
|
|
102
|
+
(cleaned.endsWith("=") ? remainder === 0 : remainder !== 1);
|
|
103
|
+
if (!wellFormed) {
|
|
104
|
+
throw new Error("base64Data is not valid base64 (expected A-Z, a-z, 0-9 with + and / or - and _, correctly = padded, or a data: URL)");
|
|
105
|
+
}
|
|
106
|
+
return cleaned;
|
|
107
|
+
}
|
package/dist/tools/index.d.ts
CHANGED
|
@@ -1,3 +1,3 @@
|
|
|
1
|
-
import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
|
|
2
|
-
import { ProtectClient } from "../client.js";
|
|
1
|
+
import type { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
|
|
2
|
+
import type { ProtectClient } from "../client.js";
|
|
3
3
|
export declare function registerAllTools(server: McpServer, client: ProtectClient, readOnly: boolean): void;
|
package/dist/tools/index.js
CHANGED
|
@@ -1,25 +1,30 @@
|
|
|
1
1
|
"use strict";
|
|
2
2
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
3
|
exports.registerAllTools = registerAllTools;
|
|
4
|
+
const registrar_js_1 = require("./registrar.js");
|
|
4
5
|
const system_js_1 = require("./system.js");
|
|
5
6
|
const cameras_js_1 = require("./cameras.js");
|
|
6
7
|
const devices_js_1 = require("./devices.js");
|
|
7
8
|
const device_actions_js_1 = require("./device-actions.js");
|
|
8
9
|
const liveviews_js_1 = require("./liveviews.js");
|
|
9
10
|
const files_js_1 = require("./files.js");
|
|
11
|
+
const alarm_manager_js_1 = require("./alarm-manager.js");
|
|
10
12
|
const subscriptions_js_1 = require("./subscriptions.js");
|
|
11
13
|
const arm_profiles_js_1 = require("./arm-profiles.js");
|
|
12
14
|
const users_js_1 = require("./users.js");
|
|
13
15
|
const pos_js_1 = require("./pos.js");
|
|
14
16
|
function registerAllTools(server, client, readOnly) {
|
|
15
|
-
|
|
16
|
-
(0,
|
|
17
|
-
(0,
|
|
18
|
-
(0,
|
|
19
|
-
(0,
|
|
20
|
-
(0,
|
|
21
|
-
(0,
|
|
22
|
-
(0,
|
|
23
|
-
(0,
|
|
24
|
-
(0,
|
|
17
|
+
// Modules register through instrumentTools only; see registrar.ts.
|
|
18
|
+
const tools = (0, registrar_js_1.instrumentTools)(server, readOnly);
|
|
19
|
+
(0, system_js_1.registerSystemTools)(tools, client);
|
|
20
|
+
(0, subscriptions_js_1.registerSubscriptionTools)(tools, client);
|
|
21
|
+
(0, cameras_js_1.registerCameraTools)(tools, client, readOnly);
|
|
22
|
+
(0, devices_js_1.registerDeviceTools)(tools, client, readOnly);
|
|
23
|
+
(0, device_actions_js_1.registerDeviceActionTools)(tools, client, readOnly);
|
|
24
|
+
(0, liveviews_js_1.registerLiveviewTools)(tools, client, readOnly);
|
|
25
|
+
(0, files_js_1.registerFileTools)(tools, client, readOnly);
|
|
26
|
+
(0, alarm_manager_js_1.registerAlarmManagerTools)(tools, client, readOnly);
|
|
27
|
+
(0, arm_profiles_js_1.registerArmProfileTools)(tools, client, readOnly);
|
|
28
|
+
(0, users_js_1.registerUserTools)(tools, client);
|
|
29
|
+
(0, pos_js_1.registerPosTools)(tools, client, readOnly);
|
|
25
30
|
}
|
|
@@ -1,3 +1,3 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import type { ToolRegistrar } from "./registrar.js";
|
|
2
2
|
import { ProtectClient } from "../client.js";
|
|
3
|
-
export declare function registerLiveviewTools(server:
|
|
3
|
+
export declare function registerLiveviewTools(server: ToolRegistrar, client: ProtectClient, readOnly: boolean): void;
|
package/dist/tools/liveviews.js
CHANGED
|
@@ -5,86 +5,86 @@ const zod_1 = require("zod");
|
|
|
5
5
|
const responses_js_1 = require("../utils/responses.js");
|
|
6
6
|
const safety_js_1 = require("../utils/safety.js");
|
|
7
7
|
const url_js_1 = require("../utils/url.js");
|
|
8
|
+
const paging_js_1 = require("../utils/paging.js");
|
|
9
|
+
const params_js_1 = require("../utils/params.js");
|
|
8
10
|
const misc_js_1 = require("../schemas/misc.js");
|
|
11
|
+
// POST and PATCH both take the full liveview object; every field is required
|
|
12
|
+
// (7.3.70 docs). Slot item schema is not rendered in the docs, so slots pass
|
|
13
|
+
// through beyond the observed { cameras, cycleMode, cycleInterval }.
|
|
14
|
+
const liveviewSlotInput = zod_1.z
|
|
15
|
+
.object({
|
|
16
|
+
cameras: zod_1.z.array(zod_1.z.string()).describe("Camera IDs shown in this slot"),
|
|
17
|
+
cycleMode: zod_1.z.string().describe('Cycle mode, e.g. "time" or "motion"'),
|
|
18
|
+
cycleInterval: zod_1.z.number().describe("Cycle interval in seconds"),
|
|
19
|
+
})
|
|
20
|
+
.loose();
|
|
21
|
+
const liveviewBody = zod_1.z
|
|
22
|
+
.object({
|
|
23
|
+
id: (0, params_js_1.resourceId)().describe("Liveview ID"),
|
|
24
|
+
modelKey: zod_1.z.literal("liveview").describe('Must be "liveview"'),
|
|
25
|
+
name: zod_1.z.string().describe("Liveview name"),
|
|
26
|
+
isDefault: zod_1.z.boolean().describe("Whether this is the default liveview for all viewers"),
|
|
27
|
+
isGlobal: zod_1.z.boolean().describe("Whether this liveview is available to all users"),
|
|
28
|
+
owner: zod_1.z.string().describe("Owning user ID"),
|
|
29
|
+
layout: zod_1.z.number().min(1).max(26).describe("Number of slots (1-26)"),
|
|
30
|
+
slots: zod_1.z.array(liveviewSlotInput).describe("Per-slot cameras and cycling settings"),
|
|
31
|
+
})
|
|
32
|
+
.loose();
|
|
9
33
|
function registerLiveviewTools(server, client, readOnly) {
|
|
10
34
|
server.registerTool("protect_list_liveviews", {
|
|
11
|
-
description: "List all live views (camera grid layouts shown on viewers / in the Protect UI). Returns array; each liveview includes (Integration API 7.
|
|
35
|
+
description: "List all live views (camera grid layouts shown on viewers / in the Protect UI). Returns array; each liveview includes (Integration API 7.3.70-verified): id, modelKey, name, isDefault, isGlobal, layout (number, slot count), owner (user ID), slots (array of {cameras: string[], cycleMode, cycleInterval}). NOTE: slots use a `cameras` string-array, not a single `cameraId`." + paging_js_1.PAGING_NOTE,
|
|
36
|
+
inputSchema: paging_js_1.pagingInput,
|
|
12
37
|
outputSchema: misc_js_1.liveviewListOutputSchema,
|
|
13
38
|
annotations: safety_js_1.READ_ONLY,
|
|
14
|
-
}, async () => {
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
return (0, responses_js_1.formatSuccess)(data);
|
|
18
|
-
}
|
|
19
|
-
catch (err) {
|
|
20
|
-
return (0, responses_js_1.formatError)(err);
|
|
21
|
-
}
|
|
39
|
+
}, async ({ limit, offset }) => {
|
|
40
|
+
const data = await client.get("/liveviews");
|
|
41
|
+
return (0, responses_js_1.formatSuccess)((0, paging_js_1.pageOf)(data, limit, offset));
|
|
22
42
|
});
|
|
23
43
|
server.registerTool("protect_get_liveview", {
|
|
24
44
|
description: "Get details for a specific live view by ID. Returns: id, modelKey, name, isDefault, isGlobal, layout, owner, slots (each slot: cameras string[], cycleMode, cycleInterval). The full slot list is needed when updating because PATCH replaces the slots array.",
|
|
25
|
-
inputSchema: { id:
|
|
45
|
+
inputSchema: { id: (0, params_js_1.resourceId)().describe("Liveview ID") },
|
|
26
46
|
outputSchema: misc_js_1.liveviewOutputSchema,
|
|
27
47
|
annotations: safety_js_1.READ_ONLY,
|
|
28
48
|
}, async ({ id }) => {
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
return (0, responses_js_1.formatSuccess)(data);
|
|
32
|
-
}
|
|
33
|
-
catch (err) {
|
|
34
|
-
return (0, responses_js_1.formatError)(err);
|
|
35
|
-
}
|
|
49
|
+
const data = await client.get((0, url_js_1.safePath) `/liveviews/${id}`);
|
|
50
|
+
return (0, responses_js_1.formatSuccess)(data);
|
|
36
51
|
});
|
|
37
52
|
if (readOnly)
|
|
38
53
|
return;
|
|
39
54
|
server.registerTool("protect_create_liveview", {
|
|
40
|
-
description: "Create a new live view",
|
|
55
|
+
description: "Create a new live view. Returns: the created liveview (same fields as protect_get_liveview).",
|
|
41
56
|
inputSchema: {
|
|
42
|
-
settings:
|
|
43
|
-
|
|
44
|
-
.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)"),
|
|
45
|
-
dryRun: zod_1.z
|
|
46
|
-
.boolean()
|
|
47
|
-
.optional()
|
|
48
|
-
.describe("If true, return what would happen without making changes"),
|
|
57
|
+
settings: liveviewBody.describe("Full liveview object (all fields required)"),
|
|
58
|
+
dryRun: (0, safety_js_1.dryRunInput)(),
|
|
49
59
|
},
|
|
50
60
|
outputSchema: misc_js_1.liveviewOutputSchema,
|
|
51
61
|
annotations: safety_js_1.WRITE,
|
|
52
62
|
}, async ({ settings, dryRun }) => {
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
return (0, safety_js_1.formatDryRun)("POST", "/liveviews", settings);
|
|
56
|
-
}
|
|
57
|
-
const data = await client.post("/liveviews", settings);
|
|
58
|
-
return (0, responses_js_1.formatSuccess)(data);
|
|
59
|
-
}
|
|
60
|
-
catch (err) {
|
|
61
|
-
return (0, responses_js_1.formatError)(err);
|
|
63
|
+
if (dryRun) {
|
|
64
|
+
return (0, safety_js_1.formatDryRun)("POST", "/liveviews", settings);
|
|
62
65
|
}
|
|
66
|
+
const data = await client.post("/liveviews", settings);
|
|
67
|
+
return (0, responses_js_1.formatSuccess)(data);
|
|
63
68
|
});
|
|
64
69
|
server.registerTool("protect_update_liveview", {
|
|
65
|
-
description: "Update an existing live view (full replacement via PUT-style PATCH — include all fields)",
|
|
70
|
+
description: "Update an existing live view (full replacement via PUT-style PATCH — include all fields). Returns: the updated liveview.",
|
|
66
71
|
inputSchema: {
|
|
67
|
-
id:
|
|
68
|
-
settings:
|
|
69
|
-
|
|
70
|
-
.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)"),
|
|
71
|
-
dryRun: zod_1.z
|
|
72
|
-
.boolean()
|
|
73
|
-
.optional()
|
|
74
|
-
.describe("If true, return what would happen without making changes"),
|
|
72
|
+
id: (0, params_js_1.resourceId)().describe("Liveview ID"),
|
|
73
|
+
settings: liveviewBody.describe("Full liveview object (all fields required — fetch it with protect_get_liveview first)"),
|
|
74
|
+
dryRun: (0, safety_js_1.dryRunInput)(),
|
|
75
75
|
},
|
|
76
76
|
outputSchema: misc_js_1.liveviewOutputSchema,
|
|
77
|
-
annotations: safety_js_1.
|
|
77
|
+
annotations: safety_js_1.DESTRUCTIVE,
|
|
78
78
|
}, async ({ id, settings, dryRun }) => {
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
}
|
|
83
|
-
const data = await client.patch((0, url_js_1.safePath) `/liveviews/${id}`, settings);
|
|
84
|
-
return (0, responses_js_1.formatSuccess)(data);
|
|
79
|
+
// The body is a full liveview object, so it carries its own id. A
|
|
80
|
+
// mismatch means the caller copied the wrong liveview's body.
|
|
81
|
+
if (settings.id !== id) {
|
|
82
|
+
throw new Error(`settings.id (${settings.id}) does not match the liveview being updated (${id})`);
|
|
85
83
|
}
|
|
86
|
-
|
|
87
|
-
return (0,
|
|
84
|
+
if (dryRun) {
|
|
85
|
+
return (0, safety_js_1.formatDryRun)("PATCH", (0, url_js_1.safePath) `/liveviews/${id}`, settings);
|
|
88
86
|
}
|
|
87
|
+
const data = await client.patch((0, url_js_1.safePath) `/liveviews/${id}`, settings);
|
|
88
|
+
return (0, responses_js_1.formatSuccess)(data);
|
|
89
89
|
});
|
|
90
90
|
}
|
package/dist/tools/pos.d.ts
CHANGED
|
@@ -1,10 +1,12 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import type { ToolRegistrar } from "./registrar.js";
|
|
2
2
|
import { ProtectClient } from "../client.js";
|
|
3
3
|
/**
|
|
4
4
|
* Point-of-sale event ingestion (added in Protect 7.3.x).
|
|
5
5
|
*
|
|
6
|
-
* `lineItems` entries use `.
|
|
7
|
-
* item schema collapsed — the request sample shows `{ title, quantity }`, but
|
|
6
|
+
* `lineItems` entries use `.loose()` because the 7.3.70 docs render the
|
|
7
|
+
* item schema collapsed — the request sample shows `{ title, quantity: 1 }`, but
|
|
8
8
|
* an undocumented extra field should reach the API rather than be stripped.
|
|
9
|
+
* Since the item constraints are not published, `quantity` is only required
|
|
10
|
+
* to be positive (fractional quantities such as weights are allowed).
|
|
9
11
|
*/
|
|
10
|
-
export declare function registerPosTools(server:
|
|
12
|
+
export declare function registerPosTools(server: ToolRegistrar, client: ProtectClient, readOnly: boolean): void;
|
package/dist/tools/pos.js
CHANGED
|
@@ -5,13 +5,16 @@ const zod_1 = require("zod");
|
|
|
5
5
|
const responses_js_1 = require("../utils/responses.js");
|
|
6
6
|
const safety_js_1 = require("../utils/safety.js");
|
|
7
7
|
const url_js_1 = require("../utils/url.js");
|
|
8
|
+
const params_js_1 = require("../utils/params.js");
|
|
8
9
|
const pos_js_1 = require("../schemas/pos.js");
|
|
9
10
|
/**
|
|
10
11
|
* Point-of-sale event ingestion (added in Protect 7.3.x).
|
|
11
12
|
*
|
|
12
|
-
* `lineItems` entries use `.
|
|
13
|
-
* item schema collapsed — the request sample shows `{ title, quantity }`, but
|
|
13
|
+
* `lineItems` entries use `.loose()` because the 7.3.70 docs render the
|
|
14
|
+
* item schema collapsed — the request sample shows `{ title, quantity: 1 }`, but
|
|
14
15
|
* an undocumented extra field should reach the API rather than be stripped.
|
|
16
|
+
* Since the item constraints are not published, `quantity` is only required
|
|
17
|
+
* to be positive (fractional quantities such as weights are allowed).
|
|
15
18
|
*/
|
|
16
19
|
function registerPosTools(server, client, readOnly) {
|
|
17
20
|
if (readOnly)
|
|
@@ -19,9 +22,9 @@ function registerPosTools(server, client, readOnly) {
|
|
|
19
22
|
const lineItemSchema = zod_1.z
|
|
20
23
|
.object({
|
|
21
24
|
title: zod_1.z.string().describe("Line item name"),
|
|
22
|
-
quantity: zod_1.z.number().
|
|
25
|
+
quantity: zod_1.z.number().positive().describe("Quantity purchased (> 0)"),
|
|
23
26
|
})
|
|
24
|
-
.
|
|
27
|
+
.loose();
|
|
25
28
|
server.registerTool("protect_ingest_pos_transaction", {
|
|
26
29
|
description: "Record a point-of-sale transaction as a camera event so the transaction " +
|
|
27
30
|
"details can be overlaid on recorded footage. Requires write access to the " +
|
|
@@ -33,7 +36,7 @@ function registerPosTools(server, client, readOnly) {
|
|
|
33
36
|
"processed for this camera; retry shortly. " +
|
|
34
37
|
"Returns: created (boolean), eventId (string).",
|
|
35
38
|
inputSchema: {
|
|
36
|
-
id:
|
|
39
|
+
id: (0, params_js_1.resourceId)().describe("Camera ID to attach the transaction event to"),
|
|
37
40
|
type: zod_1.z.enum(["sale", "refund"]).describe("Transaction type"),
|
|
38
41
|
externalId: zod_1.z
|
|
39
42
|
.string()
|
|
@@ -59,7 +62,7 @@ function registerPosTools(server, client, readOnly) {
|
|
|
59
62
|
id: zod_1.z.string().optional().describe("Store/location ID"),
|
|
60
63
|
name: zod_1.z.string().optional().describe("Store/location name"),
|
|
61
64
|
})
|
|
62
|
-
.
|
|
65
|
+
.loose()
|
|
63
66
|
.optional()
|
|
64
67
|
.describe("Point-of-sale location"),
|
|
65
68
|
paymentTypes: zod_1.z
|
|
@@ -78,24 +81,16 @@ function registerPosTools(server, client, readOnly) {
|
|
|
78
81
|
"out-of-range value with a 400 and clamps an in-skew value to now. " +
|
|
79
82
|
"This tool forwards the value unchanged rather than pre-checking it, " +
|
|
80
83
|
"because the window is relative to NVR time, not this process clock."),
|
|
81
|
-
dryRun:
|
|
82
|
-
.boolean()
|
|
83
|
-
.optional()
|
|
84
|
-
.describe("If true, return what would happen without making changes"),
|
|
84
|
+
dryRun: (0, safety_js_1.dryRunInput)(),
|
|
85
85
|
},
|
|
86
86
|
outputSchema: pos_js_1.posTransactionOutputSchema,
|
|
87
87
|
annotations: safety_js_1.WRITE,
|
|
88
88
|
}, async ({ id, dryRun, ...transaction }) => {
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
return (0, safety_js_1.formatDryRun)("POST", path, transaction);
|
|
93
|
-
}
|
|
94
|
-
const data = await client.post(path, transaction);
|
|
95
|
-
return (0, responses_js_1.formatSuccess)(data);
|
|
96
|
-
}
|
|
97
|
-
catch (err) {
|
|
98
|
-
return (0, responses_js_1.formatError)(err);
|
|
89
|
+
const path = (0, url_js_1.safePath) `/pos/cameras/${id}/transactions`;
|
|
90
|
+
if (dryRun) {
|
|
91
|
+
return (0, safety_js_1.formatDryRun)("POST", path, transaction);
|
|
99
92
|
}
|
|
93
|
+
const data = await client.post(path, transaction);
|
|
94
|
+
return (0, responses_js_1.formatSuccess)(data);
|
|
100
95
|
});
|
|
101
96
|
}
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
import type { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
|
|
2
|
+
/**
|
|
3
|
+
* What a tool module may do with the server: register tools, nothing else.
|
|
4
|
+
* Modules get this rather than the McpServer, so they cannot reach the
|
|
5
|
+
* registration paths that would bypass instrumentTools (the experimental
|
|
6
|
+
* task API, RegisteredTool.update(), the deprecated tool()).
|
|
7
|
+
*/
|
|
8
|
+
export type ToolRegistrar = Pick<McpServer, "registerTool">;
|
|
9
|
+
/**
|
|
10
|
+
* A registrar that applies these rules to every tool it registers, so no tool
|
|
11
|
+
* module can forget them:
|
|
12
|
+
*
|
|
13
|
+
* - The outputSchema is published as a loose object (see looseOutput): a raw
|
|
14
|
+
* shape would publish `additionalProperties: false`, and SDK clients would
|
|
15
|
+
* reject any field Protect adds.
|
|
16
|
+
* - A write tool that takes `dryRun` also declares the preview fields in its
|
|
17
|
+
* outputSchema, so its dry-run result validates and is documented.
|
|
18
|
+
* - Handlers run with their call's abort signal in scope, so ProtectClient
|
|
19
|
+
* aborts the Protect request (and subscriptions close their WebSocket) when
|
|
20
|
+
* the MCP client cancels the call.
|
|
21
|
+
* - In read-only mode, registering anything but a read-only tool fails at
|
|
22
|
+
* startup. Modules also return before their write tools; this catches a
|
|
23
|
+
* write tool placed above that return.
|
|
24
|
+
* - A handler that throws returns an error result (formatError), so tool
|
|
25
|
+
* modules need no try/catch of their own.
|
|
26
|
+
* - A result too large for one stdio message becomes an error, instead of a
|
|
27
|
+
* message the client cannot read (which ends its session).
|
|
28
|
+
* - Each tool gets a human-readable title derived from its name, and by
|
|
29
|
+
* default `openWorldHint: false`: every tool talks to one known console,
|
|
30
|
+
* not to an open set of external systems.
|
|
31
|
+
*/
|
|
32
|
+
export declare function instrumentTools(server: McpServer, readOnly?: boolean): ToolRegistrar;
|
|
33
|
+
/** "protect_get_rtsp_streams" → "Get RTSP streams". */
|
|
34
|
+
export declare function toolTitle(name: string): string;
|
|
@@ -0,0 +1,103 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.instrumentTools = instrumentTools;
|
|
4
|
+
exports.toolTitle = toolTitle;
|
|
5
|
+
const zod_1 = require("zod");
|
|
6
|
+
const client_js_1 = require("../client.js");
|
|
7
|
+
const common_js_1 = require("../schemas/common.js");
|
|
8
|
+
const responses_js_1 = require("../utils/responses.js");
|
|
9
|
+
const limits_js_1 = require("../utils/limits.js");
|
|
10
|
+
/**
|
|
11
|
+
* A registrar that applies these rules to every tool it registers, so no tool
|
|
12
|
+
* module can forget them:
|
|
13
|
+
*
|
|
14
|
+
* - The outputSchema is published as a loose object (see looseOutput): a raw
|
|
15
|
+
* shape would publish `additionalProperties: false`, and SDK clients would
|
|
16
|
+
* reject any field Protect adds.
|
|
17
|
+
* - A write tool that takes `dryRun` also declares the preview fields in its
|
|
18
|
+
* outputSchema, so its dry-run result validates and is documented.
|
|
19
|
+
* - Handlers run with their call's abort signal in scope, so ProtectClient
|
|
20
|
+
* aborts the Protect request (and subscriptions close their WebSocket) when
|
|
21
|
+
* the MCP client cancels the call.
|
|
22
|
+
* - In read-only mode, registering anything but a read-only tool fails at
|
|
23
|
+
* startup. Modules also return before their write tools; this catches a
|
|
24
|
+
* write tool placed above that return.
|
|
25
|
+
* - A handler that throws returns an error result (formatError), so tool
|
|
26
|
+
* modules need no try/catch of their own.
|
|
27
|
+
* - A result too large for one stdio message becomes an error, instead of a
|
|
28
|
+
* message the client cannot read (which ends its session).
|
|
29
|
+
* - Each tool gets a human-readable title derived from its name, and by
|
|
30
|
+
* default `openWorldHint: false`: every tool talks to one known console,
|
|
31
|
+
* not to an open set of external systems.
|
|
32
|
+
*/
|
|
33
|
+
function instrumentTools(server, readOnly = false) {
|
|
34
|
+
const registerTool = (name, config, handler) => {
|
|
35
|
+
if (readOnly && config.annotations?.readOnlyHint !== true) {
|
|
36
|
+
throw new Error(`${name} is a write tool registered in read-only mode`);
|
|
37
|
+
}
|
|
38
|
+
// outputSchema is destructured out so the inner call infers its own
|
|
39
|
+
// output type rather than the caller's generic one. The SDK also accepts
|
|
40
|
+
// Zod 3 schemas; this server uses Zod 4 throughout, so narrow to that.
|
|
41
|
+
const { outputSchema, ...rest } = config;
|
|
42
|
+
const output = outputSchema;
|
|
43
|
+
const input = config.inputSchema;
|
|
44
|
+
const bound = async (...args) => {
|
|
45
|
+
// The SDK passes the call's `extra` (with its abort signal) last.
|
|
46
|
+
const extra = args.at(-1);
|
|
47
|
+
let result;
|
|
48
|
+
try {
|
|
49
|
+
result = await (0, client_js_1.withCallSignal)(extra?.signal, () => handler(...args));
|
|
50
|
+
}
|
|
51
|
+
catch (err) {
|
|
52
|
+
return (0, responses_js_1.formatError)(err);
|
|
53
|
+
}
|
|
54
|
+
return fitsOneMessage(result);
|
|
55
|
+
};
|
|
56
|
+
return server.registerTool(name, {
|
|
57
|
+
...rest,
|
|
58
|
+
...(output === undefined ? {} : { outputSchema: (0, common_js_1.looseOutput)(withPreview(output, input)) }),
|
|
59
|
+
title: config.title ?? toolTitle(name),
|
|
60
|
+
annotations: { openWorldHint: false, ...config.annotations },
|
|
61
|
+
}, bound); // the one cast: bound has handler's call shape
|
|
62
|
+
};
|
|
63
|
+
return { registerTool };
|
|
64
|
+
}
|
|
65
|
+
function withPreview(outputSchema, inputSchema) {
|
|
66
|
+
const takesDryRun = inputSchema !== undefined && !(inputSchema instanceof zod_1.z.ZodType) && "dryRun" in inputSchema;
|
|
67
|
+
if (!takesDryRun)
|
|
68
|
+
return outputSchema;
|
|
69
|
+
if (outputSchema instanceof zod_1.z.ZodObject)
|
|
70
|
+
return { ...common_js_1.dryRunPreviewFields, ...outputSchema.shape };
|
|
71
|
+
if (outputSchema instanceof zod_1.z.ZodType)
|
|
72
|
+
return outputSchema; // looseOutput rejects it
|
|
73
|
+
return { ...common_js_1.dryRunPreviewFields, ...outputSchema };
|
|
74
|
+
}
|
|
75
|
+
/**
|
|
76
|
+
* The result itself, or an error when its serialised form would exceed what
|
|
77
|
+
* one stdio message carries. Measured on the final result: escaping makes a
|
|
78
|
+
* payload under the per-kind ceilings in limits.ts larger than it looks.
|
|
79
|
+
*/
|
|
80
|
+
function fitsOneMessage(result) {
|
|
81
|
+
const bytes = Buffer.byteLength(JSON.stringify(result), "utf8");
|
|
82
|
+
return bytes <= limits_js_1.MAX_RESULT_BYTES
|
|
83
|
+
? result
|
|
84
|
+
: (0, responses_js_1.formatError)(new Error(`Result too large to send in one MCP message (${bytes.toString()} bytes); narrow the request, or page a list tool with limit and offset`));
|
|
85
|
+
}
|
|
86
|
+
// Words in tool names that read wrongly in plain sentence case.
|
|
87
|
+
const TITLE_WORDS = {
|
|
88
|
+
rtsp: "RTSP",
|
|
89
|
+
ptz: "PTZ",
|
|
90
|
+
nvrs: "NVRs",
|
|
91
|
+
ulp: "ULP",
|
|
92
|
+
pos: "POS",
|
|
93
|
+
goto: "go to",
|
|
94
|
+
};
|
|
95
|
+
/** "protect_get_rtsp_streams" → "Get RTSP streams". */
|
|
96
|
+
function toolTitle(name) {
|
|
97
|
+
const title = name
|
|
98
|
+
.replace(/^protect_/, "")
|
|
99
|
+
.split("_")
|
|
100
|
+
.map((word) => TITLE_WORDS[word] ?? word)
|
|
101
|
+
.join(" ");
|
|
102
|
+
return title.charAt(0).toUpperCase() + title.slice(1);
|
|
103
|
+
}
|
|
@@ -1,3 +1,14 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import type { ToolRegistrar } from "./registrar.js";
|
|
2
2
|
import { ProtectClient } from "../client.js";
|
|
3
|
-
|
|
3
|
+
/**
|
|
4
|
+
* Each device message carries a full device object, so a busy site can emit
|
|
5
|
+
* enough in 30s to flood the MCP client's context. Stop listening at the cap.
|
|
6
|
+
*/
|
|
7
|
+
export declare const MAX_SUBSCRIPTION_MESSAGES = 200;
|
|
8
|
+
/**
|
|
9
|
+
* The most JSON text one listen returns, about 25k tokens: the scale at which
|
|
10
|
+
* MCP hosts commonly cap or warn about a tool result. The count cap alone
|
|
11
|
+
* allowed results several times that size.
|
|
12
|
+
*/
|
|
13
|
+
export declare const MAX_SUBSCRIPTION_TEXT_CHARS = 100000;
|
|
14
|
+
export declare function registerSubscriptionTools(server: ToolRegistrar, client: ProtectClient): void;
|