@assetlab/mcp-server 1.27.0 → 1.28.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.
@@ -20,7 +20,17 @@ function buildBody(params) {
20
20
  return body;
21
21
  }
22
22
  const PM_RESOURCE_TYPES = ['TOOL', 'PART', 'MATERIAL', 'EQUIPMENT'];
23
- const FORM_TEMPLATE_CATEGORIES = ['task_checklist', 'inspection', 'compliance', 'survey'];
23
+ // Shared by the four PM tools: linking a form here makes every work order the schedule
24
+ // generates carry a copy of that form for the technician to fill in.
25
+ const FORM_TEMPLATE_LINK_SCHEMA = z
26
+ .string()
27
+ .uuid()
28
+ .optional()
29
+ .describe('Form template ID to attach to every work order this generates — resolve via list_form_templates. Use a PUBLISHED template: generation resolves the current published version of the form, so a draft attaches nothing until it is published.');
30
+ // Work orders and PM schedules store each association twice — a singular id and a plural array.
31
+ // The API mirrors whichever side is sent, but callers still need the arrays to associate several
32
+ // records at once (and systems, which have no singular field at all).
33
+ const ASSOCIATION_ARRAY_SCHEMA = (description) => z.array(z.string().uuid()).optional().describe(description);
24
34
  const FORM_TEMPLATE_STATUSES = ['draft', 'published', 'archived'];
25
35
  const FORM_ITEM_TYPES = [
26
36
  'section',
@@ -109,8 +119,15 @@ export function registerWriteTools(server, client) {
109
119
  .string()
110
120
  .uuid()
111
121
  .optional()
112
- .describe('Location ID — resolve last via list_locations filtered by building_id'),
113
- asset_id: z.string().uuid().optional().describe('Asset ID'),
122
+ .describe('Location this work order is for — resolve last via list_locations filtered by building_id. The server mirrors it into location_ids, so send this OR location_ids, not a conflicting pair.'),
123
+ asset_id: z
124
+ .string()
125
+ .uuid()
126
+ .optional()
127
+ .describe('Asset this work order is for — resolve via list_assets. The server mirrors it into asset_ids.'),
128
+ location_ids: ASSOCIATION_ARRAY_SCHEMA('Locations this work order covers — use instead of location_id when there is more than one. Sending location_id alone replaces this with that single id.'),
129
+ asset_ids: ASSOCIATION_ARRAY_SCHEMA('Assets this work order covers — use instead of asset_id when there is more than one.'),
130
+ system_ids: ASSOCIATION_ARRAY_SCHEMA('Systems this work order covers — resolve via list_systems. Systems have no singular field; this array is the only way to associate them.'),
114
131
  start_date: z.string().optional().describe('Start date (ISO 8601)'),
115
132
  due_date: z.string().optional().describe('Due date (ISO 8601)'),
116
133
  estimated_time: z.number().min(0).optional().describe('Estimated time in hours'),
@@ -161,8 +178,15 @@ export function registerWriteTools(server, client) {
161
178
  .string()
162
179
  .uuid()
163
180
  .optional()
164
- .describe('Location ID — resolve last via list_locations filtered by building_id'),
165
- asset_id: z.string().uuid().optional().describe('Asset ID'),
181
+ .describe('Location this work order is for — resolve last via list_locations filtered by building_id. The server mirrors it into location_ids, so send this OR location_ids, not a conflicting pair.'),
182
+ asset_id: z
183
+ .string()
184
+ .uuid()
185
+ .optional()
186
+ .describe('Asset this work order is for — resolve via list_assets. The server mirrors it into asset_ids.'),
187
+ location_ids: ASSOCIATION_ARRAY_SCHEMA('Locations this work order covers — use instead of location_id when there is more than one. Sending location_id alone replaces this with that single id.'),
188
+ asset_ids: ASSOCIATION_ARRAY_SCHEMA('Assets this work order covers — use instead of asset_id when there is more than one.'),
189
+ system_ids: ASSOCIATION_ARRAY_SCHEMA('Systems this work order covers — resolve via list_systems. Systems have no singular field; this array is the only way to associate them.'),
166
190
  start_date: z.string().optional().describe('Start date (ISO 8601)'),
167
191
  due_date: z.string().optional().describe('Due date (ISO 8601)'),
168
192
  estimated_time: z.number().min(0).optional().describe('Estimated time in hours'),
@@ -897,9 +921,9 @@ export function registerWriteTools(server, client) {
897
921
  .describe('Meter interval — trigger every N units'),
898
922
  meter_unit: z.string().max(50).optional().describe('Meter unit (km, miles, hours, cycles)'),
899
923
  start_date: z.string().optional().describe('Start date (ISO 8601)'),
900
- asset_ids: z.array(z.string().uuid()).optional().describe('Array of asset IDs'),
901
- system_ids: z.array(z.string().uuid()).optional().describe('Array of system IDs'),
902
- location_ids: z.array(z.string().uuid()).optional().describe('Array of location IDs'),
924
+ asset_ids: ASSOCIATION_ARRAY_SCHEMA('Assets this schedule covers — use instead of asset_id when there is more than one.'),
925
+ system_ids: ASSOCIATION_ARRAY_SCHEMA('Systems this schedule covers. Systems have no singular field; this array is the only way to associate them.'),
926
+ location_ids: ASSOCIATION_ARRAY_SCHEMA('Locations this schedule covers — use instead of location_id when there is more than one. Sending location_id alone replaces this with that single id.'),
903
927
  tasks: z
904
928
  .array(z.object({
905
929
  id: z.string().describe('Unique task ID (use a random string)'),
@@ -908,6 +932,7 @@ export function registerWriteTools(server, client) {
908
932
  }))
909
933
  .optional()
910
934
  .describe('Checklist of tasks for this PM schedule'),
935
+ form_template_id: FORM_TEMPLATE_LINK_SCHEMA,
911
936
  }, async (params) => {
912
937
  try {
913
938
  const result = await client.create('pm-schedules', buildBody(params));
@@ -971,9 +996,9 @@ export function registerWriteTools(server, client) {
971
996
  .describe('Meter interval — trigger every N units'),
972
997
  meter_unit: z.string().max(50).optional().describe('Meter unit (km, miles, hours, cycles)'),
973
998
  start_date: z.string().optional().describe('Start date (ISO 8601)'),
974
- asset_ids: z.array(z.string().uuid()).optional().describe('Array of asset IDs'),
975
- system_ids: z.array(z.string().uuid()).optional().describe('Array of system IDs'),
976
- location_ids: z.array(z.string().uuid()).optional().describe('Array of location IDs'),
999
+ asset_ids: ASSOCIATION_ARRAY_SCHEMA('Assets this schedule covers — use instead of asset_id when there is more than one.'),
1000
+ system_ids: ASSOCIATION_ARRAY_SCHEMA('Systems this schedule covers. Systems have no singular field; this array is the only way to associate them.'),
1001
+ location_ids: ASSOCIATION_ARRAY_SCHEMA('Locations this schedule covers — use instead of location_id when there is more than one. Sending location_id alone replaces this with that single id.'),
977
1002
  tasks: z
978
1003
  .array(z.object({
979
1004
  id: z.string().describe('Unique task ID'),
@@ -982,6 +1007,7 @@ export function registerWriteTools(server, client) {
982
1007
  }))
983
1008
  .optional()
984
1009
  .describe('Checklist of tasks for this PM schedule'),
1010
+ form_template_id: FORM_TEMPLATE_LINK_SCHEMA,
985
1011
  }, async ({ id, ...rest }) => {
986
1012
  try {
987
1013
  const result = await client.update('pm-schedules', id, buildBody(rest));
@@ -1066,6 +1092,7 @@ export function registerWriteTools(server, client) {
1066
1092
  .array(z.string().uuid())
1067
1093
  .optional()
1068
1094
  .describe('Default location IDs to seed on derived schedules'),
1095
+ form_template_id: FORM_TEMPLATE_LINK_SCHEMA,
1069
1096
  }, async (params) => {
1070
1097
  try {
1071
1098
  const result = await client.create('pm-templates', buildBody(normalizePmTemplateBody(params)));
@@ -1129,6 +1156,7 @@ export function registerWriteTools(server, client) {
1129
1156
  documents: z.array(z.record(z.unknown())).optional().describe('Document references'),
1130
1157
  asset_ids: z.array(z.string().uuid()).optional().describe('Default asset IDs'),
1131
1158
  location_ids: z.array(z.string().uuid()).optional().describe('Default location IDs'),
1159
+ form_template_id: FORM_TEMPLATE_LINK_SCHEMA,
1132
1160
  }, async ({ id, ...rest }) => {
1133
1161
  try {
1134
1162
  const result = await client.update('pm-templates', id, buildBody(normalizePmTemplateBody(rest)));
@@ -1153,10 +1181,11 @@ export function registerWriteTools(server, client) {
1153
1181
  server.tool('create_form_template', 'Create a form template — a reusable inspection, checklist, compliance, or survey definition. Step 1 of building a form: create it as a draft, then add questions with create_form_template_item (or bulk_create on form-template-items), then publish with update_form_template status=published (publishing is rejected if any question is malformed). Requires form_templates:write scope.', {
1154
1182
  name: z.string().min(1).max(500).describe('Form template name (required)'),
1155
1183
  description: z.string().max(5000).optional().describe('Description'),
1156
- category: z
1157
- .enum(FORM_TEMPLATE_CATEGORIES)
1184
+ work_category_id: z
1185
+ .string()
1186
+ .uuid()
1158
1187
  .optional()
1159
- .describe('Form category: inspection (default), task_checklist, compliance, or survey. Manufacturer-recommended equipment checks inspection.'),
1188
+ .describe('Work category ID — the same tenant-configured categories used by work orders (e.g. Electrical, Plumbing, HVAC). Look them up with list_work_categories and pick the closest match; omit if none fits.'),
1160
1189
  status: z
1161
1190
  .enum(FORM_TEMPLATE_STATUSES)
1162
1191
  .optional()
@@ -1174,10 +1203,11 @@ export function registerWriteTools(server, client) {
1174
1203
  id: z.string().uuid().describe('Form template ID'),
1175
1204
  name: z.string().min(1).max(500).optional().describe('Form template name'),
1176
1205
  description: z.string().max(5000).optional().describe('Description'),
1177
- category: z
1178
- .enum(FORM_TEMPLATE_CATEGORIES)
1206
+ work_category_id: z
1207
+ .string()
1208
+ .uuid()
1179
1209
  .optional()
1180
- .describe('Form category (task_checklist, inspection, compliance, or survey)'),
1210
+ .describe('Work category ID (look up with list_work_categories)'),
1181
1211
  status: z
1182
1212
  .enum(FORM_TEMPLATE_STATUSES)
1183
1213
  .optional()
@@ -1286,6 +1316,44 @@ export function registerWriteTools(server, client) {
1286
1316
  }
1287
1317
  });
1288
1318
  // ============================================================
1319
+ // 8d. Form responses — attach / detach (scope: form_responses)
1320
+ // ============================================================
1321
+ server.tool('create_form_response', "Attach a published form to one record so it can be filled in — the direct way to put an inspection or checklist on an existing work order. The form's questions are snapshotted at attach time, so later edits to the template never change a form already in progress. To put a form on every work order a PM schedule generates, set form_template_id on the schedule instead of calling this per work order. Answering and completing the form happen in the AssetLab app, not through this API. Requires form_responses:write scope.", {
1322
+ template_id: z
1323
+ .string()
1324
+ .uuid()
1325
+ .describe('Published form template ID — resolve via list_form_templates. A draft or archived template is rejected; publish it first with update_form_template status="published".'),
1326
+ subject_type: z
1327
+ .enum(['work_order', 'pm_schedule', 'infrastructure_asset', 'compliance_record', 'site'])
1328
+ .describe('What kind of record the form is being attached to'),
1329
+ subject_id: z
1330
+ .string()
1331
+ .uuid()
1332
+ .describe('ID of the record — resolve via the matching list tool (list_work_orders, list_pm_schedules, list_infrastructure_assets, list_compliance_records, list_sites)'),
1333
+ }, async (params) => {
1334
+ try {
1335
+ const result = await client.create('form-responses', buildBody(params));
1336
+ return formatResult(result);
1337
+ }
1338
+ catch (err) {
1339
+ return formatError(err);
1340
+ }
1341
+ });
1342
+ server.tool('delete_form_response', 'Remove a form from the record it is attached to, along with any answers already given. Use this before attaching a different form, since a record holds at most one form. Requires form_responses:write scope.', {
1343
+ id: z
1344
+ .string()
1345
+ .uuid()
1346
+ .describe('Form response ID — resolve via list_form_responses filtered by subject_id'),
1347
+ }, async ({ id }) => {
1348
+ try {
1349
+ const result = await client.remove('form-responses', id);
1350
+ return formatResult(result);
1351
+ }
1352
+ catch (err) {
1353
+ return formatError(err);
1354
+ }
1355
+ });
1356
+ // ============================================================
1289
1357
  // 9. Projects (scope: projects)
1290
1358
  // ============================================================
1291
1359
  server.tool('create_project', 'Create a new project. Requires projects:write scope.', {
@@ -4280,6 +4348,7 @@ export function registerWriteTools(server, client) {
4280
4348
  'pm-templates',
4281
4349
  'form-templates',
4282
4350
  'form-template-items',
4351
+ 'form-responses',
4283
4352
  'projects',
4284
4353
  'contracts',
4285
4354
  'invoices',