@owine/unifi-protect-mcp 3.0.0 → 3.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/README.md +29 -15
- package/dist/client.d.ts +17 -0
- package/dist/client.js +87 -15
- package/dist/config.d.ts +1 -0
- package/dist/config.js +4 -0
- package/dist/index.js +21 -0
- package/dist/instructions.js +1 -0
- package/dist/schemas/devices.d.ts +4 -0
- package/dist/schemas/devices.js +1 -0
- package/dist/schemas/misc.d.ts +64 -0
- package/dist/schemas/misc.js +31 -2
- package/dist/tools/arm-profiles.js +1 -1
- package/dist/tools/cameras.js +3 -3
- package/dist/tools/device-actions.js +19 -0
- package/dist/tools/devices.js +11 -11
- package/dist/tools/external-arm-profiles.d.ts +9 -0
- package/dist/tools/external-arm-profiles.js +182 -0
- package/dist/tools/index.js +2 -0
- package/dist/tools/liveviews.js +1 -1
- package/dist/tools/subscriptions.d.ts +8 -0
- package/dist/tools/subscriptions.js +32 -3
- package/dist/tools/system.js +1 -1
- package/dist/tools/users.js +2 -2
- package/dist/utils/limits.d.ts +21 -0
- package/dist/utils/limits.js +22 -1
- package/dist/version.d.ts +4 -2
- package/dist/version.js +4 -2
- package/package.json +11 -11
|
@@ -84,6 +84,25 @@ function registerDeviceActionTools(server, client, readOnly) {
|
|
|
84
84
|
const data = await client.post((0, url_js_1.safePath) `/speakers/${id}/test-sound`, body);
|
|
85
85
|
return (0, responses_js_1.formatSuccess)(data);
|
|
86
86
|
});
|
|
87
|
+
server.registerTool("protect_stop_speaker_siren", {
|
|
88
|
+
description: "Stop a siren playing on a speaker. Requires confirm: true: it silences an alarm siren. Returns HTTP 400 if the speaker is offline or its firmware is too old for sirens.",
|
|
89
|
+
inputSchema: {
|
|
90
|
+
id: (0, params_js_1.resourceId)().describe("Speaker ID"),
|
|
91
|
+
confirm: STOP_SIREN.input,
|
|
92
|
+
dryRun: (0, safety_js_1.dryRunInput)(),
|
|
93
|
+
},
|
|
94
|
+
annotations: safety_js_1.DESTRUCTIVE,
|
|
95
|
+
}, async ({ id, confirm, dryRun }) => {
|
|
96
|
+
const path = (0, url_js_1.safePath) `/speakers/${id}/stop-siren`;
|
|
97
|
+
if (dryRun) {
|
|
98
|
+
return (0, safety_js_1.formatDryRun)("POST", path);
|
|
99
|
+
}
|
|
100
|
+
const denied = STOP_SIREN.check(confirm);
|
|
101
|
+
if (denied)
|
|
102
|
+
return denied;
|
|
103
|
+
const data = await client.post(path);
|
|
104
|
+
return (0, responses_js_1.formatSuccess)(data);
|
|
105
|
+
});
|
|
87
106
|
// --- Relays ---
|
|
88
107
|
server.registerTool("protect_activate_relay_output", {
|
|
89
108
|
description: "Control a relay output. Relays commonly drive gates, door strikes and garage doors, so this requires confirm: true. state (\"on\"/\"off\") is required: the API's toggle mode is not exposed because a retried toggle would flip the output back. Read the current output state with protect_get_relay. When state is \"on\", pulseDuration auto-turns off after the given milliseconds.",
|
package/dist/tools/devices.js
CHANGED
|
@@ -24,7 +24,7 @@ const DEVICES = [
|
|
|
24
24
|
plural: "lights",
|
|
25
25
|
label: "Light",
|
|
26
26
|
hint: "Known fields: name (string), isLightForceEnabled (boolean), lightModeSettings (object with mode, enableAt), lightDeviceSettings (object with isIndicatorEnabled, pirDuration, pirSensitivity, ledLevel)",
|
|
27
|
-
returns: "id, modelKey, name, mac, state, type, guid, lightModeSettings (mode, enableAt), lightDeviceSettings (isIndicatorEnabled, pirDuration, pirSensitivity, ledLevel), isDark, isLightOn, isLightForceEnabled, lastMotion, isPirMotionDetected, camera (7.
|
|
27
|
+
returns: "id, modelKey, name, mac, state, type, guid, lightModeSettings (mode, enableAt), lightDeviceSettings (isIndicatorEnabled, pirDuration, pirSensitivity, ledLevel), isDark, isLightOn, isLightForceEnabled, lastMotion, isPirMotionDetected, camera (7.4.99 docs)",
|
|
28
28
|
},
|
|
29
29
|
{
|
|
30
30
|
urlPath: "sensors",
|
|
@@ -33,7 +33,7 @@ const DEVICES = [
|
|
|
33
33
|
plural: "sensors",
|
|
34
34
|
label: "Sensor",
|
|
35
35
|
hint: "Known fields: name (string), lightSettings / humiditySettings / temperatureSettings (objects: isEnabled, margin, lowThreshold, highThreshold), motionSettings / glassBreakSettings (objects: isEnabled, sensitivity, sensitivityWhenArmed), alarmSettings (object: isEnabled), scheduleMode (\"always\" | \"when_armed\"), armProfileIds (array of up to 32 strings, or null), hasCustomSensitivityWhenArmed (boolean). leakSettings is read-only. Returns HTTP 409 if a setting is not supported by this sensor",
|
|
36
|
-
returns: "id, modelKey, name, mac, state, type, guid, mountType, featureFlags (temperature, humidity, light, motion, waterLeak, open, tamper, smoke — each {channelCount}), batteryStatus (percentage, isLow), stats (light, humidity, temperature — each {value, status}), lightSettings / humiditySettings / temperatureSettings (isEnabled, margin, lowThreshold, highThreshold), isOpened, openStatusChangedAt, isMotionDetected, motionDetectedAt, motionSettings / glassBreakSettings (isEnabled, sensitivity, sensitivityWhenArmed), scheduleMode, armProfileIds, hasCustomSensitivityWhenArmed, alarmTriggeredAt, alarmSettings, leakDetectedAt, externalLeakDetectedAt, leakSettings (isInternalEnabled, isExternalEnabled), tamperingDetectedAt, wirelessConnectionState (signalState: signalQuality, signalStrength; batteryStatus: percentage, isLow; bridge) (7.
|
|
36
|
+
returns: "id, modelKey, name, mac, state, type, guid, mountType, featureFlags (temperature, humidity, light, motion, waterLeak, open, tamper, smoke — each {channelCount}), batteryStatus (percentage, isLow), stats (light, humidity, temperature — each {value, status}), lightSettings / humiditySettings / temperatureSettings (isEnabled, margin, lowThreshold, highThreshold), isOpened, openStatusChangedAt, isMotionDetected, motionDetectedAt, motionSettings / glassBreakSettings (isEnabled, sensitivity, sensitivityWhenArmed), scheduleMode, armProfileIds, hasCustomSensitivityWhenArmed, alarmTriggeredAt, alarmSettings, leakDetectedAt, externalLeakDetectedAt, leakSettings (isInternalEnabled, isExternalEnabled), tamperingDetectedAt, isMountedIncorrectly, wirelessConnectionState (signalState: signalQuality, signalStrength; batteryStatus: percentage, isLow; bridge) (7.4.99 docs)",
|
|
37
37
|
},
|
|
38
38
|
{
|
|
39
39
|
urlPath: "chimes",
|
|
@@ -41,7 +41,7 @@ const DEVICES = [
|
|
|
41
41
|
plural: "chimes",
|
|
42
42
|
label: "Chime",
|
|
43
43
|
hint: "Known fields: name (string), cameraIds (array of doorbell camera IDs linked to the chime), ringSettings (array of objects: cameraId, volume, ringtoneId, repeatTimes)",
|
|
44
|
-
returns: "id, modelKey, name, mac, state, type, guid, cameraIds (array of camera IDs), ringSettings (array of objects: cameraId, volume, ringtoneId, repeatTimes) — verified live 7.
|
|
44
|
+
returns: "id, modelKey, name, mac, state, type, guid, cameraIds (array of camera IDs), ringSettings (array of objects: cameraId, volume, ringtoneId, repeatTimes) — verified live 7.4.99",
|
|
45
45
|
},
|
|
46
46
|
{
|
|
47
47
|
urlPath: "viewers",
|
|
@@ -49,7 +49,7 @@ const DEVICES = [
|
|
|
49
49
|
plural: "viewers",
|
|
50
50
|
label: "Viewer",
|
|
51
51
|
hint: "Known fields: name (string), liveview (string, liveview ID to display, or null)",
|
|
52
|
-
returns: "id, modelKey, name, mac, state, type, guid, liveview, streamLimit (7.
|
|
52
|
+
returns: "id, modelKey, name, mac, state, type, guid, liveview, streamLimit (7.4.99 docs)",
|
|
53
53
|
},
|
|
54
54
|
{
|
|
55
55
|
urlPath: "sirens",
|
|
@@ -59,7 +59,7 @@ const DEVICES = [
|
|
|
59
59
|
plural: "sirens",
|
|
60
60
|
label: "Siren",
|
|
61
61
|
hint: "Known fields: name (string), volume (integer 1-100), ledSettings (object with isEnabled boolean)",
|
|
62
|
-
returns: "id, modelKey, name, mac, state, type, guid, volume, ledSettings (isEnabled), sirenStatus (isActive, activatedAt, duration), connectionType, wirelessConnectionState (signalState: signalQuality, signalStrength; batteryStatus: percentage, isLow; bridge) (7.
|
|
62
|
+
returns: "id, modelKey, name, mac, state, type, guid, volume, ledSettings (isEnabled), sirenStatus (isActive, activatedAt, duration), connectionType, wirelessConnectionState (signalState: signalQuality, signalStrength; batteryStatus: percentage, isLow; bridge) (7.4.99 docs)",
|
|
63
63
|
},
|
|
64
64
|
{
|
|
65
65
|
urlPath: "fobs",
|
|
@@ -68,7 +68,7 @@ const DEVICES = [
|
|
|
68
68
|
plural: "fobs",
|
|
69
69
|
label: "Fob",
|
|
70
70
|
hint: "Known fields: name (string). armControlSettings is not documented as patchable; sending it requires confirm: true",
|
|
71
|
-
returns: "id, modelKey, name, mac, state, type, guid, awayState, buttonLabels, featureFlags (buttons[], hasKeypad), armControlSettings (enabled, armProfileId, nightProfileId), keypadSettings (beepEnabled, beepVolume), wirelessConnectionState (signalState: signalQuality, signalStrength; batteryStatus: percentage, isLow; bridge) (7.
|
|
71
|
+
returns: "id, modelKey, name, mac, state, type, guid, awayState, buttonLabels, featureFlags (buttons[], hasKeypad), armControlSettings (enabled, armProfileId, nightProfileId), keypadSettings (beepEnabled, beepVolume), wirelessConnectionState (signalState: signalQuality, signalStrength; batteryStatus: percentage, isLow; bridge) (7.4.99 docs)",
|
|
72
72
|
},
|
|
73
73
|
{
|
|
74
74
|
urlPath: "relays",
|
|
@@ -76,7 +76,7 @@ const DEVICES = [
|
|
|
76
76
|
plural: "relays",
|
|
77
77
|
label: "Relay",
|
|
78
78
|
hint: "Known fields: name (string), ledSettings (object with isEnabled boolean)",
|
|
79
|
-
returns: "id, modelKey, name, mac, state, type, guid, ledSettings (isEnabled), outputs (array of objects: id, name, type, delay, pulseDuration, state, rebootState — id is the outputId for protect_activate_relay_output), inputs (array of objects: id, name, state, actionTrigger, actionType, actionOutputId), wirelessConnectionState (signalState: signalQuality, signalStrength; batteryStatus: percentage, isLow; bridge) (7.
|
|
79
|
+
returns: "id, modelKey, name, mac, state, type, guid, ledSettings (isEnabled), outputs (array of objects: id, name, type, delay, pulseDuration, state, rebootState — id is the outputId for protect_activate_relay_output), inputs (array of objects: id, name, state, actionTrigger, actionType, actionOutputId), wirelessConnectionState (signalState: signalQuality, signalStrength; batteryStatus: percentage, isLow; bridge) (7.4.99 docs)",
|
|
80
80
|
},
|
|
81
81
|
{
|
|
82
82
|
urlPath: "speakers",
|
|
@@ -84,7 +84,7 @@ const DEVICES = [
|
|
|
84
84
|
plural: "speakers",
|
|
85
85
|
label: "Speaker",
|
|
86
86
|
hint: "Known fields: name (string), volume (integer 0-100), micVolume (integer 0-100), isMicEnabled (boolean)",
|
|
87
|
-
returns: "id, modelKey, name, mac, state, type, guid, volume, micVolume, isMicEnabled, speakerState (status, mode), featureFlags (hasMic) (7.
|
|
87
|
+
returns: "id, modelKey, name, mac, state, type, guid, volume, micVolume, isMicEnabled, speakerState (status, mode), featureFlags (hasMic) (7.4.99 docs)",
|
|
88
88
|
},
|
|
89
89
|
{
|
|
90
90
|
urlPath: "bridges",
|
|
@@ -92,7 +92,7 @@ const DEVICES = [
|
|
|
92
92
|
plural: "bridges",
|
|
93
93
|
label: "Bridge",
|
|
94
94
|
hint: "Known fields: name (string)",
|
|
95
|
-
returns: "id, modelKey, name, mac, state, type, guid, platform, clients (array of MACs), maxClients (7.
|
|
95
|
+
returns: "id, modelKey, name, mac, state, type, guid, platform, clients (array of MACs), maxClients (7.4.99 docs)",
|
|
96
96
|
},
|
|
97
97
|
{
|
|
98
98
|
urlPath: "link-stations",
|
|
@@ -101,7 +101,7 @@ const DEVICES = [
|
|
|
101
101
|
label: "Link station",
|
|
102
102
|
note: "Link stations are non-alarm-hub gateways (use protect_list_alarm_hubs for alarm hubs).",
|
|
103
103
|
hint: "Known fields: name (string)",
|
|
104
|
-
returns: "id, modelKey (\"linkstation\"), name, mac, state, type, guid, isAlarmHub, ledSettings (isEnabled), lastEvent, alarmHub (armed, battery, buckboost, connector, cover, deviceTamperStatus (e.g. \"tampered\"), currentMeterChannelStatus, currentMeterStatus, inputPower, poeout, powerMeter, output and input (maps keyed by channel), inputTerminalStatus, outputTerminalStatus, emergencyTerminalStatus, auxiliaryPowerTerminalStatus), threadState (network: status, role, networkName, channel, panId, extendedPanId, joinedDeviceCount, errorReason, lastUpdatedAt) (7.
|
|
104
|
+
returns: "id, modelKey (\"linkstation\"), name, mac, state, type, guid, isAlarmHub, ledSettings (isEnabled), lastEvent, alarmHub (armed, battery, buckboost, connector, cover, deviceTamperStatus (e.g. \"tampered\"), currentMeterChannelStatus, currentMeterStatus, inputPower, poeout, powerMeter, output and input (maps keyed by channel), inputTerminalStatus, outputTerminalStatus, emergencyTerminalStatus, auxiliaryPowerTerminalStatus), threadState (network: status, role, networkName, channel, panId, extendedPanId, joinedDeviceCount, errorReason, lastUpdatedAt) (7.4.99 docs)",
|
|
105
105
|
},
|
|
106
106
|
{
|
|
107
107
|
urlPath: "alarm-hubs",
|
|
@@ -109,7 +109,7 @@ const DEVICES = [
|
|
|
109
109
|
plural: "alarm_hubs",
|
|
110
110
|
label: "Alarm hub",
|
|
111
111
|
hint: "Known fields: name (string)",
|
|
112
|
-
returns: "id, modelKey (\"linkstation\"), name, mac, state, type, guid, isAlarmHub, ledSettings (isEnabled), lastEvent, alarmHub (armed, battery, buckboost, connector, cover, deviceTamperStatus (e.g. \"tampered\"), currentMeterChannelStatus, currentMeterStatus, inputPower, poeout, powerMeter, output and input (maps keyed by channel), inputTerminalStatus, outputTerminalStatus, emergencyTerminalStatus, auxiliaryPowerTerminalStatus), threadState (network: status, role, networkName, channel, panId, extendedPanId, joinedDeviceCount, errorReason, lastUpdatedAt) (7.
|
|
112
|
+
returns: "id, modelKey (\"linkstation\"), name, mac, state, type, guid, isAlarmHub, ledSettings (isEnabled), lastEvent, alarmHub (armed, battery, buckboost, connector, cover, deviceTamperStatus (e.g. \"tampered\"), currentMeterChannelStatus, currentMeterStatus, inputPower, poeout, powerMeter, output and input (maps keyed by channel), inputTerminalStatus, outputTerminalStatus, emergencyTerminalStatus, auxiliaryPowerTerminalStatus), threadState (network: status, role, networkName, channel, panId, extendedPanId, joinedDeviceCount, errorReason, lastUpdatedAt) (7.4.99 docs)",
|
|
113
113
|
},
|
|
114
114
|
];
|
|
115
115
|
function registerDeviceCrud(server, client, cfg, readOnly) {
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
import type { ToolRegistrar } from "./registrar.js";
|
|
2
|
+
import { ProtectClient } from "../client.js";
|
|
3
|
+
/**
|
|
4
|
+
* Settings that change what the alarm protects, when, or who can disarm it:
|
|
5
|
+
* a longer delay leaves the site unprotected for longer, and clearing the PIN
|
|
6
|
+
* lets anyone disarm the profile.
|
|
7
|
+
*/
|
|
8
|
+
export declare const EXTERNAL_ARM_PROFILE_CONFIRM_FIELDS: readonly ["schedule", "alarm_ids", "activation_delay", "deactivation_delay", "pin_code"];
|
|
9
|
+
export declare function registerExternalArmProfileTools(server: ToolRegistrar, client: ProtectClient, readOnly: boolean): void;
|
|
@@ -0,0 +1,182 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.EXTERNAL_ARM_PROFILE_CONFIRM_FIELDS = void 0;
|
|
4
|
+
exports.registerExternalArmProfileTools = registerExternalArmProfileTools;
|
|
5
|
+
const zod_1 = require("zod");
|
|
6
|
+
const responses_js_1 = require("../utils/responses.js");
|
|
7
|
+
const safety_js_1 = require("../utils/safety.js");
|
|
8
|
+
const url_js_1 = require("../utils/url.js");
|
|
9
|
+
const paging_js_1 = require("../utils/paging.js");
|
|
10
|
+
const params_js_1 = require("../utils/params.js");
|
|
11
|
+
const misc_js_1 = require("../schemas/misc.js");
|
|
12
|
+
/**
|
|
13
|
+
* Arm profiles of the global Alarm Manager, which Protect 7.4 proxies under
|
|
14
|
+
* /external/arm-profiles. They are not the local alarm manager's profiles
|
|
15
|
+
* (arm-profiles.ts): the API passes the alarm manager's snake_case records
|
|
16
|
+
* through, delays are in seconds rather than milliseconds, and arming is per
|
|
17
|
+
* profile instead of through one "current" profile. Verified live on 7.4.99:
|
|
18
|
+
* with the global Alarm Manager on, the local /arm-profiles routes answer 400.
|
|
19
|
+
*/
|
|
20
|
+
/** The longest activation or deactivation delay the API accepts, in seconds. */
|
|
21
|
+
const MAX_DELAY_SECONDS = 3600;
|
|
22
|
+
const AVAILABILITY = "Only available when the console uses the global Alarm Manager (protect_list_nvrs featureFlags.useExternalAlarmManager is true).";
|
|
23
|
+
const delay = (which) => zod_1.z.number().int().min(0).max(MAX_DELAY_SECONDS).describe(`${which} delay in seconds (0-${MAX_DELAY_SECONDS.toString()})`);
|
|
24
|
+
const schedule = zod_1.z
|
|
25
|
+
.record(zod_1.z.string(), zod_1.z.unknown())
|
|
26
|
+
.refine((o) => Object.keys(o).length > 0, "schedule must include at least one day")
|
|
27
|
+
.describe('Weekly schedule keyed by day (sun, mon, tue, wed, thu, fri, sat). Each day is { type: "intervals", entries: [{ start: { hour, minute }, end: { hour, minute } }] }, the shape the 7.4.99 docs show; other day types are not documented.');
|
|
28
|
+
const alarmIds = zod_1.z.array(zod_1.z.string().min(1)).describe("Alarm Manager alarm IDs to associate with the profile");
|
|
29
|
+
const pinCode = zod_1.z.string().describe("PIN code for the profile");
|
|
30
|
+
const RETURNS = "id, title, state (e.g. disarmed, breached), state_set_at, state_promotion_due_at, alarm_ids[], schedule (sun..sat, each {type, entries[{start, end: {hour, minute}}]}), activation_delay, deactivation_delay (seconds), has_pin (the PIN is not returned), active_breach_id, created_at, updated_at, created_by, updated_by (7.4.99 docs)";
|
|
31
|
+
/**
|
|
32
|
+
* Settings that change what the alarm protects, when, or who can disarm it:
|
|
33
|
+
* a longer delay leaves the site unprotected for longer, and clearing the PIN
|
|
34
|
+
* lets anyone disarm the profile.
|
|
35
|
+
*/
|
|
36
|
+
exports.EXTERNAL_ARM_PROFILE_CONFIRM_FIELDS = [
|
|
37
|
+
"schedule",
|
|
38
|
+
"alarm_ids",
|
|
39
|
+
"activation_delay",
|
|
40
|
+
"deactivation_delay",
|
|
41
|
+
"pin_code",
|
|
42
|
+
];
|
|
43
|
+
// Each gate's confirm input and handler check share one action text.
|
|
44
|
+
const DELETE_PROFILE = (0, safety_js_1.confirmGate)("delete the external arm profile");
|
|
45
|
+
const DISARM_PROFILE = (0, safety_js_1.confirmGate)("disarm the external arm profile");
|
|
46
|
+
/** The request body: only the fields the caller set. */
|
|
47
|
+
function definedFields(fields) {
|
|
48
|
+
return Object.fromEntries(Object.entries(fields).filter(([, value]) => value !== undefined));
|
|
49
|
+
}
|
|
50
|
+
function registerExternalArmProfileTools(server, client, readOnly) {
|
|
51
|
+
server.registerTool("protect_list_external_arm_profiles", {
|
|
52
|
+
description: `List the global Alarm Manager's arm profiles. ${AVAILABILITY} Returns array; each profile: ${RETURNS}.${paging_js_1.PAGING_NOTE}`,
|
|
53
|
+
inputSchema: paging_js_1.pagingInput,
|
|
54
|
+
outputSchema: misc_js_1.externalArmProfileListOutputSchema,
|
|
55
|
+
annotations: safety_js_1.READ_ONLY,
|
|
56
|
+
}, async ({ limit, offset }) => {
|
|
57
|
+
const data = await client.get("/external/arm-profiles");
|
|
58
|
+
return (0, responses_js_1.formatSuccess)((0, paging_js_1.pageOf)(data, limit, offset));
|
|
59
|
+
});
|
|
60
|
+
server.registerTool("protect_get_external_arm_profile", {
|
|
61
|
+
description: `Get one of the global Alarm Manager's arm profiles by ID. ${AVAILABILITY} Returns: ${RETURNS}.`,
|
|
62
|
+
inputSchema: {
|
|
63
|
+
id: (0, params_js_1.resourceId)().describe("External arm profile ID"),
|
|
64
|
+
},
|
|
65
|
+
outputSchema: misc_js_1.externalArmProfileOutputSchema,
|
|
66
|
+
annotations: safety_js_1.READ_ONLY,
|
|
67
|
+
}, async ({ id }) => {
|
|
68
|
+
const data = await client.get((0, url_js_1.safePath) `/external/arm-profiles/${id}`);
|
|
69
|
+
return (0, responses_js_1.formatSuccess)(data);
|
|
70
|
+
});
|
|
71
|
+
if (readOnly)
|
|
72
|
+
return;
|
|
73
|
+
server.registerTool("protect_create_external_arm_profile", {
|
|
74
|
+
description: `Create an arm profile in the global Alarm Manager. ${AVAILABILITY} Returns the created profile.`,
|
|
75
|
+
inputSchema: {
|
|
76
|
+
title: zod_1.z.string().min(1).describe("Profile title"),
|
|
77
|
+
activation_delay: delay("Activation").optional(),
|
|
78
|
+
deactivation_delay: delay("Deactivation").optional(),
|
|
79
|
+
schedule: schedule.optional(),
|
|
80
|
+
alarm_ids: alarmIds.optional(),
|
|
81
|
+
pin_code: pinCode.nullable().optional(),
|
|
82
|
+
dryRun: (0, safety_js_1.dryRunInput)(),
|
|
83
|
+
},
|
|
84
|
+
outputSchema: misc_js_1.externalArmProfileOutputSchema,
|
|
85
|
+
annotations: safety_js_1.WRITE,
|
|
86
|
+
}, async ({ dryRun, ...fields }) => {
|
|
87
|
+
const body = definedFields(fields);
|
|
88
|
+
if (dryRun) {
|
|
89
|
+
return (0, safety_js_1.formatDryRun)("POST", "/external/arm-profiles", body);
|
|
90
|
+
}
|
|
91
|
+
const data = await client.post("/external/arm-profiles", body);
|
|
92
|
+
return (0, responses_js_1.formatSuccess)(data);
|
|
93
|
+
});
|
|
94
|
+
server.registerTool("protect_update_external_arm_profile", {
|
|
95
|
+
description: `Update an arm profile in the global Alarm Manager (partial update via PATCH): omitted fields stay unchanged, and null clears a nullable one. Changing ${exports.EXTERNAL_ARM_PROFILE_CONFIRM_FIELDS.join(", ")} affects alarm coverage and requires confirm: true. ${AVAILABILITY} Returns the updated profile.`,
|
|
96
|
+
inputSchema: {
|
|
97
|
+
id: (0, params_js_1.resourceId)().describe("External arm profile ID"),
|
|
98
|
+
settings: zod_1.z
|
|
99
|
+
.object({
|
|
100
|
+
title: zod_1.z.string().min(1).describe("Profile title (cannot be cleared)"),
|
|
101
|
+
activation_delay: delay("Activation").nullable(),
|
|
102
|
+
deactivation_delay: delay("Deactivation").nullable(),
|
|
103
|
+
schedule: schedule.nullable(),
|
|
104
|
+
alarm_ids: alarmIds.nullable().describe("Alarm IDs to associate; null or [] clears them"),
|
|
105
|
+
pin_code: pinCode.nullable().describe("PIN code; null clears it"),
|
|
106
|
+
})
|
|
107
|
+
.partial()
|
|
108
|
+
.loose()
|
|
109
|
+
.refine((o) => Object.keys(o).length > 0, "settings must include at least one field")
|
|
110
|
+
.describe("Partial profile settings to update"),
|
|
111
|
+
confirm: (0, safety_js_1.confirmInput)(`change ${exports.EXTERNAL_ARM_PROFILE_CONFIRM_FIELDS.join(", ")}`),
|
|
112
|
+
dryRun: (0, safety_js_1.dryRunInput)(),
|
|
113
|
+
},
|
|
114
|
+
outputSchema: misc_js_1.externalArmProfileOutputSchema,
|
|
115
|
+
annotations: safety_js_1.DESTRUCTIVE,
|
|
116
|
+
}, async ({ id, settings, confirm, dryRun }) => {
|
|
117
|
+
const path = (0, url_js_1.safePath) `/external/arm-profiles/${id}`;
|
|
118
|
+
if (dryRun) {
|
|
119
|
+
return (0, safety_js_1.formatDryRun)("PATCH", path, settings);
|
|
120
|
+
}
|
|
121
|
+
const denied = (0, safety_js_1.requireConfirmationForFields)(settings, exports.EXTERNAL_ARM_PROFILE_CONFIRM_FIELDS, confirm);
|
|
122
|
+
if (denied)
|
|
123
|
+
return denied;
|
|
124
|
+
const data = await client.patch(path, settings);
|
|
125
|
+
return (0, responses_js_1.formatSuccess)(data);
|
|
126
|
+
});
|
|
127
|
+
server.registerTool("protect_delete_external_arm_profile", {
|
|
128
|
+
description: `Delete an arm profile from the global Alarm Manager. ${AVAILABILITY} Requires confirm: true.`,
|
|
129
|
+
inputSchema: {
|
|
130
|
+
id: (0, params_js_1.resourceId)().describe("External arm profile ID"),
|
|
131
|
+
confirm: DELETE_PROFILE.input,
|
|
132
|
+
dryRun: (0, safety_js_1.dryRunInput)(),
|
|
133
|
+
},
|
|
134
|
+
annotations: safety_js_1.DESTRUCTIVE,
|
|
135
|
+
}, async ({ id, confirm, dryRun }) => {
|
|
136
|
+
const path = (0, url_js_1.safePath) `/external/arm-profiles/${id}`;
|
|
137
|
+
if (dryRun) {
|
|
138
|
+
return (0, safety_js_1.formatDryRun)("DELETE", path);
|
|
139
|
+
}
|
|
140
|
+
const denied = DELETE_PROFILE.check(confirm);
|
|
141
|
+
if (denied)
|
|
142
|
+
return denied;
|
|
143
|
+
const data = await client.delete(path);
|
|
144
|
+
return (0, responses_js_1.formatSuccess)(data);
|
|
145
|
+
});
|
|
146
|
+
server.registerTool("protect_arm_external_arm_profile", {
|
|
147
|
+
description: `Arm one of the global Alarm Manager's arm profiles. ${AVAILABILITY} Returns the profile with its new state.`,
|
|
148
|
+
inputSchema: {
|
|
149
|
+
id: (0, params_js_1.resourceId)().describe("External arm profile ID"),
|
|
150
|
+
dryRun: (0, safety_js_1.dryRunInput)(),
|
|
151
|
+
},
|
|
152
|
+
outputSchema: misc_js_1.externalArmProfileOutputSchema,
|
|
153
|
+
annotations: safety_js_1.WRITE,
|
|
154
|
+
}, async ({ id, dryRun }) => {
|
|
155
|
+
const path = (0, url_js_1.safePath) `/external/arm-profiles/${id}/actions/arm`;
|
|
156
|
+
if (dryRun) {
|
|
157
|
+
return (0, safety_js_1.formatDryRun)("POST", path);
|
|
158
|
+
}
|
|
159
|
+
const data = await client.post(path);
|
|
160
|
+
return (0, responses_js_1.formatSuccess)(data);
|
|
161
|
+
});
|
|
162
|
+
server.registerTool("protect_disarm_external_arm_profile", {
|
|
163
|
+
description: `Disarm one of the global Alarm Manager's arm profiles, leaving what it protects unguarded until it is re-armed; requires confirm: true. ${AVAILABILITY} Returns the profile with its new state.`,
|
|
164
|
+
inputSchema: {
|
|
165
|
+
id: (0, params_js_1.resourceId)().describe("External arm profile ID"),
|
|
166
|
+
confirm: DISARM_PROFILE.input,
|
|
167
|
+
dryRun: (0, safety_js_1.dryRunInput)(),
|
|
168
|
+
},
|
|
169
|
+
outputSchema: misc_js_1.externalArmProfileOutputSchema,
|
|
170
|
+
annotations: safety_js_1.DESTRUCTIVE,
|
|
171
|
+
}, async ({ id, confirm, dryRun }) => {
|
|
172
|
+
const path = (0, url_js_1.safePath) `/external/arm-profiles/${id}/actions/disarm`;
|
|
173
|
+
if (dryRun) {
|
|
174
|
+
return (0, safety_js_1.formatDryRun)("POST", path);
|
|
175
|
+
}
|
|
176
|
+
const denied = DISARM_PROFILE.check(confirm);
|
|
177
|
+
if (denied)
|
|
178
|
+
return denied;
|
|
179
|
+
const data = await client.post(path);
|
|
180
|
+
return (0, responses_js_1.formatSuccess)(data);
|
|
181
|
+
});
|
|
182
|
+
}
|
package/dist/tools/index.js
CHANGED
|
@@ -11,6 +11,7 @@ const files_js_1 = require("./files.js");
|
|
|
11
11
|
const alarm_manager_js_1 = require("./alarm-manager.js");
|
|
12
12
|
const subscriptions_js_1 = require("./subscriptions.js");
|
|
13
13
|
const arm_profiles_js_1 = require("./arm-profiles.js");
|
|
14
|
+
const external_arm_profiles_js_1 = require("./external-arm-profiles.js");
|
|
14
15
|
const users_js_1 = require("./users.js");
|
|
15
16
|
const pos_js_1 = require("./pos.js");
|
|
16
17
|
function registerAllTools(server, client, readOnly) {
|
|
@@ -25,6 +26,7 @@ function registerAllTools(server, client, readOnly) {
|
|
|
25
26
|
(0, files_js_1.registerFileTools)(tools, client, readOnly);
|
|
26
27
|
(0, alarm_manager_js_1.registerAlarmManagerTools)(tools, client, readOnly);
|
|
27
28
|
(0, arm_profiles_js_1.registerArmProfileTools)(tools, client, readOnly);
|
|
29
|
+
(0, external_arm_profiles_js_1.registerExternalArmProfileTools)(tools, client, readOnly);
|
|
28
30
|
(0, users_js_1.registerUserTools)(tools, client);
|
|
29
31
|
(0, pos_js_1.registerPosTools)(tools, client, readOnly);
|
|
30
32
|
}
|
package/dist/tools/liveviews.js
CHANGED
|
@@ -32,7 +32,7 @@ const liveviewBody = zod_1.z
|
|
|
32
32
|
.loose();
|
|
33
33
|
function registerLiveviewTools(server, client, readOnly) {
|
|
34
34
|
server.registerTool("protect_list_liveviews", {
|
|
35
|
-
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.4.99-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
36
|
inputSchema: paging_js_1.pagingInput,
|
|
37
37
|
outputSchema: misc_js_1.liveviewListOutputSchema,
|
|
38
38
|
annotations: safety_js_1.READ_ONLY,
|
|
@@ -1,5 +1,13 @@
|
|
|
1
1
|
import type { ToolRegistrar } from "./registrar.js";
|
|
2
2
|
import { ProtectClient } from "../client.js";
|
|
3
|
+
/**
|
|
4
|
+
* The model keys /subscribe/updates accepts. Verified live on 7.4.99: the
|
|
5
|
+
* console validates them against exactly this list, case-sensitively, so
|
|
6
|
+
* "linkStation" works where the resources' own modelKey "linkstation" is
|
|
7
|
+
* refused. An unknown key opens the socket, sends an error frame and closes it
|
|
8
|
+
* (code 4500), so they are checked here before connecting.
|
|
9
|
+
*/
|
|
10
|
+
export declare const UPDATE_MODEL_KEYS: readonly ["nvr", "camera", "chime", "light", "viewer", "speaker", "bridge", "sensor", "siren", "fob", "relay", "aiProcessor", "aiPort", "linkStation", "event", "externalArmProfile"];
|
|
3
11
|
/**
|
|
4
12
|
* Each device message carries a full device object, so a busy site can emit
|
|
5
13
|
* enough in 30s to flood the MCP client's context. Stop listening at the cap.
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
"use strict";
|
|
2
2
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
-
exports.MAX_SUBSCRIPTION_TEXT_CHARS = exports.MAX_SUBSCRIPTION_MESSAGES = void 0;
|
|
3
|
+
exports.MAX_SUBSCRIPTION_TEXT_CHARS = exports.MAX_SUBSCRIPTION_MESSAGES = exports.UPDATE_MODEL_KEYS = void 0;
|
|
4
4
|
exports.registerSubscriptionTools = registerSubscriptionTools;
|
|
5
5
|
const zod_1 = require("zod");
|
|
6
6
|
const client_js_1 = require("../client.js");
|
|
@@ -13,6 +13,17 @@ const durationSchema = zod_1.z
|
|
|
13
13
|
.max(30)
|
|
14
14
|
.default(5)
|
|
15
15
|
.describe("Seconds to listen (1-30, default 5)");
|
|
16
|
+
/**
|
|
17
|
+
* The model keys /subscribe/updates accepts. Verified live on 7.4.99: the
|
|
18
|
+
* console validates them against exactly this list, case-sensitively, so
|
|
19
|
+
* "linkStation" works where the resources' own modelKey "linkstation" is
|
|
20
|
+
* refused. An unknown key opens the socket, sends an error frame and closes it
|
|
21
|
+
* (code 4500), so they are checked here before connecting.
|
|
22
|
+
*/
|
|
23
|
+
exports.UPDATE_MODEL_KEYS = [
|
|
24
|
+
"nvr", "camera", "chime", "light", "viewer", "speaker", "bridge", "sensor", "siren",
|
|
25
|
+
"fob", "relay", "aiProcessor", "aiPort", "linkStation", "event", "externalArmProfile",
|
|
26
|
+
];
|
|
16
27
|
/**
|
|
17
28
|
* Each device message carries a full device object, so a busy site can emit
|
|
18
29
|
* enough in 30s to flood the MCP client's context. Stop listening at the cap.
|
|
@@ -175,7 +186,7 @@ function listenResult(data) {
|
|
|
175
186
|
}
|
|
176
187
|
function registerSubscriptionTools(server, client) {
|
|
177
188
|
server.registerTool("protect_subscribe_devices", {
|
|
178
|
-
description: `Connect to the device update WebSocket and collect messages for a specified duration (1-30s). Returns {messages, duration, error?, truncated?}; stops early with truncated=true after ${exports.MAX_SUBSCRIPTION_MESSAGES.toString()} messages or ${exports.MAX_SUBSCRIPTION_TEXT_CHARS.toLocaleString("en-US")} characters of JSON, whichever comes first. A connection that fails before any message arrives returns an error. Each message is an envelope {type, item} where item is a device resource carrying id and modelKey (camera, light, sensor, nvr, …). The 7.
|
|
189
|
+
description: `Connect to the device update WebSocket and collect messages for a specified duration (1-30s). Returns {messages, duration, error?, truncated?}; stops early with truncated=true after ${exports.MAX_SUBSCRIPTION_MESSAGES.toString()} messages or ${exports.MAX_SUBSCRIPTION_TEXT_CHARS.toLocaleString("en-US")} characters of JSON, whichever comes first. A connection that fails before any message arrives returns an error. Each message is an envelope {type, item} where item is a device resource carrying id and modelKey (camera, light, sensor, nvr, …). The 7.4.99 docs show type 'add' with the full device object as item; other message types and whether updates carry only changed fields are not documented — inspect the captured messages. Use to detect device state changes or newly adopted devices. Not available over Cloud Connector (api.ui.com).`,
|
|
179
190
|
inputSchema: { duration: durationSchema },
|
|
180
191
|
outputSchema: misc_js_1.subscriptionOutputSchema,
|
|
181
192
|
annotations: safety_js_1.READ_ONLY,
|
|
@@ -184,7 +195,7 @@ function registerSubscriptionTools(server, client) {
|
|
|
184
195
|
return listenResult(data);
|
|
185
196
|
});
|
|
186
197
|
server.registerTool("protect_subscribe_events", {
|
|
187
|
-
description: `Connect to the Protect event WebSocket and collect messages for a specified duration (1-30s). Returns {messages, duration, error?, truncated?}; stops early with truncated=true after ${exports.MAX_SUBSCRIPTION_MESSAGES.toString()} messages or ${exports.MAX_SUBSCRIPTION_TEXT_CHARS.toLocaleString("en-US")} characters of JSON, whichever comes first. A connection that fails before any message arrives returns an error. Each message is an envelope {type: 'add' | 'update', item: {id, modelKey: 'event', type (event kind, e.g. 'ring'), start, end (epoch ms), device (ID of the camera/sensor that raised it)}} (7.
|
|
198
|
+
description: `Connect to the Protect event WebSocket and collect messages for a specified duration (1-30s). Returns {messages, duration, error?, truncated?}; stops early with truncated=true after ${exports.MAX_SUBSCRIPTION_MESSAGES.toString()} messages or ${exports.MAX_SUBSCRIPTION_TEXT_CHARS.toLocaleString("en-US")} characters of JSON, whichever comes first. A connection that fails before any message arrives returns an error. Each message is an envelope {type: 'add' | 'update', item: {id, modelKey: 'event', type (event kind, e.g. 'ring'), start, end (epoch ms), device (ID of the camera/sensor that raised it)}} (7.4.99 docs). The full set of event kinds and any further event fields are not documented — inspect the captured messages. Not available over Cloud Connector (api.ui.com).`,
|
|
188
199
|
inputSchema: { duration: durationSchema },
|
|
189
200
|
outputSchema: misc_js_1.subscriptionOutputSchema,
|
|
190
201
|
annotations: safety_js_1.READ_ONLY,
|
|
@@ -192,4 +203,22 @@ function registerSubscriptionTools(server, client) {
|
|
|
192
203
|
const data = await collectMessages(client, "/subscribe/events", duration);
|
|
193
204
|
return listenResult(data);
|
|
194
205
|
});
|
|
206
|
+
server.registerTool("protect_subscribe_updates", {
|
|
207
|
+
description: `Connect to the model update WebSocket for the chosen model keys and collect messages for a specified duration (1-30s). Returns {messages, duration, error?, truncated?}; stops early with truncated=true after ${exports.MAX_SUBSCRIPTION_MESSAGES.toString()} messages or ${exports.MAX_SUBSCRIPTION_TEXT_CHARS.toLocaleString("en-US")} characters of JSON, whichever comes first. A connection that fails before any message arrives returns an error. Each message is an envelope {type, item} where item carries id and modelKey. The 7.4.99 docs show type 'add' with the full object; verified live on 7.4.99, type 'update' carries only the changed fields. The docs say removals are sent too (except for events) and that a message may cover several model IDs, but show neither — inspect the captured messages. Unlike protect_subscribe_devices, this also covers events, external arm profiles (global Alarm Manager) and AI processors/ports, filtered to the keys you pass. Not available over Cloud Connector (api.ui.com).`,
|
|
208
|
+
inputSchema: {
|
|
209
|
+
modelKeys: zod_1.z
|
|
210
|
+
.array(zod_1.z.enum(exports.UPDATE_MODEL_KEYS))
|
|
211
|
+
.min(1)
|
|
212
|
+
.describe('Model keys to subscribe to. Note "linkStation" here, although link station resources report modelKey "linkstation".'),
|
|
213
|
+
duration: durationSchema,
|
|
214
|
+
},
|
|
215
|
+
outputSchema: misc_js_1.subscriptionOutputSchema,
|
|
216
|
+
annotations: safety_js_1.READ_ONLY,
|
|
217
|
+
}, async ({ modelKeys, duration }) => {
|
|
218
|
+
// The keys are enum values, so they need no encoding; the console
|
|
219
|
+
// accepts them comma-separated.
|
|
220
|
+
const keys = [...new Set(modelKeys)].join(",");
|
|
221
|
+
const data = await collectMessages(client, `/subscribe/updates?modelKeys=${keys}`, duration);
|
|
222
|
+
return listenResult(data);
|
|
223
|
+
});
|
|
195
224
|
}
|
package/dist/tools/system.js
CHANGED
|
@@ -14,7 +14,7 @@ function registerSystemTools(server, client) {
|
|
|
14
14
|
return (0, responses_js_1.formatSuccess)(data);
|
|
15
15
|
});
|
|
16
16
|
server.registerTool("protect_list_nvrs", {
|
|
17
|
-
description: "Get the NVR (Network Video Recorder) console info. NOTE: despite the name, the Protect Integration API's /nvrs endpoint returns a SINGLE NVR object, not an array. Returns: id, modelKey, name, type (e.g. UNVR-PRO), guid, mac, doorbellSettings (defaultMessageText, defaultMessageResetTimeoutMs, customMessages[], customImages[] (preview, sprite)), armMode (status, armProfileId, armedAt, willBeArmedAt, breachDetectedAt, breachEventCount, breachTriggerEventId, breachEventId).",
|
|
17
|
+
description: "Get the NVR (Network Video Recorder) console info. NOTE: despite the name, the Protect Integration API's /nvrs endpoint returns a SINGLE NVR object, not an array. Returns: id, modelKey, name, type (e.g. UNVR-PRO), guid, mac, featureFlags (useExternalAlarmManager: true when the global Alarm Manager owns arming, so the external arm-profile tools apply), doorbellSettings (defaultMessageText, defaultMessageResetTimeoutMs, customMessages[], customImages[] (preview, sprite)), armMode (status, armProfileId, armedAt, willBeArmedAt, breachDetectedAt, breachEventCount, breachTriggerEventId, breachEventId).",
|
|
18
18
|
outputSchema: misc_js_1.nvrOutputSchema,
|
|
19
19
|
annotations: safety_js_1.READ_ONLY,
|
|
20
20
|
}, async () => {
|
package/dist/tools/users.js
CHANGED
|
@@ -9,7 +9,7 @@ const params_js_1 = require("../utils/params.js");
|
|
|
9
9
|
const misc_js_1 = require("../schemas/misc.js");
|
|
10
10
|
function registerUserTools(server, client) {
|
|
11
11
|
server.registerTool("protect_list_users", {
|
|
12
|
-
description: "List all Protect users (filtered by the API key's access permissions). Returns array; each user includes (Integration API 7.
|
|
12
|
+
description: "List all Protect users (filtered by the API key's access permissions). Returns array; each user includes (Integration API 7.4.99-verified): id, modelKey, name, firstName, lastName, email, ucoreUserId. The Integration API does NOT expose roles, permissions, login history, groups, or notification settings." + paging_js_1.PAGING_NOTE,
|
|
13
13
|
inputSchema: paging_js_1.pagingInput,
|
|
14
14
|
outputSchema: misc_js_1.userListOutputSchema,
|
|
15
15
|
annotations: safety_js_1.READ_ONLY,
|
|
@@ -27,7 +27,7 @@ function registerUserTools(server, client) {
|
|
|
27
27
|
return (0, responses_js_1.formatSuccess)(data);
|
|
28
28
|
});
|
|
29
29
|
server.registerTool("protect_list_ulp_users", {
|
|
30
|
-
description: "List UniFi Identity (ULP) users — per the API docs, those with enrolled credentials (NFC cards, fingerprints). Returns array; each ULP user includes (Integration API 7.
|
|
30
|
+
description: "List UniFi Identity (ULP) users — per the API docs, those with enrolled credentials (NFC cards, fingerprints). Returns array; each ULP user includes (Integration API 7.4.99-verified): id, modelKey, firstName, lastName, fullName, email (empty string for service accounts), status (e.g. ACTIVE). Enrolled-credential detail (NFC cards, fingerprints) is NOT exposed by this API surface." + paging_js_1.PAGING_NOTE,
|
|
31
31
|
inputSchema: paging_js_1.pagingInput,
|
|
32
32
|
outputSchema: misc_js_1.ulpUserListOutputSchema,
|
|
33
33
|
annotations: safety_js_1.READ_ONLY,
|
package/dist/utils/limits.d.ts
CHANGED
|
@@ -25,9 +25,30 @@ export declare const SERVER_READ_BUFFER_BYTES: number;
|
|
|
25
25
|
export declare const MAX_ERROR_BODY_BYTES: number;
|
|
26
26
|
/** Protect answers JSON calls quickly; a longer wait means something is wrong. */
|
|
27
27
|
export declare const REQUEST_TIMEOUT_MS = 25000;
|
|
28
|
+
/**
|
|
29
|
+
* The startup console check. It runs before the stdio handshake, so it must
|
|
30
|
+
* fail well inside the time an MCP host waits for a server to come up;
|
|
31
|
+
* otherwise the host times the server out with no reason in its log.
|
|
32
|
+
*/
|
|
33
|
+
export declare const STARTUP_CHECK_TIMEOUT_MS = 10000;
|
|
28
34
|
/**
|
|
29
35
|
* Snapshots and uploads get the longest budget that still ends before the MCP
|
|
30
36
|
* SDK client's default request timeout. It used to equal that timeout, so a
|
|
31
37
|
* slow snapshot always lost the race.
|
|
32
38
|
*/
|
|
33
39
|
export declare const BINARY_TIMEOUT_MS: number;
|
|
40
|
+
/**
|
|
41
|
+
* Rate limiting. Verified live on 7.4.99: the console caps API calls per
|
|
42
|
+
* second and answers the excess with HTTP 429 and `Retry-After` (in seconds),
|
|
43
|
+
* which a host's burst of parallel tool calls easily reaches. The 429 body is
|
|
44
|
+
* the rate limiter's own ({error, windowMs, limit}), sent before the request
|
|
45
|
+
* reaches Protect, so a rate-limited write was not applied and is retried too.
|
|
46
|
+
*/
|
|
47
|
+
export declare const MAX_RATE_LIMIT_RETRIES = 3;
|
|
48
|
+
/** The wait when a 429 names none: the console's limit window. */
|
|
49
|
+
export declare const DEFAULT_RETRY_AFTER_MS = 1000;
|
|
50
|
+
/**
|
|
51
|
+
* The longest Retry-After waited out. Longer is a real throttle rather than a
|
|
52
|
+
* burst, so the 429 is returned instead of holding the tool call.
|
|
53
|
+
*/
|
|
54
|
+
export declare const MAX_RETRY_AFTER_MS = 5000;
|
package/dist/utils/limits.js
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
"use strict";
|
|
2
2
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
-
exports.BINARY_TIMEOUT_MS = exports.REQUEST_TIMEOUT_MS = exports.MAX_ERROR_BODY_BYTES = exports.SERVER_READ_BUFFER_BYTES = exports.MAX_UPLOAD_BASE64_CHARS = exports.MAX_SNAPSHOT_BYTES = exports.MAX_RESPONSE_BYTES = exports.MAX_RESULT_BYTES = void 0;
|
|
3
|
+
exports.MAX_RETRY_AFTER_MS = exports.DEFAULT_RETRY_AFTER_MS = exports.MAX_RATE_LIMIT_RETRIES = exports.BINARY_TIMEOUT_MS = exports.STARTUP_CHECK_TIMEOUT_MS = exports.REQUEST_TIMEOUT_MS = exports.MAX_ERROR_BODY_BYTES = exports.SERVER_READ_BUFFER_BYTES = exports.MAX_UPLOAD_BASE64_CHARS = exports.MAX_SNAPSHOT_BYTES = exports.MAX_RESPONSE_BYTES = exports.MAX_RESULT_BYTES = void 0;
|
|
4
4
|
const stdio_js_1 = require("@modelcontextprotocol/sdk/shared/stdio.js");
|
|
5
5
|
const protocol_js_1 = require("@modelcontextprotocol/sdk/shared/protocol.js");
|
|
6
6
|
/**
|
|
@@ -60,6 +60,12 @@ exports.SERVER_READ_BUFFER_BYTES = stdio_js_1.STDIO_DEFAULT_MAX_BUFFER_SIZE * RE
|
|
|
60
60
|
exports.MAX_ERROR_BODY_BYTES = 64 * 1024;
|
|
61
61
|
/** Protect answers JSON calls quickly; a longer wait means something is wrong. */
|
|
62
62
|
exports.REQUEST_TIMEOUT_MS = 25_000;
|
|
63
|
+
/**
|
|
64
|
+
* The startup console check. It runs before the stdio handshake, so it must
|
|
65
|
+
* fail well inside the time an MCP host waits for a server to come up;
|
|
66
|
+
* otherwise the host times the server out with no reason in its log.
|
|
67
|
+
*/
|
|
68
|
+
exports.STARTUP_CHECK_TIMEOUT_MS = 10_000;
|
|
63
69
|
/**
|
|
64
70
|
* Time left for the MCP client between our timeout and its own, so the
|
|
65
71
|
* server's error (which names the call, and says whether a write may have
|
|
@@ -72,3 +78,18 @@ const CLIENT_TIMEOUT_MARGIN_MS = 15_000;
|
|
|
72
78
|
* slow snapshot always lost the race.
|
|
73
79
|
*/
|
|
74
80
|
exports.BINARY_TIMEOUT_MS = protocol_js_1.DEFAULT_REQUEST_TIMEOUT_MSEC - CLIENT_TIMEOUT_MARGIN_MS;
|
|
81
|
+
/**
|
|
82
|
+
* Rate limiting. Verified live on 7.4.99: the console caps API calls per
|
|
83
|
+
* second and answers the excess with HTTP 429 and `Retry-After` (in seconds),
|
|
84
|
+
* which a host's burst of parallel tool calls easily reaches. The 429 body is
|
|
85
|
+
* the rate limiter's own ({error, windowMs, limit}), sent before the request
|
|
86
|
+
* reaches Protect, so a rate-limited write was not applied and is retried too.
|
|
87
|
+
*/
|
|
88
|
+
exports.MAX_RATE_LIMIT_RETRIES = 3;
|
|
89
|
+
/** The wait when a 429 names none: the console's limit window. */
|
|
90
|
+
exports.DEFAULT_RETRY_AFTER_MS = 1_000;
|
|
91
|
+
/**
|
|
92
|
+
* The longest Retry-After waited out. Longer is a real throttle rather than a
|
|
93
|
+
* burst, so the 429 is returned instead of holding the tool call.
|
|
94
|
+
*/
|
|
95
|
+
exports.MAX_RETRY_AFTER_MS = 5_000;
|
package/dist/version.d.ts
CHANGED
|
@@ -1,6 +1,8 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* The published package version, reported to MCP clients during initialize.
|
|
3
|
-
* Read at runtime
|
|
4
|
-
* release
|
|
3
|
+
* Read at runtime from package.json, which reads 0.0.0-development in the
|
|
4
|
+
* repo; the release workflow stamps the tag's version into it before packing,
|
|
5
|
+
* so a published tarball reports its own version. Resolves to the package
|
|
6
|
+
* root from both src/ and dist/.
|
|
5
7
|
*/
|
|
6
8
|
export declare const SERVER_VERSION: string;
|
package/dist/version.js
CHANGED
|
@@ -5,7 +5,9 @@ const node_fs_1 = require("node:fs");
|
|
|
5
5
|
const node_path_1 = require("node:path");
|
|
6
6
|
/**
|
|
7
7
|
* The published package version, reported to MCP clients during initialize.
|
|
8
|
-
* Read at runtime
|
|
9
|
-
* release
|
|
8
|
+
* Read at runtime from package.json, which reads 0.0.0-development in the
|
|
9
|
+
* repo; the release workflow stamps the tag's version into it before packing,
|
|
10
|
+
* so a published tarball reports its own version. Resolves to the package
|
|
11
|
+
* root from both src/ and dist/.
|
|
10
12
|
*/
|
|
11
13
|
exports.SERVER_VERSION = JSON.parse((0, node_fs_1.readFileSync)((0, node_path_1.join)(__dirname, "..", "package.json"), "utf8")).version;
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@owine/unifi-protect-mcp",
|
|
3
|
-
"version": "3.
|
|
3
|
+
"version": "3.2.0",
|
|
4
4
|
"description": "MCP server for UniFi Protect Integration API",
|
|
5
5
|
"main": "dist/index.js",
|
|
6
6
|
"bin": {
|
|
@@ -21,9 +21,9 @@
|
|
|
21
21
|
"lint:fix": "eslint . --fix"
|
|
22
22
|
},
|
|
23
23
|
"engines": {
|
|
24
|
-
"node": "^22.19.0 || ^24.0.0"
|
|
24
|
+
"node": "^22.19.0 || ^24.0.0 || ^26.0.0"
|
|
25
25
|
},
|
|
26
|
-
"packageManager": "pnpm@12.
|
|
26
|
+
"packageManager": "pnpm@12.21.0",
|
|
27
27
|
"repository": {
|
|
28
28
|
"type": "git",
|
|
29
29
|
"url": "git+https://github.com/owine/unifi-protect-mcp.git"
|
|
@@ -38,21 +38,21 @@
|
|
|
38
38
|
"author": "owine",
|
|
39
39
|
"license": "MIT",
|
|
40
40
|
"dependencies": {
|
|
41
|
-
"@modelcontextprotocol/sdk": "1.32.
|
|
42
|
-
"undici": "8.12.
|
|
43
|
-
"ws": "8.
|
|
41
|
+
"@modelcontextprotocol/sdk": "1.32.2",
|
|
42
|
+
"undici": "8.12.4",
|
|
43
|
+
"ws": "8.23.0",
|
|
44
44
|
"zod": "4.6.7"
|
|
45
45
|
},
|
|
46
46
|
"devDependencies": {
|
|
47
47
|
"@eslint/js": "10.0.1",
|
|
48
|
-
"@types/node": "
|
|
48
|
+
"@types/node": "26.6.6",
|
|
49
49
|
"@types/ws": "8.18.2",
|
|
50
50
|
"@typescript/native": "npm:typescript@7.0.2",
|
|
51
|
-
"@vitest/coverage-v8": "5.0
|
|
52
|
-
"eslint": "10.
|
|
51
|
+
"@vitest/coverage-v8": "5.1.0",
|
|
52
|
+
"eslint": "10.15.0",
|
|
53
53
|
"lefthook": "2.2.2",
|
|
54
54
|
"typescript": "npm:@typescript/typescript6@6.0.2",
|
|
55
|
-
"typescript-eslint": "8.
|
|
56
|
-
"vitest": "5.0
|
|
55
|
+
"typescript-eslint": "8.72.2",
|
|
56
|
+
"vitest": "5.1.0"
|
|
57
57
|
}
|
|
58
58
|
}
|