rhombus-node-mcp 0.1.55 → 0.1.57

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.
Files changed (108) hide show
  1. package/dist/api/access-control-tool-api.js +231 -32
  2. package/dist/api/alarm-monitoring-tool-api.js +50 -1
  3. package/dist/api/automated-prompts-tool-api.js +9 -17
  4. package/dist/api/camera-policy-tool-api.js +122 -0
  5. package/dist/api/camera-tool-api.js +3 -5
  6. package/dist/api/camera-uptime-tool-api.js +62 -25
  7. package/dist/api/clips-tool-api.js +45 -9
  8. package/dist/api/create-tool-api.js +55 -7
  9. package/dist/api/door-tool-api.js +74 -13
  10. package/dist/api/entity-lookup-tool-api.js +8 -1
  11. package/dist/api/events-tool-api.js +79 -6
  12. package/dist/api/faces-tool-api.js +112 -7
  13. package/dist/api/get-accessible-apps.js +68 -6
  14. package/dist/api/get-entity-tool-api.js +12 -12
  15. package/dist/api/get-org-information-tool-api.js +2 -2
  16. package/dist/api/guest-management-tool-api.js +75 -1
  17. package/dist/api/location-tool-api.js +120 -28
  18. package/dist/api/lpr-tool-api.js +71 -16
  19. package/dist/api/policy-alerts-tool-api.js +7 -3
  20. package/dist/api/report-tool-api.js +4 -1
  21. package/dist/api/rules-tool-api.js +6 -16
  22. package/dist/api/schedule-tool-api.js +107 -0
  23. package/dist/api/search-tool-api.js +5 -13
  24. package/dist/api/update-tool-api.js +96 -1
  25. package/dist/api/user-tool-api.js +58 -13
  26. package/dist/createServer.js +18 -6
  27. package/dist/filtering-utils.js +421 -29
  28. package/dist/index.js +1 -1
  29. package/dist/network/network.js +122 -10
  30. package/dist/network/org-reference-cache.js +74 -0
  31. package/dist/telemetry/tracingProxy.js +4 -0
  32. package/dist/tools/entity-lookup-tool.js +8 -4
  33. package/dist/tools/get-entity-tool.js +46 -21
  34. package/dist/tools/get-org-information-tool.js +36 -3
  35. package/dist/tools/time-conversion-tool.js +65 -37
  36. package/dist/tools/time-tool.js +18 -4
  37. package/dist/tools/user-tool.js +101 -1
  38. package/dist/tools-console/access-anomaly-tool.js +4 -19
  39. package/dist/tools-console/access-control-tool.js +330 -2
  40. package/dist/tools-console/alarm-monitoring-tool.js +73 -1
  41. package/dist/tools-console/analytics-tool.js +4 -2
  42. package/dist/tools-console/automated-prompts-tool.js +2 -0
  43. package/dist/tools-console/badge-timeline-tool.js +4 -21
  44. package/dist/tools-console/camera-policy-tool.js +252 -0
  45. package/dist/tools-console/camera-tool.js +113 -89
  46. package/dist/tools-console/camera-uptime-tool.js +2 -0
  47. package/dist/tools-console/clips-tool.js +23 -31
  48. package/dist/tools-console/count-tool.js +19 -6
  49. package/dist/tools-console/create-camera-policy-tool.js +304 -122
  50. package/dist/tools-console/door-schedule-exception-tool.js +4 -33
  51. package/dist/tools-console/door-tool.js +131 -1
  52. package/dist/tools-console/elements-access-anomaly-tool.js +4 -19
  53. package/dist/tools-console/elements-badge-timeline-tool.js +4 -21
  54. package/dist/tools-console/elements-lost-badge-tool.js +4 -17
  55. package/dist/tools-console/elements-tool.js +4 -29
  56. package/dist/tools-console/events-tool.js +24 -101
  57. package/dist/tools-console/faces-tool.js +255 -27
  58. package/dist/tools-console/guest-management-tool.js +128 -1
  59. package/dist/tools-console/location-tool.js +161 -35
  60. package/dist/tools-console/lost-badge-tool.js +4 -17
  61. package/dist/tools-console/lpr-tool.js +90 -22
  62. package/dist/tools-console/netbox-access-anomaly-tool.js +4 -19
  63. package/dist/tools-console/netbox-badge-timeline-tool.js +4 -21
  64. package/dist/tools-console/netbox-lost-badge-tool.js +4 -17
  65. package/dist/tools-console/netbox-tool.js +4 -29
  66. package/dist/tools-console/onguard-tool.js +4 -30
  67. package/dist/tools-console/person-tracking-tool.js +4 -2
  68. package/dist/tools-console/policy-alerts-tool.js +6 -28
  69. package/dist/tools-console/reboot-cameras-tool.js +23 -3
  70. package/dist/tools-console/report-tool.js +12 -40
  71. package/dist/tools-console/rules-tool.js +2 -0
  72. package/dist/tools-console/schedule-tool.js +190 -0
  73. package/dist/tools-console/search-tool.js +2 -0
  74. package/dist/tools-console/update-tool.js +300 -168
  75. package/dist/tools-console/user-access-trail-tool.js +2 -0
  76. package/dist/tools-console/user-audit-tool.js +2 -0
  77. package/dist/tools-console/video-walls-tool.js +94 -20
  78. package/dist/types/access-anomaly-tool-types.js +22 -2
  79. package/dist/types/access-control-tool-types.js +116 -1
  80. package/dist/types/alarm-monitoring-tool-types.js +42 -1
  81. package/dist/types/badge-timeline-tool-types.js +18 -3
  82. package/dist/types/camera-policy-tool-types.js +104 -0
  83. package/dist/types/camera-tool-types.js +80 -1
  84. package/dist/types/clips-tool-types.js +27 -3
  85. package/dist/types/create-camera-policy-tool-types.js +275 -26
  86. package/dist/types/door-schedule-exception-tool-types.js +20 -6
  87. package/dist/types/door-tool-types.js +36 -4
  88. package/dist/types/entity-lookup-tool-types.js +6 -1
  89. package/dist/types/events-tools-types.js +24 -16
  90. package/dist/types/faces-tools-types.js +106 -3
  91. package/dist/types/get-entity-tool-types.js +76 -12
  92. package/dist/types/guest-management-tool-types.js +103 -1
  93. package/dist/types/location-tool-types.js +100 -14
  94. package/dist/types/lost-badge-tool-types.js +17 -2
  95. package/dist/types/lpr-tool-types.js +47 -8
  96. package/dist/types/onguard-tool-types.js +21 -1
  97. package/dist/types/policy-alerts-tool-types.js +1 -1
  98. package/dist/types/report-tool-types.js +29 -1
  99. package/dist/types/schedule-tool-types.js +370 -0
  100. package/dist/types/schema.js +479 -9
  101. package/dist/types/time-conversion-tool-types.js +3 -4
  102. package/dist/types/update-tool-types.js +40 -18
  103. package/dist/types/user-tool-types.js +44 -1
  104. package/dist/types/video-walls-tool-types.js +30 -2
  105. package/dist/types/zod-schemas.js +5128 -996
  106. package/dist/util.js +62 -3
  107. package/dist/utils/entity-name-match.js +11 -0
  108. package/package.json +3 -1
@@ -3,35 +3,10 @@ import { ApiPayloadSchema, OUTPUT_SCHEMA, TOOL_ARGS, } from "../types/policy-ale
3
3
  import { extractFromToolExtra } from "../util.js";
4
4
  const TOOL_NAME = "policy-alerts-tool";
5
5
  const TOOL_DESCRIPTION = `
6
- Retrieves Rhombus policy alerts. Policy alerts in the Rhombus system are generated based on user-defined alert
7
- policies configured in the Rhombus Console. These policies trigger alerts when specific events occur, such as:
8
- * AI & Computer Vision Events: Based on intelligent video analytics for motion, people, vehicles, facial recognition,
9
- license plate recognition, or unusual behavior.
10
- * Device Status Changes: Like camera disconnections or sensor low battery.
11
- * Physical or Visual Tamper: Detection of physical movement of a device or obstruction of a camera's field of view.
12
- * Access Control Events: Such as unauthorized access attempts in restricted areas.
6
+ Retrieves Rhombus policy alerts alerts generated by user-defined alert policies when configured events occur: AI/computer-vision events (motion, people, vehicles, faces, license plates, unusual behavior), device status changes (disconnection, low battery), physical/visual tamper, and access-control events. Alerts are NOT the same as notifications (only some alerts notify, per user settings). Can also report which alert labels have been seen. Supports filtering by existing/expiring, time range, device UUIDs or location UUIDs — pagination and per-queryType requirements are documented on the parameters.
13
7
 
14
- Alerts are generated on triggers, but are NOT the same as notifications. Only certain alerts generate notifications based on user settings.
15
-
16
- Can inquire about labels that have been seen.
17
-
18
- Please note, this is not an exhaustive list, and there may be other types of triggers or events that generate
19
- policy alerts within the Rhombus system.
20
-
21
- This tool allows you to filter existing alerts by existing/expiring, a specific time range (before or after a timestamp in ISO 8601 format),
22
- by a list of device UUIDs, or by a list of location UUIDs.
23
- You can also specify the maximum number of results to return.
24
- The output is provided in JSON format.
25
-
26
- **Pagination**: Results are paginated and have a maximum page size. If the response includes a \`lastEvaluatedKey\` (for queryType "expiringSoon") or both \`lastTimestampISO\` and \`lastUuid\` (for queryTypes "existing" and "alert-groups"), more results are available. Pass these values back in the next call using the corresponding input parameters (lastEvaluatedKey, or lastTimestampISO and lastUuid) to retrieve the next page. Repeat until the response no longer includes these fields.
27
-
28
- IMPORTANT: The "unhealthy-devices" queryType returns historical alert notifications that were triggered for device health issues.
29
- It does NOT return live/real-time device connection status. If no device health alert policies are configured, or alerts were
30
- dismissed, this may return empty even when devices are offline.
31
-
32
- **To check which devices are currently online/offline, use the get-entity-tool instead.** The get-entity-tool returns the
33
- current state of all devices including their live connection status (the "connected" field). Request all entity types
34
- (CAMERA, DOORBELL_CAMERA, BADGE_READER, etc.) and check the "connected" field on each device to determine which are offline.`;
8
+ IMPORTANT: queryType "unhealthy-devices" returns HISTORICAL device-health alert notifications, not live status it can be empty while devices are offline. To check which devices are currently online/offline, use get-entity-tool instead (filterBy on its "connected" field).
9
+ `;
35
10
  const TOOL_HANDLER = async (args, extra) => {
36
11
  const payload = ApiPayloadSchema.parse(args);
37
12
  const { requestModifiers, sessionId } = extractFromToolExtra(extra);
@@ -71,8 +46,11 @@ const TOOL_HANDLER = async (args, extra) => {
71
46
  };
72
47
  export function createTool(server) {
73
48
  server.registerTool(TOOL_NAME, {
49
+ title: "Policy Alerts",
74
50
  description: TOOL_DESCRIPTION,
75
51
  inputSchema: TOOL_ARGS,
76
52
  outputSchema: OUTPUT_SCHEMA.shape,
53
+ // `dismiss` cannot be undone, which is what destructiveHint is for.
54
+ annotations: { readOnlyHint: false, destructiveHint: true },
77
55
  }, TOOL_HANDLER);
78
56
  }
@@ -1,3 +1,4 @@
1
+ import { z } from "zod";
1
2
  import { isConfirmed, requireConfirmation } from "../utils/confirmation.js";
2
3
  import { rebootCameras } from "../api/reboot-cameras-tool-api.js";
3
4
  import { TOOL_ARGS } from "../types/reboot-cameras-tool-types.js";
@@ -10,12 +11,15 @@ const TOOL_HANDLER = async (args, extra) => {
10
11
  return confirmation;
11
12
  }
12
13
  const cameraRebootData = await rebootCameras(cameraUuids, extra._meta?.requestModifiers, extra.sessionId);
13
- if (!cameraRebootData) {
14
+ if (!cameraRebootData || ("error" in cameraRebootData && cameraRebootData.error)) {
14
15
  return {
16
+ isError: true,
15
17
  content: [
16
18
  {
17
19
  type: "text",
18
- text: "Failed to reboot cameras",
20
+ text: cameraRebootData && "status" in cameraRebootData && cameraRebootData.status
21
+ ? String(cameraRebootData.status)
22
+ : "Failed to reboot cameras",
19
23
  },
20
24
  ],
21
25
  };
@@ -27,8 +31,24 @@ const TOOL_HANDLER = async (args, extra) => {
27
31
  text: JSON.stringify(cameraRebootData),
28
32
  },
29
33
  ],
34
+ structuredContent: cameraRebootData,
30
35
  };
31
36
  };
32
37
  export function createTool(server) {
33
- server.tool(TOOL_NAME, TOOL_DESCRIPTION, TOOL_ARGS, TOOL_HANDLER);
38
+ server.registerTool(TOOL_NAME, {
39
+ title: "Reboot Cameras",
40
+ description: TOOL_DESCRIPTION,
41
+ inputSchema: TOOL_ARGS,
42
+ // The confirmation-needed result goes through createToolTextContent,
43
+ // which sets isError: true — so it legitimately skips output validation.
44
+ outputSchema: {
45
+ status: z
46
+ .string()
47
+ .optional()
48
+ .describe("SUCCESS when every camera rebooted, PARTIAL_SUCCESS when some did, ERROR when none did"),
49
+ successCount: z.number().optional(),
50
+ errorCount: z.number().optional(),
51
+ },
52
+ annotations: { readOnlyHint: false, destructiveHint: true },
53
+ }, TOOL_HANDLER);
34
54
  }
@@ -3,50 +3,20 @@ import { logger } from "../logger.js";
3
3
  import { OUTPUT_SCHEMA, RequestType, TOOL_ARGS, } from "../types/report-tool-types.js";
4
4
  import { extractFromToolExtra } from "../util.js";
5
5
  const TOOL_NAME = "report-tool";
6
+ // Tool descriptions are billed on EVERY LLM call even while the tool is
7
+ // deferred behind hosted tool_search, so this string carries only what the
8
+ // model needs to CHOOSE this tool. Per-requestType behaviour, call ordering,
9
+ // and the people/occupancy strategy live on the requestType parameter
10
+ // description (unbilled until the tool is loaded, still in front of the model
11
+ // when it builds the call). See PERF_MASTER_PLAN P2 #4a.
6
12
  const TOOL_DESCRIPTION = `
7
- **Scope:** This tool returns **aggregated counts and time-series summaries** over specified intervals and scopes. Use **events-tool** when you need raw, event-level data (individual events with timestamps). Use this tool for high-level reports, analytics, and trends—especially over periods of a day or more.
13
+ **Scope:** **aggregated counts and time-series summaries** over specified intervals and scopes (device, location, region, org). Use **events-tool** when you need raw, event-level data (individual events with timestamps). Use this tool for high-level reports, analytics, and trends—especially over periods of a day or more.
8
14
 
9
- **Interval guidance:** A shorter interval (HOURLY instead of DAILY) gives a better representation of data over time. Balance interval and range so you don't request too much data. For ranges spanning a week or so, HOURLY is appropriate.
10
-
11
- ---
12
-
13
- **People / occupancy counting strategy**
14
-
15
- When asked to count people on a camera or at a location, follow this strategy:
16
- 1. **Always call GET_OCCUPANCY_ENABLED_CAMERAS first** to discover which cameras have occupancy counting enabled.
17
- 2. If the target camera IS in the list, call **GET_OCCUPANCY_COUNT_REPORT** for that device. The response will automatically include a \`faceCountEnrichment\` field with the number of unique individuals identified by face recognition in the same time range. Present both data sources: occupancy estimate and unique face count.
18
- 3. If the target camera is NOT in the list, **tell the user** that camera does not have occupancy counting enabled, and list the cameras that do. You can still call GET_SUMMARY_COUNT_REPORT with PEOPLE type — its response will also include \`faceCountEnrichment\` with unique face data as a fallback. If the PEOPLE count returns zero, the response will also include the list of occupancy-enabled cameras and a hint.
19
- 4. When both occupancy data and face recognition data are available, **synthesize both** in your answer (e.g., "Occupancy estimates ~15 people. Face recognition identified 9 unique individuals during this period.").
20
-
21
- **PEOPLE type (in GET_SUMMARY_COUNT_REPORT):** Not a unique person count; it counts people-detection events. Requires people detection to be enabled on the camera. Use for high-level activity trends, not for deduplicated head counts.
22
-
23
- ---
15
+ Covers: people counting and occupancy reports (enriched with unique-face counts), line-crossing / threshold ingress-egress counts, custom LLM event reports and their prompt configurations (e.g. "black dog sightings", "delivery truck arrivals", "parking availability %"), the org audit log of user and admin actions, device diagnostic feeds, and the most recent people-count readings for a device.
24
16
 
25
- **Summary and occupancy**
26
- - **GET_SUMMARY_COUNT_REPORT:** Aggregated counts (people, faces, motion, vehicles, etc.) over time at device, location, or org scope. Interval: minutely, hourly, daily, weekly, monthly, yearly. When called with PEOPLE type at DEVICE scope, the response is automatically enriched with face recognition data.
27
- - **GET_OCCUPANCY_ENABLED_CAMERAS:** List of cameras with occupancy reporting enabled. **Always call this first** before any people/occupancy counting request to verify camera support.
28
- - **GET_OCCUPANCY_COUNT_REPORT:** Occupancy count time series for a specific device over a time range. Response is automatically enriched with face recognition data. If the device does not support occupancy, the response will include a hint and the list of cameras that do.
17
+ Pick one with "requestType". Each type's arguments, response enrichment, and required call ordering (some types must be called first to discover which cameras support a feature) are documented on the requestType parameter.
29
18
 
30
- ---
31
-
32
- **Line crossing**
33
- - **GET_LINE_CROSSING_ENABLED_CAMERAS:** Cameras at a location with line crossing enabled, plus their configs. Call first to see which cameras support threshold crossing reports.
34
- - **GET_THRESHOLD_CROSSING_COUNT_REPORT:** Ingress/egress counts for line crossings over time. Supports human and vehicle detection; bucket size: quarter hour, hour, day, week. Response includes computed metrics: average entries/exits per hour, hour with most entries/exits, busiest hour (with breakdown).
35
-
36
- ---
37
-
38
- **Custom LLM events**
39
- - **FIND_PROMPT_CONFIGURATIONS:** All custom event prompt configurations (e.g. "black dog sightings", "delivery truck arrivals", "parking availability %"). Each has prompt text, UUID, and promptType (COUNT, PERCENT, BOOLEAN). Call first to discover available custom events.
40
- - **GET_CUSTOM_LLM_REPORT:** **This is the PRIMARY way to get custom event reports.** Aggregated time-series for one custom event by prompt UUID. Automatically selects the correct API based on promptType: COUNT (numeric counts), PERCENT (percentages), BOOLEAN (true/false). Intervals: minutely, quarter-hourly, hourly, daily, weekly, monthly. **Always use this for custom event reports, trends, and analytics.** Use FIND_PROMPT_CONFIGURATIONS first to get the promptUuid and promptType.
41
- - **GET_CUSTOM_EVENTS_REPORT:** Raw individual event values only (not aggregated). Use only when you need per-event granularity, not for reports or trends.
42
-
43
- ---
44
-
45
- **Audit and diagnostics**
46
- - **GET_AUDIT_FEED:** Audit log of all user/admin actions in the org over a time range. Returns who did what and when (principalName, targetName, action, displayText).
47
- - **GET_DIAGNOSTIC_FEED:** Device diagnostic events over a time range.
48
- - **GET_THRESHOLD_CROSSING_EVENTS:** Individual line-crossing events (not aggregated counts).
49
- - **GET_PEOPLE_COUNT_EVENTS:** Most recent people count readings for specified devices.
19
+ **Interval guidance:** A shorter interval (HOURLY instead of DAILY) gives a better representation of data over time. Balance interval and range so you don't request too much data. For ranges spanning a week or so, HOURLY is appropriate.
50
20
  `;
51
21
  const TOOL_HANDLER = async (args, extra) => {
52
22
  const { requestType } = args;
@@ -322,8 +292,10 @@ const TOOL_HANDLER = async (args, extra) => {
322
292
  };
323
293
  export function createTool(server) {
324
294
  server.registerTool(TOOL_NAME, {
295
+ title: "Reports",
325
296
  description: TOOL_DESCRIPTION,
326
297
  inputSchema: TOOL_ARGS.shape,
327
298
  outputSchema: OUTPUT_SCHEMA.shape,
299
+ annotations: { readOnlyHint: true },
328
300
  }, TOOL_HANDLER);
329
301
  }
@@ -69,8 +69,10 @@ const TOOL_HANDLER = async (args, _extra) => {
69
69
  };
70
70
  export function createTool(server) {
71
71
  server.registerTool(TOOL_NAME, {
72
+ title: "Automation Rules",
72
73
  description: TOOL_DESCRIPTION,
73
74
  inputSchema: TOOL_ARGS,
74
75
  outputSchema: OUTPUT_SCHEMA.shape,
76
+ annotations: { readOnlyHint: false, destructiveHint: true },
75
77
  }, TOOL_HANDLER);
76
78
  }
@@ -0,0 +1,190 @@
1
+ import { createSchedule, deleteSchedule, findSchedules, getPoliciesUsingSchedule, updateSchedule, } from "../api/schedule-tool-api.js";
2
+ import { INTERVALS_EXAMPLE, OUTPUT_SCHEMA, ScheduleToolRequestType, TOOL_ARGS, WEEKLY_REPEATING_STRATEGY, describeIntervals, parseWeeklyIntervals, } from "../types/schedule-tool-types.js";
3
+ import { createToolStructuredContent, createToolTextContent, extractFromToolExtra, } from "../util.js";
4
+ const TOOL_NAME = "schedule-tool";
5
+ const TOOL_DESCRIPTION = `
6
+ This tool manages Rhombus schedules — the named weekly time windows that policies attach to, e.g. "Business Hours" or "After Hours".
7
+
8
+ Use it to resolve a schedule NAME to the scheduleUuid that other tools require. create-camera-policy-tool needs a scheduleUuid and has no other legitimate source for one, so start here rather than guessing a UUID.
9
+
10
+ It has the following modes of operation, determined by the "requestType" parameter:
11
+ - ${ScheduleToolRequestType.LIST}: List every schedule in the organization with its UUID, name and active windows.
12
+ - ${ScheduleToolRequestType.GET}: Get one schedule by scheduleUuid.
13
+ - ${ScheduleToolRequestType.CREATE}: Create a weekly schedule. Requires name and intervals, e.g. intervals=${INTERVALS_EXAMPLE}.
14
+ - ${ScheduleToolRequestType.UPDATE}: Rename a schedule and/or replace its windows. Requires scheduleUuid plus name and/or intervals. Fields you omit are left unchanged; passing intervals REPLACES all existing windows.
15
+ - ${ScheduleToolRequestType.DELETE}: Delete a schedule. Requires scheduleUuid. Refuses (without deleting) when policies still reference the schedule, and lists them — see confirmDeleteInUse.
16
+ - ${ScheduleToolRequestType.GET_POLICIES_USING}: List the policies that reference a schedule. Requires scheduleUuid.
17
+
18
+ Schedules are org-wide and shared: editing one changes the behaviour of every policy attached to it. Schedules reported with "mutable": false are system-managed and cannot be updated or deleted.
19
+ `;
20
+ /** Look a schedule up in the org list; the API has no get-one endpoint. */
21
+ async function findScheduleByUuid(scheduleUuid, requestModifiers, sessionId) {
22
+ const { schedules } = await findSchedules(requestModifiers, sessionId);
23
+ return schedules.find(schedule => schedule.uuid === scheduleUuid);
24
+ }
25
+ const TOOL_HANDLER = async (args, extra) => {
26
+ const { requestModifiers, sessionId } = extractFromToolExtra(extra);
27
+ try {
28
+ switch (args.requestType) {
29
+ case ScheduleToolRequestType.LIST: {
30
+ const { schedules, warningMsg } = await findSchedules(requestModifiers, sessionId);
31
+ return createToolStructuredContent({
32
+ schedules,
33
+ warningMsg,
34
+ // A bare [] reads as "no schedules exist" indistinguishably from
35
+ // "this org has none configured yet"; say which it is.
36
+ note: schedules.length === 0
37
+ ? "This organization has no schedules configured. A camera or door policy cannot be scheduled until one is created — offer to create one with requestType 'create'."
38
+ : undefined,
39
+ });
40
+ }
41
+ case ScheduleToolRequestType.GET: {
42
+ if (!args.scheduleUuid) {
43
+ return createToolTextContent("scheduleUuid is required for 'get'.");
44
+ }
45
+ const schedule = await findScheduleByUuid(args.scheduleUuid, requestModifiers, sessionId);
46
+ if (!schedule) {
47
+ return createToolTextContent(`No schedule in this organization has the uuid "${args.scheduleUuid}". Use requestType 'list' to see the schedules that exist — do not guess a uuid.`);
48
+ }
49
+ return createToolStructuredContent({ schedule });
50
+ }
51
+ case ScheduleToolRequestType.CREATE: {
52
+ if (!args.name?.trim()) {
53
+ return createToolTextContent("name is required for 'create'.");
54
+ }
55
+ if (!args.intervals) {
56
+ return createToolTextContent(`intervals is required for 'create'. Example: ${INTERVALS_EXAMPLE}`);
57
+ }
58
+ const parsed = parseWeeklyIntervals(args.intervals);
59
+ if (!parsed.ok) {
60
+ // Pre-mutation: nothing was created, so a corrected retry is safe.
61
+ return createToolTextContent(`RETRYABLE — nothing was created. ${parsed.message}`);
62
+ }
63
+ const scheduleInput = {
64
+ name: args.name.trim(),
65
+ strategy: WEEKLY_REPEATING_STRATEGY,
66
+ intervalList: parsed.value,
67
+ };
68
+ const { scheduleUuid, warningMsg } = await createSchedule(scheduleInput, requestModifiers, sessionId);
69
+ return createToolStructuredContent({
70
+ created: { success: true, scheduleUuid },
71
+ warningMsg,
72
+ note: `Created "${args.name.trim()}" active ${describeIntervals(parsed.value)}.`,
73
+ });
74
+ }
75
+ case ScheduleToolRequestType.UPDATE: {
76
+ if (!args.scheduleUuid) {
77
+ return createToolTextContent("scheduleUuid is required for 'update'.");
78
+ }
79
+ if (!args.name?.trim() && !args.intervals) {
80
+ return createToolTextContent("'update' needs at least one of name or intervals — both were empty, so there is nothing to change.");
81
+ }
82
+ // api2's updateSchedule takes the WHOLE schedule, so sending only the
83
+ // changed field would blank the other one. Read first and merge.
84
+ const existing = await findScheduleByUuid(args.scheduleUuid, requestModifiers, sessionId);
85
+ if (!existing) {
86
+ return createToolTextContent(`No schedule in this organization has the uuid "${args.scheduleUuid}". Use requestType 'list' to see the schedules that exist.`);
87
+ }
88
+ if (existing.mutable === false) {
89
+ return createToolTextContent(`Schedule "${existing.name ?? args.scheduleUuid}" is system-managed (mutable: false) and cannot be edited. Tell the user this schedule is read-only; creating a new schedule is the alternative.`);
90
+ }
91
+ let intervalList = existing.intervals
92
+ ?.flatMap(interval => typeof interval.minuteOfWeekStart === "number" &&
93
+ typeof interval.minuteOfWeekStop === "number"
94
+ ? [
95
+ {
96
+ minuteOfWeekStart: interval.minuteOfWeekStart,
97
+ minuteOfWeekStop: interval.minuteOfWeekStop,
98
+ },
99
+ ]
100
+ : [])
101
+ .slice();
102
+ let replacedWindows = false;
103
+ if (args.intervals) {
104
+ const parsed = parseWeeklyIntervals(args.intervals);
105
+ if (!parsed.ok) {
106
+ return createToolTextContent(`RETRYABLE — nothing was changed. ${parsed.message}`);
107
+ }
108
+ intervalList = parsed.value;
109
+ replacedWindows = true;
110
+ }
111
+ const { updated, warningMsg } = await updateSchedule({
112
+ uuid: args.scheduleUuid,
113
+ name: args.name?.trim() || existing.name,
114
+ strategy: WEEKLY_REPEATING_STRATEGY,
115
+ intervalList,
116
+ }, requestModifiers, sessionId);
117
+ return createToolStructuredContent({
118
+ updated,
119
+ warningMsg,
120
+ note: replacedWindows
121
+ ? `Replaced all windows on "${args.name?.trim() || existing.name}" — it is now active ${describeIntervals(intervalList ?? [])}. Every policy using this schedule now follows the new windows.`
122
+ : `Renamed to "${args.name?.trim()}". Its windows are unchanged.`,
123
+ });
124
+ }
125
+ case ScheduleToolRequestType.DELETE: {
126
+ if (!args.scheduleUuid) {
127
+ return createToolTextContent("scheduleUuid is required for 'delete'.");
128
+ }
129
+ const existing = await findScheduleByUuid(args.scheduleUuid, requestModifiers, sessionId);
130
+ if (!existing) {
131
+ return createToolTextContent(`No schedule in this organization has the uuid "${args.scheduleUuid}". Use requestType 'list' to see the schedules that exist.`);
132
+ }
133
+ if (existing.mutable === false) {
134
+ return createToolTextContent(`Schedule "${existing.name ?? args.scheduleUuid}" is system-managed (mutable: false) and cannot be deleted.`);
135
+ }
136
+ // Deleting a schedule silently rewires every policy attached to it, so
137
+ // check usage first and make the caller confirm.
138
+ const { usedBy } = await getPoliciesUsingSchedule(args.scheduleUuid, requestModifiers, sessionId);
139
+ if (usedBy.length > 0 && !args.confirmDeleteInUse) {
140
+ return createToolStructuredContent({
141
+ usedBy,
142
+ note: `NOT DELETED — nothing was changed. "${existing.name ?? args.scheduleUuid}" is still used by ` +
143
+ `${usedBy.length} ${usedBy.length === 1 ? "policy" : "policies"}: ` +
144
+ `${usedBy.map(policy => `${policy.name ?? policy.uuid} (${policy.policyType})`).join(", ")}. ` +
145
+ `Deleting it changes how those policies behave. Tell the user which policies are affected and ask them to ` +
146
+ `confirm, then call this tool again with confirmDeleteInUse: true.`,
147
+ });
148
+ }
149
+ const { deleted, warningMsg } = await deleteSchedule(args.scheduleUuid, requestModifiers, sessionId);
150
+ return createToolStructuredContent({
151
+ deleted,
152
+ warningMsg,
153
+ note: usedBy.length > 0
154
+ ? `Deleted "${existing.name ?? args.scheduleUuid}", which was in use by ${usedBy.length} ${usedBy.length === 1 ? "policy" : "policies"}: ${usedBy.map(policy => policy.name ?? policy.uuid).join(", ")}. Those policies no longer have this schedule.`
155
+ : `Deleted "${existing.name ?? args.scheduleUuid}". No policies referenced it.`,
156
+ });
157
+ }
158
+ case ScheduleToolRequestType.GET_POLICIES_USING: {
159
+ if (!args.scheduleUuid) {
160
+ return createToolTextContent("scheduleUuid is required for 'get-policies-using'.");
161
+ }
162
+ const { usedBy, warningMsg } = await getPoliciesUsingSchedule(args.scheduleUuid, requestModifiers, sessionId);
163
+ return createToolStructuredContent({
164
+ usedBy,
165
+ warningMsg,
166
+ note: usedBy.length === 0
167
+ ? "No policies reference this schedule, so it can be deleted or edited without affecting anything."
168
+ : undefined,
169
+ });
170
+ }
171
+ }
172
+ }
173
+ catch (error) {
174
+ return createToolStructuredContent({
175
+ error: error instanceof Error ? error.message : "Unknown error",
176
+ });
177
+ }
178
+ return createToolStructuredContent({
179
+ error: `Invalid requestType. Valid values are: ${Object.values(ScheduleToolRequestType).join(", ")}.`,
180
+ });
181
+ };
182
+ export function createTool(server) {
183
+ server.registerTool(TOOL_NAME, {
184
+ title: "Schedules",
185
+ description: TOOL_DESCRIPTION,
186
+ inputSchema: TOOL_ARGS,
187
+ outputSchema: OUTPUT_SCHEMA.shape,
188
+ annotations: { readOnlyHint: false, destructiveHint: true },
189
+ }, TOOL_HANDLER);
190
+ }
@@ -62,8 +62,10 @@ const TOOL_HANDLER = async (args, _extra) => {
62
62
  };
63
63
  export function createTool(server) {
64
64
  server.registerTool(TOOL_NAME, {
65
+ title: "Search",
65
66
  description: TOOL_DESCRIPTION,
66
67
  inputSchema: TOOL_ARGS,
67
68
  outputSchema: OUTPUT_SCHEMA.shape,
69
+ annotations: { readOnlyHint: true },
68
70
  }, TOOL_HANDLER);
69
71
  }