rhombus-node-mcp 0.1.32 → 0.1.34

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.
@@ -0,0 +1,195 @@
1
+ import { postApi } from "../network/network.js";
2
+ import { ChatFollowUpActionEnum, } from "../types/automated-prompts-tool-types.js";
3
+ import { formatIsoWithOffset } from "../util.js";
4
+ function extractNotifyUserUuids(followUpActions) {
5
+ if (!followUpActions)
6
+ return undefined;
7
+ const notifyAction = followUpActions.find((action) => action?.type === ChatFollowUpActionEnum.NOTIFY_USERS);
8
+ if (!notifyAction)
9
+ return undefined;
10
+ const userUuids = notifyAction.userUuids;
11
+ return userUuids?.filter((u) => u !== null) ?? [];
12
+ }
13
+ function toSummary(prompt, timeZone) {
14
+ if (!prompt)
15
+ return undefined;
16
+ return {
17
+ uuid: prompt.uuid ?? undefined,
18
+ prompt: prompt.prompt ?? undefined,
19
+ responseTemplate: prompt.responseTemplate ?? undefined,
20
+ frequency: prompt.frequency
21
+ ? {
22
+ frequency: prompt.frequency.frequency ?? undefined,
23
+ unit: prompt.frequency.unit ?? undefined,
24
+ }
25
+ : undefined,
26
+ invokeAt: formatIsoWithOffset(prompt.invokeAtMs, timeZone),
27
+ permissionGroupUuid: prompt.permissionGroupUuid ?? undefined,
28
+ notifyUserUuids: extractNotifyUserUuids(prompt.followUpActions),
29
+ scheduleUuid: prompt.scheduleUuid ?? undefined,
30
+ orgUuid: prompt.orgUuid ?? undefined,
31
+ };
32
+ }
33
+ function toHistoryEntry(record, timeZone) {
34
+ if (!record)
35
+ return undefined;
36
+ return {
37
+ uuid: record.uuid ?? undefined,
38
+ automatedPromptUuid: record.automatedPromptUuid ?? undefined,
39
+ query: record.query ?? undefined,
40
+ response: record.response ?? undefined,
41
+ queriedAt: formatIsoWithOffset(record.queriedAtMs, timeZone),
42
+ respondedAt: formatIsoWithOffset(record.respondedAtMs, timeZone),
43
+ responseType: record.responseType ?? undefined,
44
+ };
45
+ }
46
+ function buildSettingsBody(input, includeUuid) {
47
+ const settings = {};
48
+ if (includeUuid && "promptUuid" in input && input.promptUuid) {
49
+ settings.uuid = input.promptUuid;
50
+ }
51
+ if (input.prompt !== undefined && input.prompt !== null) {
52
+ settings.prompt = input.prompt;
53
+ }
54
+ if (input.responseTemplate !== undefined && input.responseTemplate !== null) {
55
+ settings.responseTemplate = input.responseTemplate;
56
+ }
57
+ if (input.invokeAtMs !== undefined && input.invokeAtMs !== null) {
58
+ settings.invokeAtMs = input.invokeAtMs;
59
+ }
60
+ if (input.frequencyValue !== undefined &&
61
+ input.frequencyValue !== null &&
62
+ input.frequencyUnit !== undefined &&
63
+ input.frequencyUnit !== null) {
64
+ settings.frequency = {
65
+ frequency: input.frequencyValue,
66
+ unit: input.frequencyUnit,
67
+ };
68
+ }
69
+ if (input.permissionGroupUuid !== undefined && input.permissionGroupUuid !== null) {
70
+ settings.permissionGroupUuid = input.permissionGroupUuid;
71
+ }
72
+ if (input.notifyUserUuids !== undefined && input.notifyUserUuids !== null) {
73
+ settings.followUpActions = input.notifyUserUuids.length
74
+ ? [
75
+ {
76
+ type: ChatFollowUpActionEnum.NOTIFY_USERS,
77
+ userUuids: input.notifyUserUuids,
78
+ },
79
+ ]
80
+ : [];
81
+ }
82
+ return settings;
83
+ }
84
+ export async function listAutomatedPrompts(pageRequest, requestModifiers, sessionId, timeZone) {
85
+ const res = await postApi({
86
+ route: "/chatbot/automation/getAutomatedPromptsForOrg",
87
+ body: pageRequest,
88
+ modifiers: requestModifiers,
89
+ sessionId,
90
+ });
91
+ if (res.error)
92
+ throw new Error(JSON.stringify(res));
93
+ const settingsList = (res.settingsList ?? [])
94
+ .map((p) => toSummary(p, timeZone))
95
+ .filter((s) => s !== undefined);
96
+ return {
97
+ settingsList,
98
+ lastEvaluatedKey: res.lastEvaluatedKey ?? undefined,
99
+ };
100
+ }
101
+ export async function getAutomatedPrompt(promptUuid, requestModifiers, sessionId, timeZone) {
102
+ const res = await postApi({
103
+ route: "/chatbot/automation/getAutomatedPrompt",
104
+ body: { promptUuid },
105
+ modifiers: requestModifiers,
106
+ sessionId,
107
+ });
108
+ if (res.error)
109
+ throw new Error(JSON.stringify(res));
110
+ return toSummary(res.settings, timeZone);
111
+ }
112
+ export async function createAutomatedPrompt(input, requestModifiers, sessionId, timeZone) {
113
+ const settings = buildSettingsBody(input, /*includeUuid*/ false);
114
+ const res = await postApi({
115
+ route: "/chatbot/automation/createAutomatedPrompt",
116
+ body: { settings },
117
+ modifiers: requestModifiers,
118
+ sessionId,
119
+ });
120
+ if (res.error)
121
+ throw new Error(JSON.stringify(res));
122
+ return toSummary(res.settings, timeZone);
123
+ }
124
+ export async function updateAutomatedPrompt(input, requestModifiers, sessionId, timeZone) {
125
+ const selectiveUpdate = buildSettingsBody(input, /*includeUuid*/ true);
126
+ const res = await postApi({
127
+ route: "/chatbot/automation/updateAutomatedPrompt",
128
+ body: { selectiveUpdate },
129
+ modifiers: requestModifiers,
130
+ sessionId,
131
+ });
132
+ if (res.error)
133
+ throw new Error(JSON.stringify(res));
134
+ return toSummary(res.settings, timeZone);
135
+ }
136
+ export async function deleteAutomatedPrompt(promptUuid, requestModifiers, sessionId) {
137
+ const res = await postApi({
138
+ route: "/chatbot/automation/deleteAutomatedPrompt",
139
+ body: { promptUuid },
140
+ modifiers: requestModifiers,
141
+ sessionId,
142
+ });
143
+ if (res.error)
144
+ throw new Error(JSON.stringify(res));
145
+ return { ok: true, action: "deleted" };
146
+ }
147
+ export async function getAutomatedPromptChatHistory(promptUuid, pageRequest, requestModifiers, sessionId, timeZone) {
148
+ const res = await postApi({
149
+ route: "/chatbot/automation/getAutomatedPromptChatHistory",
150
+ body: {
151
+ promptUuid,
152
+ lastEvaluatedKey: pageRequest.lastEvaluatedKey ?? undefined,
153
+ maxPageSize: pageRequest.maxPageSize ?? undefined,
154
+ },
155
+ modifiers: requestModifiers,
156
+ sessionId,
157
+ });
158
+ if (res.error)
159
+ throw new Error(JSON.stringify(res));
160
+ const chatHistory = (res.chatHistory ?? [])
161
+ .map((r) => toHistoryEntry(r, timeZone))
162
+ .filter((e) => e !== undefined);
163
+ return {
164
+ chatHistory,
165
+ lastEvaluatedKey: res.lastEvaluatedKey ?? undefined,
166
+ };
167
+ }
168
+ export async function shareAutomatedPromptResponse(chatUuid, visibility, requestModifiers, sessionId) {
169
+ const res = await postApi({
170
+ route: "/chatbot/automation/shareAutomatedPromptResponse",
171
+ body: {
172
+ chatUuid,
173
+ privacy: { visibility },
174
+ },
175
+ modifiers: requestModifiers,
176
+ sessionId,
177
+ });
178
+ if (res.error)
179
+ throw new Error(JSON.stringify(res));
180
+ return { ok: true, action: `visibility set to ${visibility}` };
181
+ }
182
+ export async function verifyJobScheduled(promptUuid, requestModifiers, sessionId) {
183
+ const res = await postApi({
184
+ route: "/chatbot/automation/verifyJobScheduled",
185
+ body: { promptUuid },
186
+ modifiers: requestModifiers,
187
+ sessionId,
188
+ });
189
+ if (res.error)
190
+ throw new Error(JSON.stringify(res));
191
+ return {
192
+ scheduleExpression: res.scheduleExpression ?? undefined,
193
+ scheduleTimezone: res.scheduleTimezone ?? undefined,
194
+ };
195
+ }
@@ -58,6 +58,39 @@ export async function getPermissionsForCurrentUser(requestModifiers, sessionId)
58
58
  : undefined,
59
59
  };
60
60
  }
61
+ /**
62
+ * Keep only non-null string entries from a nullable string[] coming back from
63
+ * the generated schema types.
64
+ */
65
+ function cleanStringArray(arr) {
66
+ if (!arr)
67
+ return undefined;
68
+ const cleaned = arr.filter((v) => typeof v === "string");
69
+ return cleaned.length > 0 ? cleaned : undefined;
70
+ }
71
+ /**
72
+ * Map a `{ [k]: PermissionEnum }` to a `Record<string, string>` dropping null
73
+ * values. Returns undefined when the input is nullish so the field is omitted
74
+ * from the JSON payload entirely (and the filtering proxy's `includeFields`
75
+ * can omit it without ambiguity).
76
+ */
77
+ function toStringMap(map) {
78
+ if (!map)
79
+ return undefined;
80
+ const entries = Object.entries(map).filter((pair) => typeof pair[1] === "string");
81
+ return entries.length > 0 ? Object.fromEntries(entries) : undefined;
82
+ }
83
+ function toNestedStringMap(map) {
84
+ if (!map)
85
+ return undefined;
86
+ const result = {};
87
+ for (const [outer, inner] of Object.entries(map)) {
88
+ const cleanedInner = toStringMap(inner);
89
+ if (cleanedInner)
90
+ result[outer] = cleanedInner;
91
+ }
92
+ return Object.keys(result).length > 0 ? result : undefined;
93
+ }
61
94
  export async function getPermissionGroups(requestModifiers, sessionId) {
62
95
  const res = await postApi({
63
96
  route: "/permission/getPermissionGroupsForOrg",
@@ -71,7 +104,23 @@ export async function getPermissionGroups(requestModifiers, sessionId) {
71
104
  return (res.permissionGroups?.map(group => ({
72
105
  uuid: group.uuid ?? undefined,
73
106
  name: group.name ?? undefined,
107
+ description: group.description ?? undefined,
74
108
  orgUuid: group.orgUuid ?? undefined,
75
- role: group.role ?? undefined,
109
+ mutable: group.mutable ?? undefined,
110
+ superAdmin: group.superAdmin ?? undefined,
111
+ installer: group.installer ?? undefined,
112
+ inLine: group.inLine ?? undefined,
113
+ storedInS3: group.storedInS3 ?? undefined,
114
+ defaultPermissionForNewLocations: group.defaultPermissionForNewLocations ?? undefined,
115
+ defaultAccessControlPermissionForNewLocations: group.defaultAccessControlPermissionForNewLocations ?? undefined,
116
+ functionalityList: group.functionalityList ?? undefined,
117
+ accessibleLocations: cleanStringArray(group.accessibleLocations),
118
+ assignablePermissionGroups: cleanStringArray(group.assignablePermissionGroups),
119
+ // Heavy maps — callers should typically prune these out via includeFields.
120
+ locationAccessMap: toStringMap(group.locationAccessMap),
121
+ accessControlLocationAccessMap: toStringMap(group.accessControlLocationAccessMap),
122
+ deviceAccessMap: toStringMap(group.deviceAccessMap),
123
+ userPermissionGroupAccessMap: toStringMap(group.userPermissionGroupAccessMap),
124
+ locationGranularAccessMap: toNestedStringMap(group.locationGranularAccessMap),
76
125
  })) ?? []);
77
126
  }
@@ -0,0 +1,145 @@
1
+ import { createAutomatedPrompt, deleteAutomatedPrompt, getAutomatedPrompt, getAutomatedPromptChatHistory, listAutomatedPrompts, shareAutomatedPromptResponse, updateAutomatedPrompt, verifyJobScheduled, } from "../api/automated-prompts-tool-api.js";
2
+ import { AutomatedPromptsRequestType, CreateRequestSchema, DeleteRequestSchema, GetHistoryRequestSchema, GetRequestSchema, ListRequestSchema, OUTPUT_SCHEMA, ShareResponseRequestSchema, TOOL_ARGS, UpdateRequestSchema, VerifyScheduledRequestSchema, } from "../types/automated-prompts-tool-types.js";
3
+ import { createToolStructuredContent, createToolTextContent, extractFromToolExtra, } from "../util.js";
4
+ const TOOL_NAME = "automated-prompts-tool";
5
+ const TOOL_DESCRIPTION = `
6
+ This tool manages Rhombus MIND automated prompts - scheduled chatbot jobs that run a prompt at a recurring interval and store each response. Use it to list, inspect, create, update, delete, page through past responses for, share, or re-verify the schedule of an automated prompt.
7
+
8
+ Modes (set "requestType"):
9
+ - ${AutomatedPromptsRequestType.LIST}: List all automated prompts in the org. Optional 'lastEvaluatedKey' / 'maxPageSize' for pagination.
10
+ - ${AutomatedPromptsRequestType.GET}: Get a single automated prompt's settings. Requires 'promptUuid'.
11
+ - ${AutomatedPromptsRequestType.CREATE}: Create a new automated prompt. Requires 'prompt', 'invokeAt' (ISO 8601 with offset, must be at least 15 minutes in the future), 'frequencyValue', 'frequencyUnit', and 'permissionGroupUuid'. Optional 'responseTemplate' and 'notifyUserUuids'.
12
+ - ${AutomatedPromptsRequestType.UPDATE}: Selectively update an automated prompt. Requires 'promptUuid'; only the fields you set will be changed. To change the role, the caller must have access to both the current and new role. Pass an empty 'notifyUserUuids' array to clear notifyees.
13
+ - ${AutomatedPromptsRequestType.DELETE}: Delete an automated prompt and all of its stored responses. Requires 'promptUuid'.
14
+ - ${AutomatedPromptsRequestType.GET_HISTORY}: Page through responses generated by an automated prompt. Requires 'promptUuid'. Optional 'lastEvaluatedKey' / 'maxPageSize'.
15
+ - ${AutomatedPromptsRequestType.SHARE_RESPONSE}: Update the visibility of one stored response. Requires 'chatUuid' and 'visibility' (PUBLIC, ORG_WIDE, SELECT_USERS, PRIVATE).
16
+ - ${AutomatedPromptsRequestType.VERIFY_SCHEDULED}: Re-verify that the job is scheduled to trigger; the server reschedules it if missing. Requires 'promptUuid'.
17
+
18
+ Notes:
19
+ - Use 'user-tool' to look up user UUIDs and resolve them to names/emails. 'notifyUserUuids' must be members of the chosen 'permissionGroupUuid'.
20
+ - 'invokeAt' is an ISO 8601 timestamp; both 'Z' and '+/-HH:mm' offsets are accepted. The tool converts it to milliseconds for the API.
21
+ - Timestamps in the output are ISO 8601 strings with timezone offset (defaults to America/Los_Angeles when no org timezone is available).
22
+ - 'submitTestPrompt' is intentionally not exposed by this tool.
23
+ `;
24
+ function formatZodError(err) {
25
+ return err.issues
26
+ .map(issue => {
27
+ const path = issue.path.join(".");
28
+ return path ? `${path}: ${issue.message}` : issue.message;
29
+ })
30
+ .join("; ");
31
+ }
32
+ function validationErrorResponse(err) {
33
+ return createToolTextContent(JSON.stringify({ error: formatZodError(err) }));
34
+ }
35
+ const TOOL_HANDLER = async (args, _extra) => {
36
+ const { requestModifiers, sessionId } = extractFromToolExtra(_extra);
37
+ try {
38
+ switch (args.requestType) {
39
+ case AutomatedPromptsRequestType.LIST: {
40
+ const parsed = ListRequestSchema.safeParse(args);
41
+ if (!parsed.success)
42
+ return validationErrorResponse(parsed.error);
43
+ const { lastEvaluatedKey, maxPageSize } = parsed.data;
44
+ const result = await listAutomatedPrompts({
45
+ lastEvaluatedKey: lastEvaluatedKey ?? undefined,
46
+ maxPageSize: maxPageSize ?? undefined,
47
+ }, requestModifiers, sessionId);
48
+ return createToolStructuredContent({
49
+ settingsList: result.settingsList,
50
+ lastEvaluatedKey: result.lastEvaluatedKey,
51
+ });
52
+ }
53
+ case AutomatedPromptsRequestType.GET: {
54
+ const parsed = GetRequestSchema.safeParse(args);
55
+ if (!parsed.success)
56
+ return validationErrorResponse(parsed.error);
57
+ const settings = await getAutomatedPrompt(parsed.data.promptUuid, requestModifiers, sessionId);
58
+ return createToolStructuredContent({ settings });
59
+ }
60
+ case AutomatedPromptsRequestType.CREATE: {
61
+ const parsed = CreateRequestSchema.safeParse(args);
62
+ if (!parsed.success)
63
+ return validationErrorResponse(parsed.error);
64
+ const data = parsed.data;
65
+ const settings = await createAutomatedPrompt({
66
+ prompt: data.prompt,
67
+ invokeAtMs: new Date(data.invokeAt).getTime(),
68
+ frequencyValue: data.frequencyValue,
69
+ frequencyUnit: data.frequencyUnit,
70
+ permissionGroupUuid: data.permissionGroupUuid,
71
+ responseTemplate: data.responseTemplate ?? undefined,
72
+ notifyUserUuids: data.notifyUserUuids ?? undefined,
73
+ }, requestModifiers, sessionId);
74
+ return createToolStructuredContent({ settings });
75
+ }
76
+ case AutomatedPromptsRequestType.UPDATE: {
77
+ const parsed = UpdateRequestSchema.safeParse(args);
78
+ if (!parsed.success)
79
+ return validationErrorResponse(parsed.error);
80
+ const data = parsed.data;
81
+ const settings = await updateAutomatedPrompt({
82
+ promptUuid: data.promptUuid,
83
+ prompt: data.prompt ?? undefined,
84
+ invokeAtMs: data.invokeAt ? new Date(data.invokeAt).getTime() : undefined,
85
+ frequencyValue: data.frequencyValue ?? undefined,
86
+ frequencyUnit: data.frequencyUnit ?? undefined,
87
+ permissionGroupUuid: data.permissionGroupUuid ?? undefined,
88
+ responseTemplate: data.responseTemplate ?? undefined,
89
+ notifyUserUuids: data.notifyUserUuids ?? undefined,
90
+ }, requestModifiers, sessionId);
91
+ return createToolStructuredContent({ settings });
92
+ }
93
+ case AutomatedPromptsRequestType.DELETE: {
94
+ const parsed = DeleteRequestSchema.safeParse(args);
95
+ if (!parsed.success)
96
+ return validationErrorResponse(parsed.error);
97
+ const success = await deleteAutomatedPrompt(parsed.data.promptUuid, requestModifiers, sessionId);
98
+ return createToolStructuredContent({ success });
99
+ }
100
+ case AutomatedPromptsRequestType.GET_HISTORY: {
101
+ const parsed = GetHistoryRequestSchema.safeParse(args);
102
+ if (!parsed.success)
103
+ return validationErrorResponse(parsed.error);
104
+ const data = parsed.data;
105
+ const result = await getAutomatedPromptChatHistory(data.promptUuid, {
106
+ lastEvaluatedKey: data.lastEvaluatedKey ?? undefined,
107
+ maxPageSize: data.maxPageSize ?? undefined,
108
+ }, requestModifiers, sessionId);
109
+ return createToolStructuredContent({
110
+ chatHistory: result.chatHistory,
111
+ lastEvaluatedKey: result.lastEvaluatedKey,
112
+ });
113
+ }
114
+ case AutomatedPromptsRequestType.SHARE_RESPONSE: {
115
+ const parsed = ShareResponseRequestSchema.safeParse(args);
116
+ if (!parsed.success)
117
+ return validationErrorResponse(parsed.error);
118
+ const { chatUuid, visibility } = parsed.data;
119
+ const success = await shareAutomatedPromptResponse(chatUuid, visibility, requestModifiers, sessionId);
120
+ return createToolStructuredContent({ success });
121
+ }
122
+ case AutomatedPromptsRequestType.VERIFY_SCHEDULED: {
123
+ const parsed = VerifyScheduledRequestSchema.safeParse(args);
124
+ if (!parsed.success)
125
+ return validationErrorResponse(parsed.error);
126
+ const verifyResult = await verifyJobScheduled(parsed.data.promptUuid, requestModifiers, sessionId);
127
+ return createToolStructuredContent({ verifyResult });
128
+ }
129
+ }
130
+ }
131
+ catch (error) {
132
+ if (error instanceof Error) {
133
+ return createToolStructuredContent({ error: error.message });
134
+ }
135
+ return createToolStructuredContent({ error: "Unknown error" });
136
+ }
137
+ return createToolStructuredContent({ error: "Invalid request type" });
138
+ };
139
+ export function createTool(server) {
140
+ server.registerTool(TOOL_NAME, {
141
+ description: TOOL_DESCRIPTION,
142
+ inputSchema: TOOL_ARGS,
143
+ outputSchema: OUTPUT_SCHEMA.shape,
144
+ }, TOOL_HANDLER);
145
+ }
@@ -29,6 +29,29 @@ What follows is a description of the behavior of this tool given the requestType
29
29
  This tool retrieves the current configuration for a specified camera or associated device (e.g., sensor, access controller). The returned JSON object can include detailed camera settings (e.g., resolution, bitrate) and various device-specific configurations (e.g. storage settings).
30
30
 
31
31
  NOTE: To update camera settings, use the update-tool instead.
32
+
33
+ ---
34
+
35
+ **AUTOMATIC SNAPSHOT FOR IMAGE QUALITY ISSUES** — When a user mentions camera image quality (darkness, brightness, blur, washed out, "doesn't look great", "fix the image", etc.), you MUST IMMEDIATELY:
36
+ 1. Call camera-tool with requestType "image" to capture a snapshot WITHOUT asking first.
37
+ 2. Analyze the image to identify quality issues.
38
+ 3. Call camera-tool with requestType "get-settings" to check current camera settings.
39
+ 4. Propose specific setting changes based on your analysis (store the exact values you plan to change, e.g. img_brightness, wdr_strength).
40
+ 5. When the user confirms ("yes", "confirm", "fix it", "apply", "go ahead", "ok", etc.), call update-tool with those stored settings — see update-tool's description for the confirmation flow. NEVER skip the update-tool call.
41
+
42
+ Examples that REQUIRE the automatic snapshot flow:
43
+ - "This camera's image doesn't look great"
44
+ - "The image quality is poor"
45
+ - "Can you fix the image"
46
+ - "Adjust settings to be optimal"
47
+ - "The camera looks blurry/dark/washed out"
48
+ - Any mention of image appearance problems.
49
+
50
+ **VISUAL-FEATURE CAMERA FILTERING** — When the user asks for cameras filtered by what they can see (indoors/outdoors, "facing the street", "with a view of X", parking lot, entrance), you MUST:
51
+ 1. First get the camera list via get-entity-tool or location-tool.
52
+ 2. Then call camera-tool with requestType "image" for EACH candidate camera (in PARALLEL).
53
+ 3. Analyze each image to determine if it meets the user's criteria.
54
+ 4. Return only the cameras that match.
32
55
  `;
33
56
  const logger = getLogger("camera-tool");
34
57
  const TOOL_ARGS = BASE_TOOL_ARGS;
@@ -23,6 +23,20 @@ It has the following modes of operation, determined by the "requestType" paramet
23
23
  - ${DoorScheduleExceptionRequestType.UPDATE_EXCEPTION}: Update a door schedule exception. Requires exception (DoorScheduleExceptionType object). If intervals are omitted but defaultState and date range are provided, the tool will generate a full-day interval.
24
24
 
25
25
  Use get-entity-tool to look up location and door UUIDs when needed.
26
+
27
+ ---
28
+
29
+ **Mutating doorUuids on an existing exception (add/remove/replace doors):**
30
+ \`update-exception\` REPLACES \`doorUuids\` with whatever you pass — it is not a delta operation. To safely remove or add doors while preserving the others:
31
+
32
+ 1. Call \`find-exceptions\` (or \`get-exception\`) to fetch the exception. The response includes the **full \`doorUuids\` array** for that exception — that IS the current door list.
33
+ 2. Compute the new array yourself:
34
+ - **Remove doors:** filter the existing \`doorUuids\` array, dropping the ones to remove.
35
+ - **Add doors:** append the new UUIDs to the existing array (deduped).
36
+ - **Replace wholesale:** just use the new set.
37
+ 3. Call \`update-exception\` with \`exception.uuid\` and \`exception.doorUuids\` set to your computed array. Other fields (name, dates, intervals, defaultState) are optional — omit them to leave them unchanged.
38
+
39
+ **You already have the current door list in the find-exceptions response.** Do not ask the user for it, do not claim you need additional lookups, and do not refuse the mutation citing missing context. The doorUuids array you got back IS the context.
26
40
  `;
27
41
  function buildDateRangeFilter(args) {
28
42
  return {
@@ -4,15 +4,14 @@ import { TOOL_ARGS } from "../types/get-entity-tool-types.js";
4
4
  import { createToolTextContent, extractFromToolExtra } from "../util.js";
5
5
  const TOOL_NAME = "get-entity-tool";
6
6
  const TOOL_DESCRIPTION = `
7
- Retrieves entities (or devices) of certain types.
8
- Can request multiple entity types at once.
9
- The return structure is a JSON string that contains the states of the requested entities.
10
- This data is exact. Whatever entities exist will be returned here.
7
+ Retrieves entities (or devices) of certain types — cameras, doorbell cameras, badge readers, access-controlled doors, audio gateways, door sensors, environmental sensors, motion sensors, buttons, keypads, environmental gateways. Can request multiple entity types at once. The return structure is a JSON string that contains the states (including names, UUIDs, location, model, firmware, connection status) of the requested entities. This data is exact.
11
8
 
12
- This is the primary tool for checking device health and connectivity status. Each device in the response
13
- includes a "connected" boolean field indicating whether it is currently online (true) or offline (false).
14
- When asked about device health, offline devices, or connectivity issues, use this tool to fetch all device
15
- types and check the "connected" field to identify which devices are offline or unreachable.`;
9
+ **Primary use cases:**
10
+ 1. **Looking up a device by name.** When the user mentions a specific camera, door, sensor, etc. by name (e.g. "describe camera 1919 Front Door Entrance", "what's the status of HW Lab door"), call this tool with the matching entityType, scan the returned list, and **fuzzy/case-insensitive substring match** the user's reference against the \`name\` field. Don't ask the user to clarify try this lookup first, and only ask if there are genuinely multiple plausible matches in the results.
11
+ 2. **Listing all devices of a type** (cameras, doors, sensors, etc.) for a location or org-wide.
12
+ 3. **Checking device health and connectivity.** Each device includes a \`connected\` boolean (true = online, false = offline). For "which devices are offline?" / "is X online?" / health questions, fetch the relevant entityTypes and inspect \`connected\`.
13
+
14
+ When the user asks to "describe", "look up", "find", "show me", or "tell me about" a named device, this is almost always the right starting tool — call it before asking the user for more specifics.`;
16
15
  const TOOL_HANDLER = async (args, extra) => {
17
16
  const { entityTypes, timeZone, tempUnit } = args;
18
17
  const filterBy = args.filterBy ?? { locationUuids: null };
@@ -2,7 +2,7 @@ import { getOrg } from "../api/get-org-information-tool-api.js";
2
2
  import { TOOL_ARGS } from "../types/get-org-information-tool-types.js";
3
3
  const TOOL_NAME = "get-org-information";
4
4
  const TOOL_DESCRIPTION = "Get general information about the organization including org name, camera configuration defaults, contact information, and org settings.";
5
- const TOOL_HANDLER = async (args, extra) => {
5
+ const TOOL_HANDLER = async (_, extra) => {
6
6
  const org = await getOrg(extra._meta?.requestModifiers, extra.sessionId);
7
7
  return {
8
8
  content: [
@@ -6,6 +6,8 @@ const TOOL_NAME = "report-tool";
6
6
  const TOOL_DESCRIPTION = `
7
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.
8
8
 
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
+
9
11
  ---
10
12
 
11
13
  **People / occupancy counting strategy**
@@ -1,7 +1,11 @@
1
1
  import { parseTimeDescription } from "../api/time-tool-api.js";
2
2
  import { TOOL_ARGS } from "../types/time-tool-types.js";
3
3
  const TOOL_NAME = "time-tool";
4
- const TOOL_DESCRIPTION = "This tool is capable of returning the time from a natural language query. If the user asks about the 'current time' use this tool. Try to kee time_description as close to the users initial query as possible. For example if someone says 'was X person seen today?' then time_description should be 'today'.";
4
+ const TOOL_DESCRIPTION = `This tool returns timestamps from natural-language descriptions of time. If the user asks about the "current time", use this tool. Keep time_description as close to the user's original phrasing as possible e.g. for "was X person seen today?" use time_description "today".
5
+
6
+ **When to call:** Whenever the user provides a natural-language time description ("today", "5 days ago", "last week", "this morning"), call time-tool to get accurate timestamps. Do not invent timestamps yourself. If you will need a timestamp as input to another tool, call time-tool first; multiple parallel calls are fine.
7
+
8
+ **Default timezone:** Assume "America/Los_Angeles" unless the user specifies otherwise or device/location context indicates a different one.`;
5
9
  const TOOL_HANDLER = async (args, extra) => {
6
10
  const { time_description, timezone } = args;
7
11
  const result = parseTimeDescription(time_description ?? undefined, timezone ?? undefined, extra);
@@ -27,6 +27,21 @@ Future support planned for:
27
27
  - Badge readers
28
28
 
29
29
  The tool uses elicitation forms for rich user interaction and shows current settings before updates.
30
+
31
+ ---
32
+
33
+ **CAMERA SETTINGS UPDATE FLOW** — Use this tool for ALL camera settings updates (brightness, contrast, WDR, resolution, audio, LED, etc.).
34
+ - For camera image-quality fixes: provide entityType="camera", entityUuid, and the specific settings to change in cameraVideoSettings.
35
+ - Example for dark image: update-tool(entityType="camera", entityUuid="<uuid>", cameraVideoSettings='{"img_brightness": 0, "wdr_strength": 64}').
36
+ - Example for washed out: update-tool(entityType="camera", entityUuid="<uuid>", cameraVideoSettings='{"img_brightness": -50, "img_contrast": 80}').
37
+ - Saturation matters — saturation 0 yields a grayscale image. Most cameras look best with mid-range values; tune from there.
38
+
39
+ **CONFIRMATION FLOW (MANDATORY)** — When the conversation history shows you analyzed a camera and proposed fixes, and the user replies with any affirmative ("yes", "confirm", "fix it", "apply", "do it", "go ahead", "proceed", "sure", "ok"):
40
+ 1. DO NOT generate any text response first.
41
+ 2. IMMEDIATELY call update-tool with the camera settings you previously identified.
42
+ 3. Only after update-tool returns successfully, say "Done! Check your camera now…".
43
+
44
+ NEVER respond saying settings were updated without first calling update-tool — without the call, no changes take effect. Avoid multiple rounds of confirmation; get one confirmation for all proposed changes.
30
45
  `;
31
46
  const TOOL_HANDLER = async (args, extra) => {
32
47
  const { entityType, entityUuid, cameraVideoSettings, cameraAudioSettings, cameraDeviceSettings, step, } = args;
@@ -9,9 +9,16 @@ It has the following modes of operation, determined by the "requestType" paramet
9
9
  - ${UserToolRequestType.LIST_USERS}: List all users in the organization with their details and roles.
10
10
  - ${UserToolRequestType.FIND_BY_EMAIL}: Find a specific user by their email address. Requires the email parameter.
11
11
  - ${UserToolRequestType.GET_PERMISSIONS}: Get the permissions for the current API user/token.
12
- - ${UserToolRequestType.GET_PERMISSION_GROUPS}: List all permission groups defined in the organization.
12
+ - ${UserToolRequestType.GET_PERMISSION_GROUPS}: List all permission groups defined in the organization. Each row can be very large — see below.
13
13
 
14
14
  User UUIDs returned here can be used with the access-control-tool to look up credentials.
15
+
16
+ IMPORTANT for '${UserToolRequestType.GET_PERMISSION_GROUPS}':
17
+ Each permission group row includes five access maps whose size scales with the org's locations, devices, and other permission groups. The total payload for 'userPermissionGroupAccessMap' across all rows grows O(N^2) in the number of permission groups. Before calling, decide which fields you actually need and pass them via 'includeFields':
18
+ - Safe/small fields (O(1) per row): 'permissionGroups.uuid', 'permissionGroups.name', 'permissionGroups.description', 'permissionGroups.mutable', 'permissionGroups.superAdmin', 'permissionGroups.installer', 'permissionGroups.defaultPermissionForNewLocations', 'permissionGroups.defaultAccessControlPermissionForNewLocations'.
19
+ - Bounded fields (O(K) per row): 'permissionGroups.functionalityList', 'permissionGroups.accessibleLocations', 'permissionGroups.assignablePermissionGroups'.
20
+ - Heavy fields (O(locations) / O(devices) / O(groups) per row): 'permissionGroups.locationAccessMap', 'permissionGroups.accessControlLocationAccessMap', 'permissionGroups.deviceAccessMap', 'permissionGroups.userPermissionGroupAccessMap', 'permissionGroups.locationGranularAccessMap'. Only request these when you specifically need them for a user.
21
+ Typical usage when just picking a role uuid: 'includeFields: ["permissionGroups.uuid", "permissionGroups.name", "permissionGroups.description"]'.
15
22
  `;
16
23
  const TOOL_HANDLER = async (args, _extra) => {
17
24
  const { requestModifiers, sessionId } = extractFromToolExtra(_extra);
@@ -0,0 +1,203 @@
1
+ import { z } from "zod";
2
+ import { ISOTimestampFormatDescription } from "../utils/timestampInput.js";
3
+ import { ChatFollowUpActionEnum, ChatVisibilityEnum, FrequencyUnitEnum } from "./schema.js";
4
+ export var AutomatedPromptsRequestType;
5
+ (function (AutomatedPromptsRequestType) {
6
+ AutomatedPromptsRequestType["LIST"] = "list";
7
+ AutomatedPromptsRequestType["GET"] = "get";
8
+ AutomatedPromptsRequestType["CREATE"] = "create";
9
+ AutomatedPromptsRequestType["UPDATE"] = "update";
10
+ AutomatedPromptsRequestType["DELETE"] = "delete";
11
+ AutomatedPromptsRequestType["GET_HISTORY"] = "get-history";
12
+ AutomatedPromptsRequestType["SHARE_RESPONSE"] = "share-response";
13
+ AutomatedPromptsRequestType["VERIFY_SCHEDULED"] = "verify-scheduled";
14
+ })(AutomatedPromptsRequestType || (AutomatedPromptsRequestType = {}));
15
+ export const TOOL_ARGS = {
16
+ requestType: z
17
+ .enum(AutomatedPromptsRequestType)
18
+ .describe("The type of automated prompt request to make."),
19
+ promptUuid: z
20
+ .string()
21
+ .nullable()
22
+ .describe("UUID of the automated prompt. Required for 'get', 'update', 'delete', 'get-history', and 'verify-scheduled'."),
23
+ chatUuid: z
24
+ .string()
25
+ .nullable()
26
+ .describe("UUID of a chat record (an individual response generated by an automated prompt). Required for 'share-response'."),
27
+ visibility: z
28
+ .enum(ChatVisibilityEnum)
29
+ .nullable()
30
+ .describe("Visibility for a shared chat response. Required for 'share-response'. One of PUBLIC, ORG_WIDE, SELECT_USERS, PRIVATE."),
31
+ prompt: z
32
+ .string()
33
+ .nullable()
34
+ .describe("The prompt MIND will execute every time the job runs. Required for 'create', recommended for 'update'."),
35
+ responseTemplate: z
36
+ .string()
37
+ .nullable()
38
+ .describe("Optional template that guides the shape of MIND's response. Used by 'create' and 'update'."),
39
+ frequencyValue: z
40
+ .number()
41
+ .int()
42
+ .positive()
43
+ .nullable()
44
+ .describe("How many time units between runs (e.g. 1 for 'every 1 day'). Required for 'create' alongside frequencyUnit."),
45
+ frequencyUnit: z
46
+ .enum(FrequencyUnitEnum)
47
+ .nullable()
48
+ .describe("Unit for the recurrence (HOURS, DAYS, WEEKS, MONTHS). Required for 'create' alongside frequencyValue."),
49
+ invokeAt: z.iso
50
+ .datetime({
51
+ message: "Invalid datetime string. Expected ISO 8601 format.",
52
+ offset: true,
53
+ })
54
+ .nullable()
55
+ .describe("When the automated prompt should first run. Required for 'create'. Must be at least 15 minutes in the future. " +
56
+ ISOTimestampFormatDescription),
57
+ permissionGroupUuid: z
58
+ .string()
59
+ .nullable()
60
+ .describe("UUID of the permission group/role that owns the job. Users in this role (or a higher role) can view and edit the job, and MIND uses this role's permissions when running the prompt. Required for 'create'."),
61
+ notifyUserUuids: z
62
+ .array(z.string())
63
+ .nullable()
64
+ .describe("User UUIDs to email-notify when MIND finishes a run. These users must be members of the permission group set above. Used by 'create' and 'update'. Pass an empty array to clear existing notifyees on 'update'."),
65
+ lastEvaluatedKey: z
66
+ .string()
67
+ .nullable()
68
+ .describe("Pagination cursor returned by a previous 'list' or 'get-history' call. Pass to fetch the next page."),
69
+ maxPageSize: z
70
+ .number()
71
+ .int()
72
+ .positive()
73
+ .nullable()
74
+ .describe("Maximum number of items per page for 'list' and 'get-history'."),
75
+ };
76
+ const TOOL_ARGS_SCHEMA = z.object(TOOL_ARGS);
77
+ /**
78
+ * Below is the individual request types schema
79
+ */
80
+ const invokeAtDatetime = z.iso.datetime({
81
+ message: "invokeAt must be a valid ISO 8601 timestamp with offset (e.g. 2026-04-22T10:57:00-07:00).",
82
+ offset: true,
83
+ });
84
+ export const ListRequestSchema = z.object({
85
+ requestType: z.literal(AutomatedPromptsRequestType.LIST),
86
+ lastEvaluatedKey: z.string().nullish(),
87
+ maxPageSize: z.number().int().positive().nullish(),
88
+ });
89
+ export const GetRequestSchema = z.object({
90
+ requestType: z.literal(AutomatedPromptsRequestType.GET),
91
+ promptUuid: z.string().min(1, "promptUuid is required."),
92
+ });
93
+ export const CreateRequestSchema = z.object({
94
+ requestType: z.literal(AutomatedPromptsRequestType.CREATE),
95
+ prompt: z.string().min(1, "prompt is required."),
96
+ invokeAt: invokeAtDatetime,
97
+ frequencyValue: z.number().int().positive(),
98
+ frequencyUnit: z.enum(FrequencyUnitEnum),
99
+ permissionGroupUuid: z.string().min(1, "permissionGroupUuid is required."),
100
+ responseTemplate: z.string().nullish(),
101
+ notifyUserUuids: z.array(z.string()).nullish(),
102
+ });
103
+ export const UpdateRequestSchema = z
104
+ .object({
105
+ requestType: z.literal(AutomatedPromptsRequestType.UPDATE),
106
+ promptUuid: z.string().min(1, "promptUuid is required."),
107
+ prompt: z.string().min(1).nullish(),
108
+ invokeAt: invokeAtDatetime.nullish(),
109
+ frequencyValue: z.number().int().positive().nullish(),
110
+ frequencyUnit: z.enum(FrequencyUnitEnum).nullish(),
111
+ permissionGroupUuid: z.string().min(1).nullish(),
112
+ responseTemplate: z.string().nullish(),
113
+ notifyUserUuids: z.array(z.string()).nullish(),
114
+ })
115
+ .refine(data => (data.frequencyValue == null) === (data.frequencyUnit == null), {
116
+ message: "frequencyValue and frequencyUnit must be provided together when updating recurrence.",
117
+ path: ["frequencyValue"],
118
+ });
119
+ export const DeleteRequestSchema = z.object({
120
+ requestType: z.literal(AutomatedPromptsRequestType.DELETE),
121
+ promptUuid: z.string().min(1, "promptUuid is required."),
122
+ });
123
+ export const GetHistoryRequestSchema = z.object({
124
+ requestType: z.literal(AutomatedPromptsRequestType.GET_HISTORY),
125
+ promptUuid: z.string().min(1, "promptUuid is required."),
126
+ lastEvaluatedKey: z.string().nullish(),
127
+ maxPageSize: z.number().int().positive().nullish(),
128
+ });
129
+ export const ShareResponseRequestSchema = z.object({
130
+ requestType: z.literal(AutomatedPromptsRequestType.SHARE_RESPONSE),
131
+ chatUuid: z.string().min(1, "chatUuid is required."),
132
+ visibility: z.enum(ChatVisibilityEnum),
133
+ });
134
+ export const VerifyScheduledRequestSchema = z.object({
135
+ requestType: z.literal(AutomatedPromptsRequestType.VERIFY_SCHEDULED),
136
+ promptUuid: z.string().min(1, "promptUuid is required."),
137
+ });
138
+ /* More shared types */
139
+ const FrequencySchema = z
140
+ .object({
141
+ frequency: z.number().optional(),
142
+ unit: z.enum(FrequencyUnitEnum).optional(),
143
+ })
144
+ .describe("How often the prompt runs.");
145
+ const AutomatedPromptSummarySchema = z
146
+ .object({
147
+ uuid: z.string().optional(),
148
+ prompt: z.string().optional(),
149
+ responseTemplate: z.string().optional(),
150
+ frequency: FrequencySchema.optional(),
151
+ invokeAt: z
152
+ .string()
153
+ .optional()
154
+ .describe("Next run time as an ISO 8601 string with timezone offset."),
155
+ permissionGroupUuid: z.string().optional(),
156
+ notifyUserUuids: z.array(z.string()).optional(),
157
+ scheduleUuid: z.string().optional(),
158
+ orgUuid: z.string().optional(),
159
+ })
160
+ .describe("Summary of an automated prompt's settings.");
161
+ const ChatHistoryEntrySchema = z
162
+ .object({
163
+ uuid: z.string().optional(),
164
+ automatedPromptUuid: z.string().optional(),
165
+ query: z.string().optional(),
166
+ response: z.string().optional(),
167
+ queriedAt: z
168
+ .string()
169
+ .optional()
170
+ .describe("When the run started, as an ISO 8601 string with offset."),
171
+ respondedAt: z
172
+ .string()
173
+ .optional()
174
+ .describe("When the response was generated, as an ISO 8601 string with offset."),
175
+ responseType: z.string().optional(),
176
+ })
177
+ .describe("One MIND response generated by an automated prompt run.");
178
+ export const OUTPUT_SCHEMA = z.object({
179
+ settings: AutomatedPromptSummarySchema.optional().describe("Returned by 'get', 'create', and 'update'."),
180
+ settingsList: z.array(AutomatedPromptSummarySchema).optional().describe("Returned by 'list'."),
181
+ lastEvaluatedKey: z
182
+ .string()
183
+ .optional()
184
+ .describe("Pagination cursor for the next page; absent when no more pages."),
185
+ chatHistory: z.array(ChatHistoryEntrySchema).optional().describe("Returned by 'get-history'."),
186
+ verifyResult: z
187
+ .object({
188
+ scheduleExpression: z.string().optional(),
189
+ scheduleTimezone: z.string().optional(),
190
+ })
191
+ .optional()
192
+ .describe("Returned by 'verify-scheduled'. Schedule expression and timezone for the next trigger."),
193
+ success: z
194
+ .object({
195
+ ok: z.boolean(),
196
+ action: z.string(),
197
+ })
198
+ .optional()
199
+ .describe("Returned by 'delete' and 'share-response'."),
200
+ error: z.string().optional().describe("An error message if the request failed."),
201
+ });
202
+ // Re-export for the api/tool layers so they can build ChatPrivacy follow-ups.
203
+ export { ChatFollowUpActionEnum, ChatVisibilityEnum, FrequencyUnitEnum };
@@ -1,5 +1,4 @@
1
1
  import { z } from "zod";
2
- import { INCLUDE_FIELDS_ARG, FILTER_BY_ARG } from "../util.js";
3
2
  export var UserToolRequestType;
4
3
  (function (UserToolRequestType) {
5
4
  UserToolRequestType["LIST_USERS"] = "list-users";
@@ -7,16 +6,71 @@ export var UserToolRequestType;
7
6
  UserToolRequestType["GET_PERMISSIONS"] = "get-permissions";
8
7
  UserToolRequestType["GET_PERMISSION_GROUPS"] = "get-permission-groups";
9
8
  })(UserToolRequestType || (UserToolRequestType = {}));
9
+ // NOTE: `includeFields` and `filterBy` are NOT declared here — the
10
+ // `createFilteringProxy` wrapper automatically injects them into every tool's
11
+ // inputSchema and post-processes the handler's output. Declaring them here
12
+ // would be dead code (the proxy overwrites them).
10
13
  export const TOOL_ARGS = {
11
14
  requestType: z.nativeEnum(UserToolRequestType).describe("The type of user request to make."),
12
15
  email: z
13
16
  .string()
14
17
  .nullable()
15
18
  .describe("The email address of the user to find. Required for 'find-by-email'."),
16
- includeFields: INCLUDE_FIELDS_ARG,
17
- filterBy: FILTER_BY_ARG,
18
19
  };
19
20
  const TOOL_ARGS_SCHEMA = z.object(TOOL_ARGS);
21
+ // Permission level assigned to a resource for a role. Typed loosely as string
22
+ // because the upstream schema enum (READONLY/ADMIN/LIVEONLY) may grow.
23
+ const PermissionEnumLoose = z.string();
24
+ const PermissionGroupSchema = z
25
+ .object({
26
+ uuid: z.string().optional(),
27
+ name: z.string().optional(),
28
+ description: z.string().optional(),
29
+ orgUuid: z.string().optional(),
30
+ mutable: z.boolean().optional(),
31
+ superAdmin: z.boolean().optional(),
32
+ installer: z.boolean().optional(),
33
+ inLine: z.boolean().optional(),
34
+ storedInS3: z.boolean().optional(),
35
+ defaultPermissionForNewLocations: PermissionEnumLoose.optional(),
36
+ defaultAccessControlPermissionForNewLocations: PermissionEnumLoose.optional(),
37
+ functionalityList: z
38
+ .array(z.string())
39
+ .optional()
40
+ .describe("List of functionality flags this role unlocks."),
41
+ accessibleLocations: z
42
+ .array(z.string())
43
+ .optional()
44
+ .describe("UUIDs of locations this role has any access to."),
45
+ assignablePermissionGroups: z
46
+ .array(z.string())
47
+ .optional()
48
+ .describe("UUIDs of other permission groups a member of this role may assign to users."),
49
+ // The five maps below grow with the number of locations / devices /
50
+ // other permission groups in the org. For large orgs a single row can be
51
+ // hundreds of KB; use `includeFields` to request only the maps you need.
52
+ locationAccessMap: z
53
+ .record(z.string(), PermissionEnumLoose)
54
+ .optional()
55
+ .describe("locationUuid -> permission level. Size = O(#locations)."),
56
+ accessControlLocationAccessMap: z
57
+ .record(z.string(), PermissionEnumLoose)
58
+ .optional()
59
+ .describe("locationUuid -> access-control permission level. Size = O(#locations)."),
60
+ deviceAccessMap: z
61
+ .record(z.string(), PermissionEnumLoose)
62
+ .optional()
63
+ .describe("deviceUuid -> permission level. Size = O(#devices)."),
64
+ userPermissionGroupAccessMap: z
65
+ .record(z.string(), PermissionEnumLoose)
66
+ .optional()
67
+ .describe("otherPermissionGroupUuid -> permission level. Total payload across all rows is O(N^2) in the number of permission groups — trim with includeFields."),
68
+ locationGranularAccessMap: z
69
+ .record(z.string(), z.record(z.string(), PermissionEnumLoose))
70
+ .optional()
71
+ .describe("locationUuid -> (subResourceUuid -> permission level). Size = O(#locations * avg #sub-resources)."),
72
+ })
73
+ .describe("A permission group (role) definition.");
20
74
  export const OUTPUT_SCHEMA = z.object({
21
75
  users: z
22
76
  .array(z.object({
@@ -49,13 +103,8 @@ export const OUTPUT_SCHEMA = z.object({
49
103
  .optional()
50
104
  .describe("Current user permissions"),
51
105
  permissionGroups: z
52
- .array(z.object({
53
- uuid: z.string().optional(),
54
- name: z.string().optional(),
55
- orgUuid: z.string().optional(),
56
- role: z.string().optional(),
57
- }))
106
+ .array(PermissionGroupSchema)
58
107
  .optional()
59
- .describe("List of permission groups"),
108
+ .describe("List of permission groups in the organization."),
60
109
  error: z.string().optional().describe("An error message if the request failed."),
61
110
  });
package/dist/util.js CHANGED
@@ -168,3 +168,21 @@ export function formatTimestamp(timestampMs, timeZone) {
168
168
  locale: "en-US",
169
169
  });
170
170
  }
171
+ /**
172
+ * Formats a timestamp in milliseconds to an ISO 8601 string with timezone offset.
173
+ * Format: "2025-04-21T10:57:00.000-07:00"
174
+ *
175
+ * Use this when returning timestamps in tool output schemas so the offset is preserved
176
+ * (rather than the bare "Z" produced by Date.prototype.toISOString()).
177
+ *
178
+ * @param timestampMs - Timestamp in milliseconds
179
+ * @param timeZone - Optional IANA timezone string (defaults to "America/Los_Angeles")
180
+ * @returns ISO 8601 string with offset, or undefined if input is null/undefined
181
+ */
182
+ export function formatIsoWithOffset(timestampMs, timeZone) {
183
+ if (timestampMs === null || timestampMs === undefined)
184
+ return undefined;
185
+ return (DateTime.fromMillis(timestampMs)
186
+ .setZone(timeZone || "America/Los_Angeles")
187
+ .toISO() ?? undefined);
188
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "rhombus-node-mcp",
3
- "version": "0.1.32",
3
+ "version": "0.1.34",
4
4
  "description": "MCP server for Rhombus API",
5
5
  "keywords": [
6
6
  "ai",