@assetlab/mcp-server 1.25.1 → 1.27.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,6 +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'];
24
+ const FORM_TEMPLATE_STATUSES = ['draft', 'published', 'archived'];
25
+ const FORM_ITEM_TYPES = [
26
+ 'section',
27
+ 'checkbox',
28
+ 'single_select',
29
+ 'multi_select',
30
+ 'number',
31
+ 'text',
32
+ 'photo',
33
+ ];
23
34
  function randomTaskId() {
24
35
  const g = globalThis;
25
36
  return g.crypto?.randomUUID?.() ?? `task_${Math.random().toString(36).slice(2, 12)}`;
@@ -584,13 +595,49 @@ export function registerWriteTools(server, client) {
584
595
  country: z.string().max(200).optional().describe('Country'),
585
596
  description: z.string().optional().describe('Description'),
586
597
  square_footage: z.number().min(0).optional().describe('Square footage'),
587
- cost_per_sqft: z.number().min(0).optional().describe('Cost per square foot'),
598
+ cost_per_sqft: z.number().min(0).optional().describe('Base cost per square foot'),
599
+ additional_cost_per_sqft: z
600
+ .number()
601
+ .min(0)
602
+ .optional()
603
+ .describe('Additional cost per square foot'),
604
+ operational_cost_per_sqft: z
605
+ .number()
606
+ .min(0)
607
+ .optional()
608
+ .describe('Operational cost per square foot'),
588
609
  year_built: z.number().int().min(1800).max(2100).optional().describe('Year built'),
589
610
  contact_name: z.string().max(500).optional().describe('Primary contact name'),
590
611
  contact_email: z.string().max(500).optional().describe('Contact email'),
591
612
  contact_phone: z.string().max(50).optional().describe('Contact phone'),
592
613
  owner_landlord: z.string().max(500).optional().describe('Property owner or landlord'),
593
614
  ownership_type: z.enum(['Owned', 'Leased', 'Managed']).optional().describe('Ownership type'),
615
+ lease_start_date: z.string().max(20).optional().describe('Lease start date (YYYY-MM-DD)'),
616
+ lease_end_date: z.string().max(20).optional().describe('Lease end date (YYYY-MM-DD)'),
617
+ renewal_option: z.string().max(500).optional().describe('Lease renewal option details'),
618
+ lease_details: z.string().optional().describe('Free-text lease details / notes'),
619
+ insurance_provider: z.string().max(500).optional().describe('Insurance provider name'),
620
+ insurance_policy_number: z.string().max(200).optional().describe('Insurance policy number'),
621
+ property_manager_company: z
622
+ .string()
623
+ .max(500)
624
+ .optional()
625
+ .describe('Property management company name'),
626
+ property_manager_contact_name: z
627
+ .string()
628
+ .max(500)
629
+ .optional()
630
+ .describe('Property management contact name'),
631
+ property_manager_contact_email: z
632
+ .string()
633
+ .max(500)
634
+ .optional()
635
+ .describe('Property management contact email'),
636
+ property_manager_contact_phone: z
637
+ .string()
638
+ .max(50)
639
+ .optional()
640
+ .describe('Property management contact phone'),
594
641
  }, async (params) => {
595
642
  try {
596
643
  const result = await client.create('sites', buildBody(params));
@@ -610,13 +657,49 @@ export function registerWriteTools(server, client) {
610
657
  country: z.string().max(200).optional().describe('Country'),
611
658
  description: z.string().optional().describe('Description'),
612
659
  square_footage: z.number().min(0).optional().describe('Square footage'),
613
- cost_per_sqft: z.number().min(0).optional().describe('Cost per square foot'),
660
+ cost_per_sqft: z.number().min(0).optional().describe('Base cost per square foot'),
661
+ additional_cost_per_sqft: z
662
+ .number()
663
+ .min(0)
664
+ .optional()
665
+ .describe('Additional cost per square foot'),
666
+ operational_cost_per_sqft: z
667
+ .number()
668
+ .min(0)
669
+ .optional()
670
+ .describe('Operational cost per square foot'),
614
671
  year_built: z.number().int().min(1800).max(2100).optional().describe('Year built'),
615
672
  contact_name: z.string().max(500).optional().describe('Primary contact name'),
616
673
  contact_email: z.string().max(500).optional().describe('Contact email'),
617
674
  contact_phone: z.string().max(50).optional().describe('Contact phone'),
618
675
  owner_landlord: z.string().max(500).optional().describe('Property owner or landlord'),
619
676
  ownership_type: z.enum(['Owned', 'Leased', 'Managed']).optional().describe('Ownership type'),
677
+ lease_start_date: z.string().max(20).optional().describe('Lease start date (YYYY-MM-DD)'),
678
+ lease_end_date: z.string().max(20).optional().describe('Lease end date (YYYY-MM-DD)'),
679
+ renewal_option: z.string().max(500).optional().describe('Lease renewal option details'),
680
+ lease_details: z.string().optional().describe('Free-text lease details / notes'),
681
+ insurance_provider: z.string().max(500).optional().describe('Insurance provider name'),
682
+ insurance_policy_number: z.string().max(200).optional().describe('Insurance policy number'),
683
+ property_manager_company: z
684
+ .string()
685
+ .max(500)
686
+ .optional()
687
+ .describe('Property management company name'),
688
+ property_manager_contact_name: z
689
+ .string()
690
+ .max(500)
691
+ .optional()
692
+ .describe('Property management contact name'),
693
+ property_manager_contact_email: z
694
+ .string()
695
+ .max(500)
696
+ .optional()
697
+ .describe('Property management contact email'),
698
+ property_manager_contact_phone: z
699
+ .string()
700
+ .max(50)
701
+ .optional()
702
+ .describe('Property management contact phone'),
620
703
  }, async ({ id, ...rest }) => {
621
704
  try {
622
705
  const result = await client.update('sites', id, buildBody(rest));
@@ -1065,6 +1148,144 @@ export function registerWriteTools(server, client) {
1065
1148
  }
1066
1149
  });
1067
1150
  // ============================================================
1151
+ // 8c. Forms & Inspections (scope: form_templates, form_template_items)
1152
+ // ============================================================
1153
+ 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
+ name: z.string().min(1).max(500).describe('Form template name (required)'),
1155
+ description: z.string().max(5000).optional().describe('Description'),
1156
+ category: z
1157
+ .enum(FORM_TEMPLATE_CATEGORIES)
1158
+ .optional()
1159
+ .describe('Form category: inspection (default), task_checklist, compliance, or survey. Manufacturer-recommended equipment checks → inspection.'),
1160
+ status: z
1161
+ .enum(FORM_TEMPLATE_STATUSES)
1162
+ .optional()
1163
+ .describe('Publication status — leave as draft (default) until all questions are added.'),
1164
+ }, async (params) => {
1165
+ try {
1166
+ const result = await client.create('form-templates', buildBody(params));
1167
+ return formatResult(result);
1168
+ }
1169
+ catch (err) {
1170
+ return formatError(err);
1171
+ }
1172
+ });
1173
+ server.tool('update_form_template', 'Update an existing form template by ID. Requires form_templates:write scope.', {
1174
+ id: z.string().uuid().describe('Form template ID'),
1175
+ name: z.string().min(1).max(500).optional().describe('Form template name'),
1176
+ description: z.string().max(5000).optional().describe('Description'),
1177
+ category: z
1178
+ .enum(FORM_TEMPLATE_CATEGORIES)
1179
+ .optional()
1180
+ .describe('Form category (task_checklist, inspection, compliance, or survey)'),
1181
+ status: z
1182
+ .enum(FORM_TEMPLATE_STATUSES)
1183
+ .optional()
1184
+ .describe('Publication status (draft, published, or archived)'),
1185
+ }, async ({ id, ...rest }) => {
1186
+ try {
1187
+ const result = await client.update('form-templates', id, buildBody(rest));
1188
+ return formatResult(result);
1189
+ }
1190
+ catch (err) {
1191
+ return formatError(err);
1192
+ }
1193
+ });
1194
+ server.tool('delete_form_template', 'Delete a form template by ID. Requires form_templates:write scope.', { id: z.string().uuid().describe('Form template ID') }, async ({ id }) => {
1195
+ try {
1196
+ const result = await client.remove('form-templates', id);
1197
+ return formatResult(result);
1198
+ }
1199
+ catch (err) {
1200
+ return formatError(err);
1201
+ }
1202
+ });
1203
+ server.tool('create_form_template_item', 'Add one question (item) to a form template. Build a form by calling this once per question in order, or use bulk_create on form-template-items. Requires form_template_items:write scope.', {
1204
+ template_id: z.string().uuid().describe('Form template ID — resolve via list_form_templates'),
1205
+ item_key: z
1206
+ .string()
1207
+ .min(1)
1208
+ .max(200)
1209
+ .optional()
1210
+ .describe('Optional stable key, unique within the template. OMIT IT and the server derives one from the label (recommended). Only set it when a later item’s visible_when must reference this one — then use a short slug like "compressor_status".'),
1211
+ sort_order: z
1212
+ .number()
1213
+ .int()
1214
+ .min(0)
1215
+ .describe('Display order within the template (0-based; questions render in this order)'),
1216
+ item_type: z
1217
+ .enum(FORM_ITEM_TYPES)
1218
+ .describe('Pick by the answer you want: single_select = exactly one choice (Pass/Fail, Yes/No, Yes/No/N-A, or custom — supply options); multi_select = pick several (supply options); number = a numeric reading/count (use config.min/max/unit/integer); checkbox = a single done/not-done tick; text = free comment (config.multiline for long text); photo = photo evidence (config.maxPhotos); section = a non-answerable heading that groups the questions under it.'),
1219
+ label: z.string().min(1).max(2000).describe('Question text / prompt shown to the user'),
1220
+ help_text: z.string().max(5000).optional().describe('Optional hint shown under the label'),
1221
+ required: z
1222
+ .boolean()
1223
+ .optional()
1224
+ .describe('Whether an answer is required to complete the form (default false)'),
1225
+ options: z
1226
+ .array(z.object({ value: z.string(), label: z.string() }))
1227
+ .optional()
1228
+ .describe('REQUIRED for single_select/multi_select: at least 2 choices as { value, label } with unique values. value is a machine slug (e.g. "fail"), label is shown to the user (e.g. "Fail"). Omit for other types.'),
1229
+ config: z
1230
+ .record(z.unknown())
1231
+ .optional()
1232
+ .describe('Per-type settings. number: { min, max, unit, integer, decimals }. text: { multiline, maxLength, placeholder }. multi_select: { minSelections, maxSelections }. photo: { minPhotos, maxPhotos }.'),
1233
+ visible_when: z
1234
+ .object({ itemKey: z.string(), op: z.string(), value: z.unknown().optional() })
1235
+ .nullable()
1236
+ .optional()
1237
+ .describe('Optional conditional visibility { itemKey, op, value }. itemKey must reference an EARLIER item’s key (set that item’s item_key explicitly). Operators by referenced type — single_select/checkbox: equals, not_equals, in, not_in, is_answered, is_blank; number: gt, lt, gte, lte, equals, not_equals, is_answered, is_blank; text: is_answered, is_blank, equals, not_equals; multi_select: in, not_in, is_answered, is_blank. e.g. show a "Details" text item only when item "compressor_status" equals "fail".'),
1238
+ }, async (params) => {
1239
+ try {
1240
+ const result = await client.create('form-template-items', buildBody(params));
1241
+ return formatResult(result);
1242
+ }
1243
+ catch (err) {
1244
+ return formatError(err);
1245
+ }
1246
+ });
1247
+ server.tool('update_form_template_item', 'Update an existing form template item by ID. template_id and item_key are immutable and cannot be changed. Requires form_template_items:write scope.', {
1248
+ id: z.string().uuid().describe('Form template item ID'),
1249
+ sort_order: z.number().int().min(0).optional().describe('Display order within the template'),
1250
+ item_type: z
1251
+ .enum(FORM_ITEM_TYPES)
1252
+ .optional()
1253
+ .describe('Item type (section, checkbox, single_select, multi_select, number, text, photo)'),
1254
+ label: z.string().min(1).max(2000).optional().describe('Question label / prompt'),
1255
+ help_text: z.string().max(5000).optional().describe('Help text shown under the label'),
1256
+ required: z.boolean().optional().describe('Whether an answer is required'),
1257
+ options: z
1258
+ .array(z.object({ value: z.string(), label: z.string() }))
1259
+ .optional()
1260
+ .describe('Choices for single_select / multi_select items'),
1261
+ config: z
1262
+ .record(z.unknown())
1263
+ .optional()
1264
+ .describe('Per-type configuration (e.g. { min, max, unit, multiline })'),
1265
+ visible_when: z
1266
+ .object({ itemKey: z.string(), op: z.string(), value: z.unknown().optional() })
1267
+ .nullable()
1268
+ .optional()
1269
+ .describe('Conditional-visibility rule referencing another item'),
1270
+ }, async ({ id, ...rest }) => {
1271
+ try {
1272
+ const result = await client.update('form-template-items', id, buildBody(rest));
1273
+ return formatResult(result);
1274
+ }
1275
+ catch (err) {
1276
+ return formatError(err);
1277
+ }
1278
+ });
1279
+ server.tool('delete_form_template_item', 'Delete a form template item by ID. Requires form_template_items:write scope.', { id: z.string().uuid().describe('Form template item ID') }, async ({ id }) => {
1280
+ try {
1281
+ const result = await client.remove('form-template-items', id);
1282
+ return formatResult(result);
1283
+ }
1284
+ catch (err) {
1285
+ return formatError(err);
1286
+ }
1287
+ });
1288
+ // ============================================================
1068
1289
  // 9. Projects (scope: projects)
1069
1290
  // ============================================================
1070
1291
  server.tool('create_project', 'Create a new project. Requires projects:write scope.', {
@@ -4057,6 +4278,8 @@ export function registerWriteTools(server, client) {
4057
4278
  'system-classes',
4058
4279
  'pm-schedules',
4059
4280
  'pm-templates',
4281
+ 'form-templates',
4282
+ 'form-template-items',
4060
4283
  'projects',
4061
4284
  'contracts',
4062
4285
  'invoices',