@assetlab/mcp-server 1.19.7 → 1.21.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.
@@ -6,16 +6,10 @@
6
6
  * Write tools require API keys with the appropriate :write scope.
7
7
  */
8
8
  import { z } from 'zod';
9
+ import { formatError, formatResult } from './response-shaping.js';
9
10
  // ---------------------------------------------------------------------------
10
11
  // Helpers
11
12
  // ---------------------------------------------------------------------------
12
- function formatResult(data) {
13
- return { content: [{ type: 'text', text: JSON.stringify(data, null, 2) }] };
14
- }
15
- function formatError(err) {
16
- const message = err instanceof Error ? err.message : String(err);
17
- return { content: [{ type: 'text', text: `Error: ${message}` }], isError: true };
18
- }
19
13
  /** Strip undefined values so the API only receives explicitly-set fields. */
20
14
  function buildBody(params) {
21
15
  const body = {};
@@ -48,8 +42,8 @@ function coerceNumber(value) {
48
42
  function normalizePmTemplateBody(params) {
49
43
  const out = { ...params };
50
44
  if (Array.isArray(out.tasks)) {
51
- out.tasks = out.tasks.map((raw) => {
52
- const t = (raw && typeof raw === 'object') ? raw : {};
45
+ out.tasks = out.tasks.map(raw => {
46
+ const t = raw && typeof raw === 'object' ? raw : {};
53
47
  const id = typeof t.id === 'string' && t.id.trim() !== '' ? t.id : randomTaskId();
54
48
  const description = typeof t.description === 'string' ? t.description : '';
55
49
  const completed = typeof t.completed === 'boolean' ? t.completed : false;
@@ -57,8 +51,8 @@ function normalizePmTemplateBody(params) {
57
51
  });
58
52
  }
59
53
  if (Array.isArray(out.resources)) {
60
- out.resources = out.resources.map((raw) => {
61
- const r = (raw && typeof raw === 'object') ? raw : {};
54
+ out.resources = out.resources.map(raw => {
55
+ const r = raw && typeof raw === 'object' ? raw : {};
62
56
  const res = {};
63
57
  if (typeof r.name === 'string')
64
58
  res.name = r.name;
@@ -89,11 +83,22 @@ export function registerWriteTools(server, client) {
89
83
  title: z.string().min(1).max(500).describe('Work order title (required)'),
90
84
  description: z.string().optional().describe('Detailed description'),
91
85
  priority: z.enum(['LOW', 'MEDIUM', 'HIGH', 'URGENT']).optional().describe('Priority level'),
92
- status: z.enum(['NEW', 'IN_PROGRESS', 'ON_HOLD', 'REJECTED', 'COMPLETED', 'CANCELLED']).optional().describe('Status'),
86
+ status: z
87
+ .enum(['NEW', 'IN_PROGRESS', 'ON_HOLD', 'REJECTED', 'COMPLETED', 'CANCELLED'])
88
+ .optional()
89
+ .describe('Status'),
93
90
  type: z.enum(['PM', 'REACTIVE']).optional().describe('Work order type'),
94
91
  site_id: z.string().uuid().optional().describe('Site ID — resolve first via list_sites'),
95
- building_id: z.string().uuid().optional().describe('Building ID — resolve second via list_buildings filtered by site_id'),
96
- location_id: z.string().uuid().optional().describe('Location ID — resolve last via list_locations filtered by building_id'),
92
+ building_id: z
93
+ .string()
94
+ .uuid()
95
+ .optional()
96
+ .describe('Building ID — resolve second via list_buildings filtered by site_id'),
97
+ location_id: z
98
+ .string()
99
+ .uuid()
100
+ .optional()
101
+ .describe('Location ID — resolve last via list_locations filtered by building_id'),
97
102
  asset_id: z.string().uuid().optional().describe('Asset ID'),
98
103
  start_date: z.string().optional().describe('Start date (ISO 8601)'),
99
104
  due_date: z.string().optional().describe('Due date (ISO 8601)'),
@@ -101,9 +106,20 @@ export function registerWriteTools(server, client) {
101
106
  estimated_cost: z.number().min(0).optional().describe('Estimated cost'),
102
107
  work_category_id: z.string().uuid().optional().describe('Work category ID'),
103
108
  assigned_to: z.string().optional().describe('Assigned user ID (mapped to assignees array)'),
104
- assignees: z.array(z.string()).optional().describe('Array of assigned user IDs (alternative to assigned_to for multiple assignees)'),
105
- image_url: z.string().max(2000).optional().describe('Image URL (upload via create_upload_url with bucket "attachments", then set this to the public_url)'),
106
- meter_reading: z.number().min(0).optional().describe('Meter/odometer reading at time of service'),
109
+ assignees: z
110
+ .array(z.string())
111
+ .optional()
112
+ .describe('Array of assigned user IDs (alternative to assigned_to for multiple assignees)'),
113
+ image_url: z
114
+ .string()
115
+ .max(2000)
116
+ .optional()
117
+ .describe('Image URL (upload via create_upload_url with bucket "attachments", then set this to the public_url)'),
118
+ meter_reading: z
119
+ .number()
120
+ .min(0)
121
+ .optional()
122
+ .describe('Meter/odometer reading at time of service'),
107
123
  meter_unit: z.string().max(50).optional().describe('Meter unit (km, miles, hours, cycles)'),
108
124
  }, async (params) => {
109
125
  try {
@@ -119,11 +135,22 @@ export function registerWriteTools(server, client) {
119
135
  title: z.string().min(1).max(500).optional().describe('Work order title'),
120
136
  description: z.string().optional().describe('Detailed description'),
121
137
  priority: z.enum(['LOW', 'MEDIUM', 'HIGH', 'URGENT']).optional().describe('Priority level'),
122
- status: z.enum(['NEW', 'IN_PROGRESS', 'ON_HOLD', 'REJECTED', 'COMPLETED', 'CANCELLED']).optional().describe('Status'),
138
+ status: z
139
+ .enum(['NEW', 'IN_PROGRESS', 'ON_HOLD', 'REJECTED', 'COMPLETED', 'CANCELLED'])
140
+ .optional()
141
+ .describe('Status'),
123
142
  type: z.enum(['PM', 'REACTIVE']).optional().describe('Work order type'),
124
143
  site_id: z.string().uuid().optional().describe('Site ID — resolve first via list_sites'),
125
- building_id: z.string().uuid().optional().describe('Building ID — resolve second via list_buildings filtered by site_id'),
126
- location_id: z.string().uuid().optional().describe('Location ID — resolve last via list_locations filtered by building_id'),
144
+ building_id: z
145
+ .string()
146
+ .uuid()
147
+ .optional()
148
+ .describe('Building ID — resolve second via list_buildings filtered by site_id'),
149
+ location_id: z
150
+ .string()
151
+ .uuid()
152
+ .optional()
153
+ .describe('Location ID — resolve last via list_locations filtered by building_id'),
127
154
  asset_id: z.string().uuid().optional().describe('Asset ID'),
128
155
  start_date: z.string().optional().describe('Start date (ISO 8601)'),
129
156
  due_date: z.string().optional().describe('Due date (ISO 8601)'),
@@ -131,9 +158,20 @@ export function registerWriteTools(server, client) {
131
158
  estimated_cost: z.number().min(0).optional().describe('Estimated cost'),
132
159
  work_category_id: z.string().uuid().optional().describe('Work category ID'),
133
160
  assigned_to: z.string().optional().describe('Assigned user ID (mapped to assignees array)'),
134
- assignees: z.array(z.string()).optional().describe('Array of assigned user IDs (alternative to assigned_to for multiple assignees)'),
135
- image_url: z.string().max(2000).optional().describe('Image URL (upload via create_upload_url with bucket "attachments", then set this to the public_url)'),
136
- meter_reading: z.number().min(0).optional().describe('Meter/odometer reading at time of service'),
161
+ assignees: z
162
+ .array(z.string())
163
+ .optional()
164
+ .describe('Array of assigned user IDs (alternative to assigned_to for multiple assignees)'),
165
+ image_url: z
166
+ .string()
167
+ .max(2000)
168
+ .optional()
169
+ .describe('Image URL (upload via create_upload_url with bucket "attachments", then set this to the public_url)'),
170
+ meter_reading: z
171
+ .number()
172
+ .min(0)
173
+ .optional()
174
+ .describe('Meter/odometer reading at time of service'),
137
175
  meter_unit: z.string().max(50).optional().describe('Meter unit (km, miles, hours, cycles)'),
138
176
  }, async ({ id, ...rest }) => {
139
177
  try {
@@ -159,23 +197,54 @@ export function registerWriteTools(server, client) {
159
197
  server.tool('create_asset', 'Create a new asset. Requires assets:write scope. IMPORTANT — Location hierarchy: always resolve top-down by calling list_sites first, then list_buildings filtered by site_id, then list_locations filtered by building_id. Provide all three IDs (site_id, building_id, location_id) explicitly. System hierarchy: similarly resolve via list_system_classes → list_system_groups → list_systems.', {
160
198
  name: z.string().min(1).max(500).describe('Asset name (required)'),
161
199
  description: z.string().optional().describe('Description'),
162
- asset_id: z.string().max(100).optional().describe('Custom asset identifier (unique per tenant)'),
200
+ asset_id: z
201
+ .string()
202
+ .max(100)
203
+ .optional()
204
+ .describe('Custom asset identifier (unique per tenant)'),
163
205
  asset_type_id: z.string().uuid().optional().describe('Asset type ID (from asset_types)'),
164
- manufacturer_id: z.string().uuid().optional().describe('Manufacturer ID (from manufacturers)'),
206
+ manufacturer_id: z
207
+ .string()
208
+ .uuid()
209
+ .optional()
210
+ .describe('Manufacturer ID (from manufacturers)'),
165
211
  model: z.string().max(500).optional().describe('Model name/number'),
166
212
  serial_number: z.string().max(200).optional().describe('Serial number'),
167
213
  purchase_cost: z.number().min(0).optional().describe('Purchase cost'),
168
214
  purchase_date: z.string().optional().describe('Purchase date (ISO 8601)'),
169
215
  expected_lifetime_years: z.number().min(0).optional().describe('Expected lifetime in years'),
170
216
  condition_score: z.number().min(0).max(100).optional().describe('Condition score (0-100)'),
171
- risk_factor: z.enum(['CRITICAL', 'HIGH', 'MEDIUM', 'LOW']).optional().describe('Risk factor (CRITICAL, HIGH, MEDIUM, LOW)'),
217
+ risk_factor: z
218
+ .enum(['CRITICAL', 'HIGH', 'MEDIUM', 'LOW'])
219
+ .optional()
220
+ .describe('Risk factor (CRITICAL, HIGH, MEDIUM, LOW)'),
172
221
  status_id: z.string().max(100).optional().describe('Status identifier'),
173
222
  site_id: z.string().uuid().optional().describe('Site ID — resolve first via list_sites'),
174
- building_id: z.string().uuid().optional().describe('Building ID — resolve second via list_buildings filtered by site_id'),
175
- location_id: z.string().uuid().optional().describe('Location ID — resolve last via list_locations filtered by building_id'),
176
- system_class_id: z.string().uuid().optional().describe('System class ID — resolve first via list_system_classes'),
177
- system_group_id: z.string().uuid().optional().describe('System group ID — resolve second via list_system_groups filtered by system_class_id'),
178
- system_id: z.string().uuid().optional().describe('System ID — resolve last via list_systems filtered by system_group_id'),
223
+ building_id: z
224
+ .string()
225
+ .uuid()
226
+ .optional()
227
+ .describe('Building ID — resolve second via list_buildings filtered by site_id'),
228
+ location_id: z
229
+ .string()
230
+ .uuid()
231
+ .optional()
232
+ .describe('Location ID — resolve last via list_locations filtered by building_id'),
233
+ system_class_id: z
234
+ .string()
235
+ .uuid()
236
+ .optional()
237
+ .describe('System class ID — resolve first via list_system_classes'),
238
+ system_group_id: z
239
+ .string()
240
+ .uuid()
241
+ .optional()
242
+ .describe('System group ID — resolve second via list_system_groups filtered by system_class_id'),
243
+ system_id: z
244
+ .string()
245
+ .uuid()
246
+ .optional()
247
+ .describe('System ID — resolve last via list_systems filtered by system_group_id'),
179
248
  image_url: z.string().max(2000).optional().describe('Image URL'),
180
249
  last_maintenance_date: z.string().optional().describe('Last maintenance date (ISO 8601)'),
181
250
  quantity: z.number().min(0).optional().describe('Quantity'),
@@ -183,15 +252,49 @@ export function registerWriteTools(server, client) {
183
252
  unit_replacement_value: z.number().min(0).optional().describe('Unit replacement value'),
184
253
  cost_per_sq_ft: z.number().min(0).optional().describe('Cost per square foot'),
185
254
  salvage_value: z.number().min(0).optional().describe('Salvage value'),
186
- salvage_value_percentage: z.number().min(0).max(100).optional().describe('Salvage value percentage (0-100)'),
187
- consequence_of_failure_score: z.number().int().min(0).optional().describe('Consequence of failure score'),
188
- likelihood_of_failure_score: z.number().int().min(0).optional().describe('Likelihood of failure score'),
189
- safety_impact: z.enum(['LOW', 'MEDIUM', 'HIGH', 'CRITICAL']).optional().describe('Safety impact level (LOW, MEDIUM, HIGH, CRITICAL)'),
190
- service_impact: z.enum(['LOW', 'MEDIUM', 'HIGH', 'CRITICAL']).optional().describe('Service impact level (LOW, MEDIUM, HIGH, CRITICAL)'),
191
- environmental_impact: z.enum(['LOW', 'MEDIUM', 'HIGH', 'CRITICAL']).optional().describe('Environmental impact level (LOW, MEDIUM, HIGH, CRITICAL)'),
192
- regulatory_impact: z.enum(['LOW', 'MEDIUM', 'HIGH', 'CRITICAL']).optional().describe('Regulatory impact level (LOW, MEDIUM, HIGH, CRITICAL)'),
193
- reputation_impact: z.enum(['LOW', 'MEDIUM', 'HIGH', 'CRITICAL']).optional().describe('Reputation impact level (LOW, MEDIUM, HIGH, CRITICAL)'),
194
- current_meter_reading: z.number().min(0).optional().describe('Current meter/odometer reading'),
255
+ salvage_value_percentage: z
256
+ .number()
257
+ .min(0)
258
+ .max(100)
259
+ .optional()
260
+ .describe('Salvage value percentage (0-100)'),
261
+ consequence_of_failure_score: z
262
+ .number()
263
+ .int()
264
+ .min(0)
265
+ .optional()
266
+ .describe('Consequence of failure score'),
267
+ likelihood_of_failure_score: z
268
+ .number()
269
+ .int()
270
+ .min(0)
271
+ .optional()
272
+ .describe('Likelihood of failure score'),
273
+ safety_impact: z
274
+ .enum(['LOW', 'MEDIUM', 'HIGH', 'CRITICAL'])
275
+ .optional()
276
+ .describe('Safety impact level (LOW, MEDIUM, HIGH, CRITICAL)'),
277
+ service_impact: z
278
+ .enum(['LOW', 'MEDIUM', 'HIGH', 'CRITICAL'])
279
+ .optional()
280
+ .describe('Service impact level (LOW, MEDIUM, HIGH, CRITICAL)'),
281
+ environmental_impact: z
282
+ .enum(['LOW', 'MEDIUM', 'HIGH', 'CRITICAL'])
283
+ .optional()
284
+ .describe('Environmental impact level (LOW, MEDIUM, HIGH, CRITICAL)'),
285
+ regulatory_impact: z
286
+ .enum(['LOW', 'MEDIUM', 'HIGH', 'CRITICAL'])
287
+ .optional()
288
+ .describe('Regulatory impact level (LOW, MEDIUM, HIGH, CRITICAL)'),
289
+ reputation_impact: z
290
+ .enum(['LOW', 'MEDIUM', 'HIGH', 'CRITICAL'])
291
+ .optional()
292
+ .describe('Reputation impact level (LOW, MEDIUM, HIGH, CRITICAL)'),
293
+ current_meter_reading: z
294
+ .number()
295
+ .min(0)
296
+ .optional()
297
+ .describe('Current meter/odometer reading'),
195
298
  meter_unit: z.string().max(50).optional().describe('Meter unit (km, miles, hours, cycles)'),
196
299
  }, async (params) => {
197
300
  try {
@@ -206,40 +309,105 @@ export function registerWriteTools(server, client) {
206
309
  id: z.string().uuid().describe('Asset ID'),
207
310
  name: z.string().min(1).max(500).optional().describe('Asset name'),
208
311
  description: z.string().optional().describe('Description'),
209
- asset_id: z.string().max(100).optional().describe('Custom asset identifier (unique per tenant)'),
312
+ asset_id: z
313
+ .string()
314
+ .max(100)
315
+ .optional()
316
+ .describe('Custom asset identifier (unique per tenant)'),
210
317
  asset_type_id: z.string().uuid().optional().describe('Asset type ID (from asset_types)'),
211
- manufacturer_id: z.string().uuid().optional().describe('Manufacturer ID (from manufacturers)'),
318
+ manufacturer_id: z
319
+ .string()
320
+ .uuid()
321
+ .optional()
322
+ .describe('Manufacturer ID (from manufacturers)'),
212
323
  model: z.string().max(500).optional().describe('Model name/number'),
213
324
  serial_number: z.string().max(200).optional().describe('Serial number'),
214
325
  purchase_cost: z.number().min(0).optional().describe('Purchase cost'),
215
326
  purchase_date: z.string().optional().describe('Purchase date (ISO 8601)'),
216
327
  expected_lifetime_years: z.number().min(0).optional().describe('Expected lifetime in years'),
217
328
  condition_score: z.number().min(0).max(100).optional().describe('Condition score (0-100)'),
218
- risk_factor: z.enum(['CRITICAL', 'HIGH', 'MEDIUM', 'LOW']).optional().describe('Risk factor (CRITICAL, HIGH, MEDIUM, LOW)'),
329
+ risk_factor: z
330
+ .enum(['CRITICAL', 'HIGH', 'MEDIUM', 'LOW'])
331
+ .optional()
332
+ .describe('Risk factor (CRITICAL, HIGH, MEDIUM, LOW)'),
219
333
  status_id: z.string().max(100).optional().describe('Status identifier'),
220
334
  site_id: z.string().uuid().optional().describe('Site ID — resolve first via list_sites'),
221
- building_id: z.string().uuid().optional().describe('Building ID — resolve second via list_buildings filtered by site_id'),
222
- location_id: z.string().uuid().optional().describe('Location ID — resolve last via list_locations filtered by building_id'),
223
- system_class_id: z.string().uuid().optional().describe('System class ID — resolve first via list_system_classes'),
224
- system_group_id: z.string().uuid().optional().describe('System group ID — resolve second via list_system_groups filtered by system_class_id'),
225
- system_id: z.string().uuid().optional().describe('System ID — resolve last via list_systems filtered by system_group_id'),
335
+ building_id: z
336
+ .string()
337
+ .uuid()
338
+ .optional()
339
+ .describe('Building ID — resolve second via list_buildings filtered by site_id'),
340
+ location_id: z
341
+ .string()
342
+ .uuid()
343
+ .optional()
344
+ .describe('Location ID — resolve last via list_locations filtered by building_id'),
345
+ system_class_id: z
346
+ .string()
347
+ .uuid()
348
+ .optional()
349
+ .describe('System class ID — resolve first via list_system_classes'),
350
+ system_group_id: z
351
+ .string()
352
+ .uuid()
353
+ .optional()
354
+ .describe('System group ID — resolve second via list_system_groups filtered by system_class_id'),
355
+ system_id: z
356
+ .string()
357
+ .uuid()
358
+ .optional()
359
+ .describe('System ID — resolve last via list_systems filtered by system_group_id'),
226
360
  image_url: z.string().max(2000).optional().describe('Image URL'),
227
361
  last_maintenance_date: z.string().optional().describe('Last maintenance date (ISO 8601)'),
228
- current_meter_reading: z.number().min(0).optional().describe('Current meter/odometer reading'),
362
+ current_meter_reading: z
363
+ .number()
364
+ .min(0)
365
+ .optional()
366
+ .describe('Current meter/odometer reading'),
229
367
  meter_unit: z.string().max(50).optional().describe('Meter unit (km, miles, hours, cycles)'),
230
368
  quantity: z.number().min(0).optional().describe('Quantity'),
231
369
  unit_of_measure: z.string().max(100).optional().describe('Unit of measure'),
232
370
  unit_replacement_value: z.number().min(0).optional().describe('Unit replacement value'),
233
371
  cost_per_sq_ft: z.number().min(0).optional().describe('Cost per square foot'),
234
372
  salvage_value: z.number().min(0).optional().describe('Salvage value'),
235
- salvage_value_percentage: z.number().min(0).max(100).optional().describe('Salvage value percentage (0-100)'),
236
- consequence_of_failure_score: z.number().int().min(0).optional().describe('Consequence of failure score'),
237
- likelihood_of_failure_score: z.number().int().min(0).optional().describe('Likelihood of failure score'),
238
- safety_impact: z.enum(['LOW', 'MEDIUM', 'HIGH', 'CRITICAL']).optional().describe('Safety impact level (LOW, MEDIUM, HIGH, CRITICAL)'),
239
- service_impact: z.enum(['LOW', 'MEDIUM', 'HIGH', 'CRITICAL']).optional().describe('Service impact level (LOW, MEDIUM, HIGH, CRITICAL)'),
240
- environmental_impact: z.enum(['LOW', 'MEDIUM', 'HIGH', 'CRITICAL']).optional().describe('Environmental impact level (LOW, MEDIUM, HIGH, CRITICAL)'),
241
- regulatory_impact: z.enum(['LOW', 'MEDIUM', 'HIGH', 'CRITICAL']).optional().describe('Regulatory impact level (LOW, MEDIUM, HIGH, CRITICAL)'),
242
- reputation_impact: z.enum(['LOW', 'MEDIUM', 'HIGH', 'CRITICAL']).optional().describe('Reputation impact level (LOW, MEDIUM, HIGH, CRITICAL)'),
373
+ salvage_value_percentage: z
374
+ .number()
375
+ .min(0)
376
+ .max(100)
377
+ .optional()
378
+ .describe('Salvage value percentage (0-100)'),
379
+ consequence_of_failure_score: z
380
+ .number()
381
+ .int()
382
+ .min(0)
383
+ .optional()
384
+ .describe('Consequence of failure score'),
385
+ likelihood_of_failure_score: z
386
+ .number()
387
+ .int()
388
+ .min(0)
389
+ .optional()
390
+ .describe('Likelihood of failure score'),
391
+ safety_impact: z
392
+ .enum(['LOW', 'MEDIUM', 'HIGH', 'CRITICAL'])
393
+ .optional()
394
+ .describe('Safety impact level (LOW, MEDIUM, HIGH, CRITICAL)'),
395
+ service_impact: z
396
+ .enum(['LOW', 'MEDIUM', 'HIGH', 'CRITICAL'])
397
+ .optional()
398
+ .describe('Service impact level (LOW, MEDIUM, HIGH, CRITICAL)'),
399
+ environmental_impact: z
400
+ .enum(['LOW', 'MEDIUM', 'HIGH', 'CRITICAL'])
401
+ .optional()
402
+ .describe('Environmental impact level (LOW, MEDIUM, HIGH, CRITICAL)'),
403
+ regulatory_impact: z
404
+ .enum(['LOW', 'MEDIUM', 'HIGH', 'CRITICAL'])
405
+ .optional()
406
+ .describe('Regulatory impact level (LOW, MEDIUM, HIGH, CRITICAL)'),
407
+ reputation_impact: z
408
+ .enum(['LOW', 'MEDIUM', 'HIGH', 'CRITICAL'])
409
+ .optional()
410
+ .describe('Reputation impact level (LOW, MEDIUM, HIGH, CRITICAL)'),
243
411
  }, async ({ id, ...rest }) => {
244
412
  try {
245
413
  const result = await client.update('assets', id, buildBody(rest));
@@ -265,10 +433,21 @@ export function registerWriteTools(server, client) {
265
433
  title: z.string().min(1).max(500).describe('Work request title (required)'),
266
434
  description: z.string().optional().describe('Description'),
267
435
  priority: z.enum(['LOW', 'MEDIUM', 'HIGH', 'CRITICAL']).optional().describe('Priority level'),
268
- status: z.enum(['SUBMITTED', 'APPROVED', 'REJECTED', 'CONVERTED']).optional().describe('Status'),
436
+ status: z
437
+ .enum(['SUBMITTED', 'APPROVED', 'REJECTED', 'CONVERTED'])
438
+ .optional()
439
+ .describe('Status'),
269
440
  site_id: z.string().uuid().optional().describe('Site ID — resolve first via list_sites'),
270
- building_id: z.string().uuid().optional().describe('Building ID — resolve second via list_buildings filtered by site_id'),
271
- location_id: z.string().uuid().optional().describe('Location ID — resolve last via list_locations filtered by building_id'),
441
+ building_id: z
442
+ .string()
443
+ .uuid()
444
+ .optional()
445
+ .describe('Building ID — resolve second via list_buildings filtered by site_id'),
446
+ location_id: z
447
+ .string()
448
+ .uuid()
449
+ .optional()
450
+ .describe('Location ID — resolve last via list_locations filtered by building_id'),
272
451
  asset_id: z.string().uuid().optional().describe('Asset ID'),
273
452
  system_id: z.string().uuid().optional().describe('System ID'),
274
453
  work_category_id: z.string().uuid().optional().describe('Work category ID'),
@@ -286,10 +465,21 @@ export function registerWriteTools(server, client) {
286
465
  title: z.string().min(1).max(500).optional().describe('Work request title'),
287
466
  description: z.string().optional().describe('Description'),
288
467
  priority: z.enum(['LOW', 'MEDIUM', 'HIGH', 'CRITICAL']).optional().describe('Priority level'),
289
- status: z.enum(['SUBMITTED', 'APPROVED', 'REJECTED', 'CONVERTED']).optional().describe('Status'),
468
+ status: z
469
+ .enum(['SUBMITTED', 'APPROVED', 'REJECTED', 'CONVERTED'])
470
+ .optional()
471
+ .describe('Status'),
290
472
  site_id: z.string().uuid().optional().describe('Site ID — resolve first via list_sites'),
291
- building_id: z.string().uuid().optional().describe('Building ID — resolve second via list_buildings filtered by site_id'),
292
- location_id: z.string().uuid().optional().describe('Location ID — resolve last via list_locations filtered by building_id'),
473
+ building_id: z
474
+ .string()
475
+ .uuid()
476
+ .optional()
477
+ .describe('Building ID — resolve second via list_buildings filtered by site_id'),
478
+ location_id: z
479
+ .string()
480
+ .uuid()
481
+ .optional()
482
+ .describe('Location ID — resolve last via list_locations filtered by building_id'),
293
483
  asset_id: z.string().uuid().optional().describe('Asset ID'),
294
484
  system_id: z.string().uuid().optional().describe('System ID'),
295
485
  work_category_id: z.string().uuid().optional().describe('Work category ID'),
@@ -324,8 +514,15 @@ export function registerWriteTools(server, client) {
324
514
  state: z.string().max(100).optional().describe('State/province'),
325
515
  country: z.string().max(100).optional().describe('Country'),
326
516
  status: z.string().max(50).optional().describe('Vendor status'),
327
- categories: z.array(z.string().max(100)).optional().describe('Vendor categories (e.g. ["HVAC", "Plumbing"])'),
328
- website: z.string().max(500).optional().describe('Website URL (protocol and www prefix are stripped automatically)'),
517
+ categories: z
518
+ .array(z.string().max(100))
519
+ .optional()
520
+ .describe('Vendor categories (e.g. ["HVAC", "Plumbing"])'),
521
+ website: z
522
+ .string()
523
+ .max(500)
524
+ .optional()
525
+ .describe('Website URL (protocol and www prefix are stripped automatically)'),
329
526
  description: z.string().optional().describe('Description'),
330
527
  }, async (params) => {
331
528
  try {
@@ -347,8 +544,15 @@ export function registerWriteTools(server, client) {
347
544
  state: z.string().max(100).optional().describe('State/province'),
348
545
  country: z.string().max(100).optional().describe('Country'),
349
546
  status: z.string().max(50).optional().describe('Vendor status'),
350
- categories: z.array(z.string().max(100)).optional().describe('Vendor categories (e.g. ["HVAC", "Plumbing"])'),
351
- website: z.string().max(500).optional().describe('Website URL (protocol and www prefix are stripped automatically)'),
547
+ categories: z
548
+ .array(z.string().max(100))
549
+ .optional()
550
+ .describe('Vendor categories (e.g. ["HVAC", "Plumbing"])'),
551
+ website: z
552
+ .string()
553
+ .max(500)
554
+ .optional()
555
+ .describe('Website URL (protocol and www prefix are stripped automatically)'),
352
556
  description: z.string().optional().describe('Description'),
353
557
  }, async ({ id, ...rest }) => {
354
558
  try {
@@ -440,8 +644,18 @@ export function registerWriteTools(server, client) {
440
644
  floors: z.number().int().optional().describe('Number of floors'),
441
645
  area_sqft: z.number().min(0).optional().describe('Area in square feet'),
442
646
  type: z.string().max(100).optional().describe('Building type label'),
443
- building_type_id: z.string().uuid().optional().describe('Building type ID (from building_types)'),
444
- year_built: z.number().int().min(1800).max(2100).optional().describe('Year the building was constructed'),
647
+ building_type_id: z
648
+ .string()
649
+ .uuid()
650
+ .optional()
651
+ .describe('Building type ID (from building_types)'),
652
+ year_built: z
653
+ .number()
654
+ .int()
655
+ .min(1800)
656
+ .max(2100)
657
+ .optional()
658
+ .describe('Year the building was constructed'),
445
659
  }, async (params) => {
446
660
  try {
447
661
  const result = await client.create('buildings', buildBody(params));
@@ -458,8 +672,18 @@ export function registerWriteTools(server, client) {
458
672
  floors: z.number().int().optional().describe('Number of floors'),
459
673
  area_sqft: z.number().min(0).optional().describe('Area in square feet'),
460
674
  type: z.string().max(100).optional().describe('Building type label'),
461
- building_type_id: z.string().uuid().optional().describe('Building type ID (from building_types)'),
462
- year_built: z.number().int().min(1800).max(2100).optional().describe('Year the building was constructed'),
675
+ building_type_id: z
676
+ .string()
677
+ .uuid()
678
+ .optional()
679
+ .describe('Building type ID (from building_types)'),
680
+ year_built: z
681
+ .number()
682
+ .int()
683
+ .min(1800)
684
+ .max(2100)
685
+ .optional()
686
+ .describe('Year the building was constructed'),
463
687
  }, async ({ id, ...rest }) => {
464
688
  try {
465
689
  const result = await client.update('buildings', id, buildBody(rest));
@@ -487,7 +711,11 @@ export function registerWriteTools(server, client) {
487
711
  floor: z.string().max(50).optional().describe('Floor identifier'),
488
712
  area: z.number().min(0).optional().describe('Area (sq ft or sq m)'),
489
713
  type: z.string().max(100).optional().describe('Location type label'),
490
- location_type_id: z.string().uuid().optional().describe('Location type ID (from location_types)'),
714
+ location_type_id: z
715
+ .string()
716
+ .uuid()
717
+ .optional()
718
+ .describe('Location type ID (from location_types)'),
491
719
  }, async (params) => {
492
720
  try {
493
721
  const result = await client.create('locations', buildBody(params));
@@ -504,7 +732,11 @@ export function registerWriteTools(server, client) {
504
732
  floor: z.string().max(50).optional().describe('Floor identifier'),
505
733
  area: z.number().min(0).optional().describe('Area (sq ft or sq m)'),
506
734
  type: z.string().max(100).optional().describe('Location type label'),
507
- location_type_id: z.string().uuid().optional().describe('Location type ID (from location_types)'),
735
+ location_type_id: z
736
+ .string()
737
+ .uuid()
738
+ .optional()
739
+ .describe('Location type ID (from location_types)'),
508
740
  }, async ({ id, ...rest }) => {
509
741
  try {
510
742
  const result = await client.update('locations', id, buildBody(rest));
@@ -529,14 +761,39 @@ export function registerWriteTools(server, client) {
529
761
  server.tool('create_pm_schedule', 'Create a new preventive maintenance schedule. Requires pm_schedules:write scope. IMPORTANT — Location hierarchy: always resolve top-down by calling list_sites first, then list_buildings filtered by site_id, then list_locations filtered by building_id. Provide all three IDs explicitly.', {
530
762
  title: z.string().min(1).max(500).describe('PM schedule title (required)'),
531
763
  description: z.string().optional().describe('Description'),
532
- frequency: z.enum(['DAILY', 'WEEKLY', 'MONTHLY', 'QUARTERLY', 'SEMI_ANNUAL', 'ANNUAL', 'FIVE_YEARLY', 'CUSTOM']).optional().describe('Frequency'),
533
- custom_interval_weeks: z.number().int().min(1).optional().describe('Custom interval in weeks (when frequency is CUSTOM)'),
764
+ frequency: z
765
+ .enum([
766
+ 'DAILY',
767
+ 'WEEKLY',
768
+ 'MONTHLY',
769
+ 'QUARTERLY',
770
+ 'SEMI_ANNUAL',
771
+ 'ANNUAL',
772
+ 'FIVE_YEARLY',
773
+ 'CUSTOM',
774
+ ])
775
+ .optional()
776
+ .describe('Frequency'),
777
+ custom_interval_weeks: z
778
+ .number()
779
+ .int()
780
+ .min(1)
781
+ .optional()
782
+ .describe('Custom interval in weeks (when frequency is CUSTOM)'),
534
783
  next_due: z.string().optional().describe('Next due date (ISO 8601)'),
535
784
  estimated_hours: z.number().min(0).optional().describe('Estimated hours'),
536
785
  estimated_cost: z.number().min(0).optional().describe('Estimated cost'),
537
786
  site_id: z.string().uuid().optional().describe('Site ID — resolve first via list_sites'),
538
- building_id: z.string().uuid().optional().describe('Building ID — resolve second via list_buildings filtered by site_id'),
539
- location_id: z.string().uuid().optional().describe('Location ID — resolve last via list_locations filtered by building_id'),
787
+ building_id: z
788
+ .string()
789
+ .uuid()
790
+ .optional()
791
+ .describe('Building ID — resolve second via list_buildings filtered by site_id'),
792
+ location_id: z
793
+ .string()
794
+ .uuid()
795
+ .optional()
796
+ .describe('Location ID — resolve last via list_locations filtered by building_id'),
540
797
  asset_id: z.string().uuid().optional().describe('Asset ID'),
541
798
  work_category: z.string().max(200).optional().describe('Work category label'),
542
799
  schedule_type: z.string().max(100).optional().describe('Schedule type'),
@@ -546,18 +803,28 @@ export function registerWriteTools(server, client) {
546
803
  status: z.enum(['active', 'inactive']).optional().describe('Schedule status'),
547
804
  auto_generate_wo: z.boolean().optional().describe('Auto-generate work orders'),
548
805
  floating: z.boolean().optional().describe('Floating schedule (due date based on completion)'),
549
- meter_based: z.boolean().optional().describe('Whether this PM triggers at meter intervals (e.g. every 5000 km)'),
550
- meter_interval: z.number().min(0).optional().describe('Meter interval — trigger every N units'),
806
+ meter_based: z
807
+ .boolean()
808
+ .optional()
809
+ .describe('Whether this PM triggers at meter intervals (e.g. every 5000 km)'),
810
+ meter_interval: z
811
+ .number()
812
+ .min(0)
813
+ .optional()
814
+ .describe('Meter interval — trigger every N units'),
551
815
  meter_unit: z.string().max(50).optional().describe('Meter unit (km, miles, hours, cycles)'),
552
816
  start_date: z.string().optional().describe('Start date (ISO 8601)'),
553
817
  asset_ids: z.array(z.string().uuid()).optional().describe('Array of asset IDs'),
554
818
  system_ids: z.array(z.string().uuid()).optional().describe('Array of system IDs'),
555
819
  location_ids: z.array(z.string().uuid()).optional().describe('Array of location IDs'),
556
- tasks: z.array(z.object({
820
+ tasks: z
821
+ .array(z.object({
557
822
  id: z.string().describe('Unique task ID (use a random string)'),
558
823
  description: z.string().describe('Task description'),
559
824
  completed: z.boolean().describe('Whether the task is completed'),
560
- })).optional().describe('Checklist of tasks for this PM schedule'),
825
+ }))
826
+ .optional()
827
+ .describe('Checklist of tasks for this PM schedule'),
561
828
  }, async (params) => {
562
829
  try {
563
830
  const result = await client.create('pm-schedules', buildBody(params));
@@ -571,14 +838,39 @@ export function registerWriteTools(server, client) {
571
838
  id: z.string().uuid().describe('PM schedule ID'),
572
839
  title: z.string().min(1).max(500).optional().describe('PM schedule title'),
573
840
  description: z.string().optional().describe('Description'),
574
- frequency: z.enum(['DAILY', 'WEEKLY', 'MONTHLY', 'QUARTERLY', 'SEMI_ANNUAL', 'ANNUAL', 'FIVE_YEARLY', 'CUSTOM']).optional().describe('Frequency'),
575
- custom_interval_weeks: z.number().int().min(1).optional().describe('Custom interval in weeks (when frequency is CUSTOM)'),
841
+ frequency: z
842
+ .enum([
843
+ 'DAILY',
844
+ 'WEEKLY',
845
+ 'MONTHLY',
846
+ 'QUARTERLY',
847
+ 'SEMI_ANNUAL',
848
+ 'ANNUAL',
849
+ 'FIVE_YEARLY',
850
+ 'CUSTOM',
851
+ ])
852
+ .optional()
853
+ .describe('Frequency'),
854
+ custom_interval_weeks: z
855
+ .number()
856
+ .int()
857
+ .min(1)
858
+ .optional()
859
+ .describe('Custom interval in weeks (when frequency is CUSTOM)'),
576
860
  next_due: z.string().optional().describe('Next due date (ISO 8601)'),
577
861
  estimated_hours: z.number().min(0).optional().describe('Estimated hours'),
578
862
  estimated_cost: z.number().min(0).optional().describe('Estimated cost'),
579
863
  site_id: z.string().uuid().optional().describe('Site ID — resolve first via list_sites'),
580
- building_id: z.string().uuid().optional().describe('Building ID — resolve second via list_buildings filtered by site_id'),
581
- location_id: z.string().uuid().optional().describe('Location ID — resolve last via list_locations filtered by building_id'),
864
+ building_id: z
865
+ .string()
866
+ .uuid()
867
+ .optional()
868
+ .describe('Building ID — resolve second via list_buildings filtered by site_id'),
869
+ location_id: z
870
+ .string()
871
+ .uuid()
872
+ .optional()
873
+ .describe('Location ID — resolve last via list_locations filtered by building_id'),
582
874
  asset_id: z.string().uuid().optional().describe('Asset ID'),
583
875
  work_category: z.string().max(200).optional().describe('Work category label'),
584
876
  schedule_type: z.string().max(100).optional().describe('Schedule type'),
@@ -589,17 +881,24 @@ export function registerWriteTools(server, client) {
589
881
  auto_generate_wo: z.boolean().optional().describe('Auto-generate work orders'),
590
882
  floating: z.boolean().optional().describe('Floating schedule (due date based on completion)'),
591
883
  meter_based: z.boolean().optional().describe('Whether this PM triggers at meter intervals'),
592
- meter_interval: z.number().min(0).optional().describe('Meter interval — trigger every N units'),
884
+ meter_interval: z
885
+ .number()
886
+ .min(0)
887
+ .optional()
888
+ .describe('Meter interval — trigger every N units'),
593
889
  meter_unit: z.string().max(50).optional().describe('Meter unit (km, miles, hours, cycles)'),
594
890
  start_date: z.string().optional().describe('Start date (ISO 8601)'),
595
891
  asset_ids: z.array(z.string().uuid()).optional().describe('Array of asset IDs'),
596
892
  system_ids: z.array(z.string().uuid()).optional().describe('Array of system IDs'),
597
893
  location_ids: z.array(z.string().uuid()).optional().describe('Array of location IDs'),
598
- tasks: z.array(z.object({
894
+ tasks: z
895
+ .array(z.object({
599
896
  id: z.string().describe('Unique task ID'),
600
897
  description: z.string().describe('Task description'),
601
898
  completed: z.boolean().describe('Whether the task is completed'),
602
- })).optional().describe('Checklist of tasks for this PM schedule'),
899
+ }))
900
+ .optional()
901
+ .describe('Checklist of tasks for this PM schedule'),
603
902
  }, async ({ id, ...rest }) => {
604
903
  try {
605
904
  const result = await client.update('pm-schedules', id, buildBody(rest));
@@ -624,27 +923,66 @@ export function registerWriteTools(server, client) {
624
923
  server.tool('create_pm_template', 'Create a new PM template. Templates are reusable PM definitions (not linked to a site/asset) that can seed new PM schedules. Requires pm_templates:write scope.', {
625
924
  title: z.string().min(1).max(500).describe('PM template title (required, unique per tenant)'),
626
925
  description: z.string().optional().describe('Description'),
627
- frequency: z.enum(['DAILY', 'WEEKLY', 'MONTHLY', 'QUARTERLY', 'SEMI_ANNUAL', 'ANNUAL', 'FIVE_YEARLY', 'CUSTOM']).optional().describe('Suggested maintenance frequency'),
628
- custom_interval_weeks: z.number().int().min(1).optional().describe('Custom interval in weeks (when frequency is CUSTOM)'),
926
+ frequency: z
927
+ .enum([
928
+ 'DAILY',
929
+ 'WEEKLY',
930
+ 'MONTHLY',
931
+ 'QUARTERLY',
932
+ 'SEMI_ANNUAL',
933
+ 'ANNUAL',
934
+ 'FIVE_YEARLY',
935
+ 'CUSTOM',
936
+ ])
937
+ .optional()
938
+ .describe('Suggested maintenance frequency'),
939
+ custom_interval_weeks: z
940
+ .number()
941
+ .int()
942
+ .min(1)
943
+ .optional()
944
+ .describe('Custom interval in weeks (when frequency is CUSTOM)'),
629
945
  work_category: z.string().max(200).optional().describe('Work category label (free text)'),
630
- work_category_id: z.string().uuid().optional().describe('Work category ID — resolve via list_work_categories'),
946
+ work_category_id: z
947
+ .string()
948
+ .uuid()
949
+ .optional()
950
+ .describe('Work category ID — resolve via list_work_categories'),
631
951
  estimated_hours: z.number().min(0).optional().describe('Estimated hours'),
632
952
  estimated_cost: z.number().min(0).optional().describe('Estimated cost'),
633
953
  safety_requirements: z.string().optional().describe('Safety requirements'),
634
- tasks: z.array(z.object({
954
+ tasks: z
955
+ .array(z.object({
635
956
  id: z.string().optional().describe('Unique task ID (auto-generated if omitted)'),
636
957
  description: z.string().describe('Task description'),
637
- completed: z.boolean().optional().describe('Whether the task is completed (defaults to false)'),
638
- })).optional().describe('Checklist of tasks baked into this template'),
639
- resources: z.array(z.object({
958
+ completed: z
959
+ .boolean()
960
+ .optional()
961
+ .describe('Whether the task is completed (defaults to false)'),
962
+ }))
963
+ .optional()
964
+ .describe('Checklist of tasks baked into this template'),
965
+ resources: z
966
+ .array(z.object({
640
967
  name: z.string().optional().describe('Resource name'),
641
- type: z.enum(PM_RESOURCE_TYPES).optional().describe('Resource type (TOOL, PART, MATERIAL, or EQUIPMENT)'),
968
+ type: z
969
+ .enum(PM_RESOURCE_TYPES)
970
+ .optional()
971
+ .describe('Resource type (TOOL, PART, MATERIAL, or EQUIPMENT)'),
642
972
  quantity: z.number().optional().describe('Quantity required'),
643
973
  cost: z.number().optional().describe('Unit cost'),
644
- })).optional().describe('Resource references (parts, tools, materials, equipment)'),
974
+ }))
975
+ .optional()
976
+ .describe('Resource references (parts, tools, materials, equipment)'),
645
977
  documents: z.array(z.record(z.unknown())).optional().describe('Document references'),
646
- asset_ids: z.array(z.string().uuid()).optional().describe('Default asset IDs to seed on derived schedules'),
647
- location_ids: z.array(z.string().uuid()).optional().describe('Default location IDs to seed on derived schedules'),
978
+ asset_ids: z
979
+ .array(z.string().uuid())
980
+ .optional()
981
+ .describe('Default asset IDs to seed on derived schedules'),
982
+ location_ids: z
983
+ .array(z.string().uuid())
984
+ .optional()
985
+ .describe('Default location IDs to seed on derived schedules'),
648
986
  }, async (params) => {
649
987
  try {
650
988
  const result = await client.create('pm-templates', buildBody(normalizePmTemplateBody(params)));
@@ -658,24 +996,53 @@ export function registerWriteTools(server, client) {
658
996
  id: z.string().uuid().describe('PM template ID'),
659
997
  title: z.string().min(1).max(500).optional().describe('PM template title'),
660
998
  description: z.string().optional().describe('Description'),
661
- frequency: z.enum(['DAILY', 'WEEKLY', 'MONTHLY', 'QUARTERLY', 'SEMI_ANNUAL', 'ANNUAL', 'FIVE_YEARLY', 'CUSTOM']).optional().describe('Suggested maintenance frequency'),
662
- custom_interval_weeks: z.number().int().min(1).optional().describe('Custom interval in weeks (when frequency is CUSTOM)'),
999
+ frequency: z
1000
+ .enum([
1001
+ 'DAILY',
1002
+ 'WEEKLY',
1003
+ 'MONTHLY',
1004
+ 'QUARTERLY',
1005
+ 'SEMI_ANNUAL',
1006
+ 'ANNUAL',
1007
+ 'FIVE_YEARLY',
1008
+ 'CUSTOM',
1009
+ ])
1010
+ .optional()
1011
+ .describe('Suggested maintenance frequency'),
1012
+ custom_interval_weeks: z
1013
+ .number()
1014
+ .int()
1015
+ .min(1)
1016
+ .optional()
1017
+ .describe('Custom interval in weeks (when frequency is CUSTOM)'),
663
1018
  work_category: z.string().max(200).optional().describe('Work category label (free text)'),
664
1019
  work_category_id: z.string().uuid().optional().describe('Work category ID'),
665
1020
  estimated_hours: z.number().min(0).optional().describe('Estimated hours'),
666
1021
  estimated_cost: z.number().min(0).optional().describe('Estimated cost'),
667
1022
  safety_requirements: z.string().optional().describe('Safety requirements'),
668
- tasks: z.array(z.object({
1023
+ tasks: z
1024
+ .array(z.object({
669
1025
  id: z.string().optional().describe('Unique task ID (auto-generated if omitted)'),
670
1026
  description: z.string().describe('Task description'),
671
- completed: z.boolean().optional().describe('Whether the task is completed (defaults to false)'),
672
- })).optional().describe('Checklist of tasks baked into this template'),
673
- resources: z.array(z.object({
1027
+ completed: z
1028
+ .boolean()
1029
+ .optional()
1030
+ .describe('Whether the task is completed (defaults to false)'),
1031
+ }))
1032
+ .optional()
1033
+ .describe('Checklist of tasks baked into this template'),
1034
+ resources: z
1035
+ .array(z.object({
674
1036
  name: z.string().optional().describe('Resource name'),
675
- type: z.enum(PM_RESOURCE_TYPES).optional().describe('Resource type (TOOL, PART, MATERIAL, or EQUIPMENT)'),
1037
+ type: z
1038
+ .enum(PM_RESOURCE_TYPES)
1039
+ .optional()
1040
+ .describe('Resource type (TOOL, PART, MATERIAL, or EQUIPMENT)'),
676
1041
  quantity: z.number().optional().describe('Quantity required'),
677
1042
  cost: z.number().optional().describe('Unit cost'),
678
- })).optional().describe('Resource references (parts, tools, materials, equipment)'),
1043
+ }))
1044
+ .optional()
1045
+ .describe('Resource references (parts, tools, materials, equipment)'),
679
1046
  documents: z.array(z.record(z.unknown())).optional().describe('Document references'),
680
1047
  asset_ids: z.array(z.string().uuid()).optional().describe('Default asset IDs'),
681
1048
  location_ids: z.array(z.string().uuid()).optional().describe('Default location IDs'),
@@ -705,16 +1072,42 @@ export function registerWriteTools(server, client) {
705
1072
  status: z.string().max(100).describe('Project status (required)'),
706
1073
  start_date: z.string().describe('Start date (ISO 8601, required)'),
707
1074
  project_code: z.string().max(100).optional().describe('Project code'),
708
- project_type: z.enum(['capital', 'maintenance', 'repair', 'upgrade', 'new_construction', 'renovation', 'deferred_maintenance', 'other']).optional().describe('Project type'),
1075
+ project_type: z
1076
+ .enum([
1077
+ 'capital',
1078
+ 'maintenance',
1079
+ 'repair',
1080
+ 'upgrade',
1081
+ 'new_construction',
1082
+ 'renovation',
1083
+ 'deferred_maintenance',
1084
+ 'other',
1085
+ ])
1086
+ .optional()
1087
+ .describe('Project type'),
709
1088
  current_phase: z.string().max(100).optional().describe('Current phase'),
710
1089
  description: z.string().optional().describe('Description'),
711
1090
  end_date: z.string().optional().describe('End date (ISO 8601)'),
712
1091
  budget: z.number().min(0).optional().describe('Total budget'),
713
1092
  project_manager: z.string().max(200).optional().describe('Project manager name'),
714
- progress_percentage: z.number().min(0).max(100).optional().describe('Progress percentage (0-100)'),
715
- health_status: z.enum(['on_track', 'at_risk', 'delayed', 'critical']).optional().describe('Health status'),
716
- budget_status: z.enum(['off_track', 'on_track', 'not_set', 'monitor']).optional().describe('Budget status'),
717
- progress_status: z.enum(['off_track', 'on_track', 'monitor']).optional().describe('Progress status'),
1093
+ progress_percentage: z
1094
+ .number()
1095
+ .min(0)
1096
+ .max(100)
1097
+ .optional()
1098
+ .describe('Progress percentage (0-100)'),
1099
+ health_status: z
1100
+ .enum(['on_track', 'at_risk', 'delayed', 'critical'])
1101
+ .optional()
1102
+ .describe('Health status'),
1103
+ budget_status: z
1104
+ .enum(['off_track', 'on_track', 'not_set', 'monitor'])
1105
+ .optional()
1106
+ .describe('Budget status'),
1107
+ progress_status: z
1108
+ .enum(['off_track', 'on_track', 'monitor'])
1109
+ .optional()
1110
+ .describe('Progress status'),
718
1111
  image_url: z.string().max(2000).optional().describe('Image URL'),
719
1112
  }, async (params) => {
720
1113
  try {
@@ -731,16 +1124,42 @@ export function registerWriteTools(server, client) {
731
1124
  status: z.string().max(100).optional().describe('Project status'),
732
1125
  start_date: z.string().optional().describe('Start date (ISO 8601)'),
733
1126
  project_code: z.string().max(100).optional().describe('Project code'),
734
- project_type: z.enum(['capital', 'maintenance', 'repair', 'upgrade', 'new_construction', 'renovation', 'deferred_maintenance', 'other']).optional().describe('Project type'),
1127
+ project_type: z
1128
+ .enum([
1129
+ 'capital',
1130
+ 'maintenance',
1131
+ 'repair',
1132
+ 'upgrade',
1133
+ 'new_construction',
1134
+ 'renovation',
1135
+ 'deferred_maintenance',
1136
+ 'other',
1137
+ ])
1138
+ .optional()
1139
+ .describe('Project type'),
735
1140
  current_phase: z.string().max(100).optional().describe('Current phase'),
736
1141
  description: z.string().optional().describe('Description'),
737
1142
  end_date: z.string().optional().describe('End date (ISO 8601)'),
738
1143
  budget: z.number().min(0).optional().describe('Total budget'),
739
1144
  project_manager: z.string().max(200).optional().describe('Project manager name'),
740
- progress_percentage: z.number().min(0).max(100).optional().describe('Progress percentage (0-100)'),
741
- health_status: z.enum(['on_track', 'at_risk', 'delayed', 'critical']).optional().describe('Health status'),
742
- budget_status: z.enum(['off_track', 'on_track', 'not_set', 'monitor']).optional().describe('Budget status'),
743
- progress_status: z.enum(['off_track', 'on_track', 'monitor']).optional().describe('Progress status'),
1145
+ progress_percentage: z
1146
+ .number()
1147
+ .min(0)
1148
+ .max(100)
1149
+ .optional()
1150
+ .describe('Progress percentage (0-100)'),
1151
+ health_status: z
1152
+ .enum(['on_track', 'at_risk', 'delayed', 'critical'])
1153
+ .optional()
1154
+ .describe('Health status'),
1155
+ budget_status: z
1156
+ .enum(['off_track', 'on_track', 'not_set', 'monitor'])
1157
+ .optional()
1158
+ .describe('Budget status'),
1159
+ progress_status: z
1160
+ .enum(['off_track', 'on_track', 'monitor'])
1161
+ .optional()
1162
+ .describe('Progress status'),
744
1163
  image_url: z.string().max(2000).optional().describe('Image URL'),
745
1164
  }, async ({ id, ...rest }) => {
746
1165
  try {
@@ -824,7 +1243,10 @@ export function registerWriteTools(server, client) {
824
1243
  tax_amount: z.number().min(0).optional().describe('Tax amount'),
825
1244
  due_date: z.string().optional().describe('Due date (ISO 8601)'),
826
1245
  paid_date: z.string().optional().describe('Paid date (ISO 8601)'),
827
- status: z.enum(['pending', 'approved', 'paid', 'voided']).optional().describe('Invoice status'),
1246
+ status: z
1247
+ .enum(['pending', 'approved', 'paid', 'voided'])
1248
+ .optional()
1249
+ .describe('Invoice status'),
828
1250
  notes: z.string().optional().describe('Notes'),
829
1251
  project_id: z.string().uuid().optional().describe('Project ID'),
830
1252
  work_order_id: z.string().uuid().optional().describe('Work order ID'),
@@ -849,7 +1271,10 @@ export function registerWriteTools(server, client) {
849
1271
  tax_amount: z.number().min(0).optional().describe('Tax amount'),
850
1272
  due_date: z.string().optional().describe('Due date (ISO 8601)'),
851
1273
  paid_date: z.string().optional().describe('Paid date (ISO 8601)'),
852
- status: z.enum(['pending', 'approved', 'paid', 'voided']).optional().describe('Invoice status'),
1274
+ status: z
1275
+ .enum(['pending', 'approved', 'paid', 'voided'])
1276
+ .optional()
1277
+ .describe('Invoice status'),
853
1278
  notes: z.string().optional().describe('Notes'),
854
1279
  project_id: z.string().uuid().optional().describe('Project ID'),
855
1280
  work_order_id: z.string().uuid().optional().describe('Work order ID'),
@@ -881,7 +1306,10 @@ export function registerWriteTools(server, client) {
881
1306
  po_number: z.string().min(1).max(200).describe('PO number (required)'),
882
1307
  amount: z.number().min(0).describe('PO amount (required)'),
883
1308
  description: z.string().optional().describe('Description'),
884
- status: z.enum(['draft', 'issued', 'partially_received', 'received', 'closed', 'cancelled']).optional().describe('PO status'),
1309
+ status: z
1310
+ .enum(['draft', 'issued', 'partially_received', 'received', 'closed', 'cancelled'])
1311
+ .optional()
1312
+ .describe('PO status'),
885
1313
  issued_date: z.string().optional().describe('Issued date (ISO 8601)'),
886
1314
  expected_date: z.string().optional().describe('Expected delivery date (ISO 8601)'),
887
1315
  notes: z.string().optional().describe('Notes'),
@@ -903,7 +1331,10 @@ export function registerWriteTools(server, client) {
903
1331
  po_number: z.string().min(1).max(200).optional().describe('PO number'),
904
1332
  amount: z.number().min(0).optional().describe('PO amount'),
905
1333
  description: z.string().optional().describe('Description'),
906
- status: z.enum(['draft', 'issued', 'partially_received', 'received', 'closed', 'cancelled']).optional().describe('PO status'),
1334
+ status: z
1335
+ .enum(['draft', 'issued', 'partially_received', 'received', 'closed', 'cancelled'])
1336
+ .optional()
1337
+ .describe('PO status'),
907
1338
  issued_date: z.string().optional().describe('Issued date (ISO 8601)'),
908
1339
  expected_date: z.string().optional().describe('Expected delivery date (ISO 8601)'),
909
1340
  notes: z.string().optional().describe('Notes'),
@@ -1037,7 +1468,10 @@ export function registerWriteTools(server, client) {
1037
1468
  server.tool('create_project_document_folder_template', 'Create a project document folder template. Requires project_document_folder_templates:write scope.', {
1038
1469
  name: z.string().min(1).max(500).describe('Template name (required)'),
1039
1470
  description: z.string().max(2000).optional().describe('Template description'),
1040
- structure: z.array(z.record(z.string(), z.unknown())).optional().describe('Folder hierarchy as JSON array'),
1471
+ structure: z
1472
+ .array(z.record(z.string(), z.unknown()))
1473
+ .optional()
1474
+ .describe('Folder hierarchy as JSON array'),
1041
1475
  is_default: z.boolean().optional().describe('Whether this is the default template'),
1042
1476
  }, async (params) => {
1043
1477
  try {
@@ -1052,7 +1486,10 @@ export function registerWriteTools(server, client) {
1052
1486
  id: z.string().uuid().describe('Template ID'),
1053
1487
  name: z.string().min(1).max(500).optional().describe('Template name'),
1054
1488
  description: z.string().max(2000).optional().describe('Template description'),
1055
- structure: z.array(z.record(z.string(), z.unknown())).optional().describe('Folder hierarchy as JSON array'),
1489
+ structure: z
1490
+ .array(z.record(z.string(), z.unknown()))
1491
+ .optional()
1492
+ .describe('Folder hierarchy as JSON array'),
1056
1493
  is_default: z.boolean().optional().describe('Whether this is the default template'),
1057
1494
  }, async ({ id, ...rest }) => {
1058
1495
  try {
@@ -1125,7 +1562,6 @@ export function registerWriteTools(server, client) {
1125
1562
  server.tool('create_asset_comment', 'Create a new comment on an asset. Requires asset_comments:write scope.', {
1126
1563
  asset_id: z.string().uuid().describe('Asset ID (required)'),
1127
1564
  comment: z.string().min(1).describe('Comment text (required)'),
1128
- user_id: z.string().max(200).optional().describe('User ID of commenter'),
1129
1565
  }, async (params) => {
1130
1566
  try {
1131
1567
  const result = await client.create('asset-comments', buildBody(params));
@@ -1138,7 +1574,6 @@ export function registerWriteTools(server, client) {
1138
1574
  server.tool('update_asset_comment', 'Update an existing asset comment by ID. Requires asset_comments:write scope.', {
1139
1575
  id: z.string().uuid().describe('Asset comment ID'),
1140
1576
  comment: z.string().min(1).optional().describe('Comment text'),
1141
- user_id: z.string().max(200).optional().describe('User ID of commenter'),
1142
1577
  }, async ({ id, ...rest }) => {
1143
1578
  try {
1144
1579
  const result = await client.update('asset-comments', id, buildBody(rest));
@@ -1163,7 +1598,6 @@ export function registerWriteTools(server, client) {
1163
1598
  server.tool('create_work_order_comment', 'Create a new comment on a work order. Requires work_order_comments:write scope.', {
1164
1599
  work_order_id: z.string().uuid().describe('Work order ID (required)'),
1165
1600
  comment: z.string().min(1).describe('Comment text (required)'),
1166
- user_id: z.string().max(200).optional().describe('User ID of commenter'),
1167
1601
  }, async (params) => {
1168
1602
  try {
1169
1603
  const result = await client.create('work-order-comments', buildBody(params));
@@ -1176,7 +1610,6 @@ export function registerWriteTools(server, client) {
1176
1610
  server.tool('update_work_order_comment', 'Update an existing work order comment by ID. Requires work_order_comments:write scope.', {
1177
1611
  id: z.string().uuid().describe('Work order comment ID'),
1178
1612
  comment: z.string().min(1).optional().describe('Comment text'),
1179
- user_id: z.string().max(200).optional().describe('User ID of commenter'),
1180
1613
  }, async ({ id, ...rest }) => {
1181
1614
  try {
1182
1615
  const result = await client.update('work-order-comments', id, buildBody(rest));
@@ -1202,7 +1635,6 @@ export function registerWriteTools(server, client) {
1202
1635
  project_id: z.string().uuid().describe('Project ID (required)'),
1203
1636
  content: z.string().min(1).describe('Comment content (required)'),
1204
1637
  parent_id: z.string().uuid().optional().describe('Parent comment ID (for threading)'),
1205
- user_id: z.string().max(200).optional().describe('User ID of commenter'),
1206
1638
  }, async (params) => {
1207
1639
  try {
1208
1640
  const result = await client.create('project-comments', buildBody(params));
@@ -1216,7 +1648,6 @@ export function registerWriteTools(server, client) {
1216
1648
  id: z.string().uuid().describe('Project comment ID'),
1217
1649
  content: z.string().min(1).optional().describe('Comment content'),
1218
1650
  parent_id: z.string().uuid().optional().describe('Parent comment ID'),
1219
- user_id: z.string().max(200).optional().describe('User ID of commenter'),
1220
1651
  }, async ({ id, ...rest }) => {
1221
1652
  try {
1222
1653
  const result = await client.update('project-comments', id, buildBody(rest));
@@ -1238,8 +1669,10 @@ export function registerWriteTools(server, client) {
1238
1669
  // ============================================================
1239
1670
  // 18. Asset Costs (scope: asset_costs)
1240
1671
  // ============================================================
1241
- server.tool('create_asset_cost', 'Create a new asset cost entry. Requires asset_costs:write scope.', {
1242
- category: z.enum(['Repair', 'PM', 'Operation', 'Replacement', 'Decommission', 'Other']).describe('Cost category (required)'),
1672
+ server.tool('create_asset_cost', 'Create a new asset cost entry — the record type shown on the AssetLab "Expenses" page. Requires asset_costs:write scope.', {
1673
+ category: z
1674
+ .enum(['Repair', 'PM', 'Operation', 'Replacement', 'Decommission', 'Other'])
1675
+ .describe('Cost category (required)'),
1243
1676
  amount: z.number().min(0).describe('Cost amount (required)'),
1244
1677
  cost_date: z.string().describe('Cost date (ISO 8601, required)'),
1245
1678
  asset_id: z.string().uuid().optional().describe('Asset ID'),
@@ -1247,6 +1680,8 @@ export function registerWriteTools(server, client) {
1247
1680
  building_id: z.string().uuid().optional().describe('Building ID'),
1248
1681
  work_order_id: z.string().uuid().optional().describe('Work order ID'),
1249
1682
  description: z.string().optional().describe('Description'),
1683
+ invoice_number: z.string().max(200).optional().describe('Invoice number (free text)'),
1684
+ po_number: z.string().max(200).optional().describe('Purchase order number (free text)'),
1250
1685
  }, async (params) => {
1251
1686
  try {
1252
1687
  const result = await client.create('asset-costs', buildBody(params));
@@ -1256,9 +1691,12 @@ export function registerWriteTools(server, client) {
1256
1691
  return formatError(err);
1257
1692
  }
1258
1693
  });
1259
- server.tool('update_asset_cost', 'Update an existing asset cost entry by ID. Requires asset_costs:write scope.', {
1694
+ server.tool('update_asset_cost', 'Update an existing asset cost entry by ID — the record type shown on the AssetLab "Expenses" page. Requires asset_costs:write scope.', {
1260
1695
  id: z.string().uuid().describe('Asset cost ID'),
1261
- category: z.enum(['Repair', 'PM', 'Operation', 'Replacement', 'Decommission', 'Other']).optional().describe('Cost category'),
1696
+ category: z
1697
+ .enum(['Repair', 'PM', 'Operation', 'Replacement', 'Decommission', 'Other'])
1698
+ .optional()
1699
+ .describe('Cost category'),
1262
1700
  amount: z.number().min(0).optional().describe('Cost amount'),
1263
1701
  cost_date: z.string().optional().describe('Cost date (ISO 8601)'),
1264
1702
  asset_id: z.string().uuid().optional().describe('Asset ID'),
@@ -1266,6 +1704,8 @@ export function registerWriteTools(server, client) {
1266
1704
  building_id: z.string().uuid().optional().describe('Building ID'),
1267
1705
  work_order_id: z.string().uuid().optional().describe('Work order ID'),
1268
1706
  description: z.string().optional().describe('Description'),
1707
+ invoice_number: z.string().max(200).optional().describe('Invoice number (free text)'),
1708
+ po_number: z.string().max(200).optional().describe('Purchase order number (free text)'),
1269
1709
  }, async ({ id, ...rest }) => {
1270
1710
  try {
1271
1711
  const result = await client.update('asset-costs', id, buildBody(rest));
@@ -1289,10 +1729,18 @@ export function registerWriteTools(server, client) {
1289
1729
  // ============================================================
1290
1730
  server.tool('create_asset_replacement_plan', 'Create a new asset replacement plan. Requires asset_replacement_plans:write scope.', {
1291
1731
  asset_id: z.string().uuid().describe('Asset ID (required)'),
1292
- planned_replacement_year: z.number().int().min(2000).max(2100).describe('Planned replacement year (required)'),
1732
+ planned_replacement_year: z
1733
+ .number()
1734
+ .int()
1735
+ .min(2000)
1736
+ .max(2100)
1737
+ .describe('Planned replacement year (required)'),
1293
1738
  estimated_cost: z.number().min(0).optional().describe('Estimated replacement cost'),
1294
1739
  priority: z.enum(['CRITICAL', 'HIGH', 'MEDIUM', 'LOW']).optional().describe('Priority'),
1295
- status: z.enum(['PLANNED', 'BUDGETED', 'APPROVED', 'COMPLETED', 'CANCELLED']).optional().describe('Status'),
1740
+ status: z
1741
+ .enum(['PLANNED', 'BUDGETED', 'APPROVED', 'COMPLETED', 'CANCELLED'])
1742
+ .optional()
1743
+ .describe('Status'),
1296
1744
  notes: z.string().optional().describe('Notes'),
1297
1745
  funding_source: z.string().max(200).optional().describe('Funding source'),
1298
1746
  }, async (params) => {
@@ -1307,10 +1755,19 @@ export function registerWriteTools(server, client) {
1307
1755
  server.tool('update_asset_replacement_plan', 'Update an existing asset replacement plan by ID. Requires asset_replacement_plans:write scope.', {
1308
1756
  id: z.string().uuid().describe('Asset replacement plan ID'),
1309
1757
  asset_id: z.string().uuid().optional().describe('Asset ID'),
1310
- planned_replacement_year: z.number().int().min(2000).max(2100).optional().describe('Planned replacement year'),
1758
+ planned_replacement_year: z
1759
+ .number()
1760
+ .int()
1761
+ .min(2000)
1762
+ .max(2100)
1763
+ .optional()
1764
+ .describe('Planned replacement year'),
1311
1765
  estimated_cost: z.number().min(0).optional().describe('Estimated replacement cost'),
1312
1766
  priority: z.enum(['CRITICAL', 'HIGH', 'MEDIUM', 'LOW']).optional().describe('Priority'),
1313
- status: z.enum(['PLANNED', 'BUDGETED', 'APPROVED', 'COMPLETED', 'CANCELLED']).optional().describe('Status'),
1767
+ status: z
1768
+ .enum(['PLANNED', 'BUDGETED', 'APPROVED', 'COMPLETED', 'CANCELLED'])
1769
+ .optional()
1770
+ .describe('Status'),
1314
1771
  notes: z.string().optional().describe('Notes'),
1315
1772
  funding_source: z.string().max(200).optional().describe('Funding source'),
1316
1773
  }, async ({ id, ...rest }) => {
@@ -1339,7 +1796,10 @@ export function registerWriteTools(server, client) {
1339
1796
  title: z.string().min(1).max(500).describe('Task title (required)'),
1340
1797
  description: z.string().optional().describe('Description'),
1341
1798
  phase_id: z.string().uuid().optional().describe('Phase ID'),
1342
- status: z.enum(['todo', 'in_progress', 'completed', 'blocked', 'cancelled']).optional().describe('Task status'),
1799
+ status: z
1800
+ .enum(['todo', 'in_progress', 'completed', 'blocked', 'cancelled'])
1801
+ .optional()
1802
+ .describe('Task status'),
1343
1803
  priority: z.enum(['low', 'medium', 'high', 'critical']).optional().describe('Priority'),
1344
1804
  assigned_to: z.string().max(200).optional().describe('Assigned user'),
1345
1805
  start_date: z.string().optional().describe('Start date (ISO 8601)'),
@@ -1361,7 +1821,10 @@ export function registerWriteTools(server, client) {
1361
1821
  title: z.string().min(1).max(500).optional().describe('Task title'),
1362
1822
  description: z.string().optional().describe('Description'),
1363
1823
  phase_id: z.string().uuid().optional().describe('Phase ID'),
1364
- status: z.enum(['todo', 'in_progress', 'completed', 'blocked', 'cancelled']).optional().describe('Task status'),
1824
+ status: z
1825
+ .enum(['todo', 'in_progress', 'completed', 'blocked', 'cancelled'])
1826
+ .optional()
1827
+ .describe('Task status'),
1365
1828
  priority: z.enum(['low', 'medium', 'high', 'critical']).optional().describe('Priority'),
1366
1829
  assigned_to: z.string().max(200).optional().describe('Assigned user'),
1367
1830
  start_date: z.string().optional().describe('Start date (ISO 8601)'),
@@ -1394,7 +1857,10 @@ export function registerWriteTools(server, client) {
1394
1857
  name: z.string().min(1).max(500).describe('Milestone name (required)'),
1395
1858
  due_date: z.string().describe('Due date (ISO 8601, required)'),
1396
1859
  description: z.string().optional().describe('Description'),
1397
- status: z.enum(['pending', 'completed', 'missed', 'at_risk']).optional().describe('Milestone status'),
1860
+ status: z
1861
+ .enum(['pending', 'completed', 'missed', 'at_risk'])
1862
+ .optional()
1863
+ .describe('Milestone status'),
1398
1864
  completed_date: z.string().optional().describe('Completed date (ISO 8601)'),
1399
1865
  }, async (params) => {
1400
1866
  try {
@@ -1411,7 +1877,10 @@ export function registerWriteTools(server, client) {
1411
1877
  name: z.string().min(1).max(500).optional().describe('Milestone name'),
1412
1878
  due_date: z.string().optional().describe('Due date (ISO 8601)'),
1413
1879
  description: z.string().optional().describe('Description'),
1414
- status: z.enum(['pending', 'completed', 'missed', 'at_risk']).optional().describe('Milestone status'),
1880
+ status: z
1881
+ .enum(['pending', 'completed', 'missed', 'at_risk'])
1882
+ .optional()
1883
+ .describe('Milestone status'),
1415
1884
  completed_date: z.string().optional().describe('Completed date (ISO 8601)'),
1416
1885
  }, async ({ id, ...rest }) => {
1417
1886
  try {
@@ -1438,7 +1907,10 @@ export function registerWriteTools(server, client) {
1438
1907
  project_id: z.string().uuid().describe('Project ID (required)'),
1439
1908
  name: z.string().min(1).max(500).describe('Phase name (required)'),
1440
1909
  description: z.string().optional().describe('Description'),
1441
- status: z.enum(['pending', 'in_progress', 'completed', 'skipped']).optional().describe('Phase status'),
1910
+ status: z
1911
+ .enum(['pending', 'in_progress', 'completed', 'skipped'])
1912
+ .optional()
1913
+ .describe('Phase status'),
1442
1914
  start_date: z.string().optional().describe('Start date (ISO 8601)'),
1443
1915
  end_date: z.string().optional().describe('End date (ISO 8601)'),
1444
1916
  sort_order: z.number().int().min(0).optional().describe('Sort order'),
@@ -1456,7 +1928,10 @@ export function registerWriteTools(server, client) {
1456
1928
  project_id: z.string().uuid().optional().describe('Project ID'),
1457
1929
  name: z.string().min(1).max(500).optional().describe('Phase name'),
1458
1930
  description: z.string().optional().describe('Description'),
1459
- status: z.enum(['pending', 'in_progress', 'completed', 'skipped']).optional().describe('Phase status'),
1931
+ status: z
1932
+ .enum(['pending', 'in_progress', 'completed', 'skipped'])
1933
+ .optional()
1934
+ .describe('Phase status'),
1460
1935
  start_date: z.string().optional().describe('Start date (ISO 8601)'),
1461
1936
  end_date: z.string().optional().describe('End date (ISO 8601)'),
1462
1937
  sort_order: z.number().int().min(0).optional().describe('Sort order'),
@@ -1483,7 +1958,17 @@ export function registerWriteTools(server, client) {
1483
1958
  // ============================================================
1484
1959
  server.tool('create_project_budget_item', 'Create a new project budget item. Requires project_budget_items:write scope.', {
1485
1960
  project_id: z.string().uuid().describe('Project ID (required)'),
1486
- category: z.enum(['labor', 'materials', 'equipment', 'subcontractors', 'permits', 'contingency', 'other']).describe('Budget category (required)'),
1961
+ category: z
1962
+ .enum([
1963
+ 'labor',
1964
+ 'materials',
1965
+ 'equipment',
1966
+ 'subcontractors',
1967
+ 'permits',
1968
+ 'contingency',
1969
+ 'other',
1970
+ ])
1971
+ .describe('Budget category (required)'),
1487
1972
  description: z.string().optional().describe('Description'),
1488
1973
  estimated_amount: z.number().min(0).optional().describe('Estimated amount'),
1489
1974
  actual_amount: z.number().min(0).optional().describe('Actual amount'),
@@ -1499,7 +1984,18 @@ export function registerWriteTools(server, client) {
1499
1984
  server.tool('update_project_budget_item', 'Update an existing project budget item by ID. Requires project_budget_items:write scope.', {
1500
1985
  id: z.string().uuid().describe('Project budget item ID'),
1501
1986
  project_id: z.string().uuid().optional().describe('Project ID'),
1502
- category: z.enum(['labor', 'materials', 'equipment', 'subcontractors', 'permits', 'contingency', 'other']).optional().describe('Budget category'),
1987
+ category: z
1988
+ .enum([
1989
+ 'labor',
1990
+ 'materials',
1991
+ 'equipment',
1992
+ 'subcontractors',
1993
+ 'permits',
1994
+ 'contingency',
1995
+ 'other',
1996
+ ])
1997
+ .optional()
1998
+ .describe('Budget category'),
1503
1999
  description: z.string().optional().describe('Description'),
1504
2000
  estimated_amount: z.number().min(0).optional().describe('Estimated amount'),
1505
2001
  actual_amount: z.number().min(0).optional().describe('Actual amount'),
@@ -1810,7 +2306,13 @@ export function registerWriteTools(server, client) {
1810
2306
  server.tool('create_project_phase_category', 'Create a new project phase category. Requires project_phase_categories:write scope.', {
1811
2307
  name: z.string().min(1).max(500).describe('Phase name (required), e.g. "Planning", "Design"'),
1812
2308
  description: z.string().max(2000).optional().describe('Description of the phase'),
1813
- sort_order: z.number().int().min(0).max(10000).optional().describe('Display order (lower = first)'),
2309
+ sort_order: z
2310
+ .number()
2311
+ .int()
2312
+ .min(0)
2313
+ .max(10000)
2314
+ .optional()
2315
+ .describe('Display order (lower = first)'),
1814
2316
  }, async (params) => {
1815
2317
  try {
1816
2318
  const result = await client.create('project-phase-categories', buildBody(params));
@@ -1824,7 +2326,13 @@ export function registerWriteTools(server, client) {
1824
2326
  id: z.string().uuid().describe('Project phase category ID'),
1825
2327
  name: z.string().min(1).max(500).optional().describe('Phase name'),
1826
2328
  description: z.string().max(2000).optional().describe('Description'),
1827
- sort_order: z.number().int().min(0).max(10000).optional().describe('Display order (lower = first)'),
2329
+ sort_order: z
2330
+ .number()
2331
+ .int()
2332
+ .min(0)
2333
+ .max(10000)
2334
+ .optional()
2335
+ .describe('Display order (lower = first)'),
1828
2336
  }, async ({ id, ...rest }) => {
1829
2337
  try {
1830
2338
  const result = await client.update('project-phase-categories', id, buildBody(rest));
@@ -2129,9 +2637,15 @@ export function registerWriteTools(server, client) {
2129
2637
  // 35. Custom Field Definitions (scope: custom_fields)
2130
2638
  // ============================================================
2131
2639
  server.tool('create_custom_field_definition', 'Create a new custom field definition. Requires custom_fields:write scope.', {
2132
- entity_type: z.string().min(1).max(100).describe('Entity type this field applies to (required)'),
2640
+ entity_type: z
2641
+ .string()
2642
+ .min(1)
2643
+ .max(100)
2644
+ .describe('Entity type this field applies to (required)'),
2133
2645
  field_name: z.string().min(1).max(200).describe('Field name / key (required)'),
2134
- field_type: z.enum(['text', 'number', 'date', 'boolean', 'select']).describe('Field data type (required)'),
2646
+ field_type: z
2647
+ .enum(['text', 'number', 'date', 'boolean', 'select'])
2648
+ .describe('Field data type (required)'),
2135
2649
  field_label: z.string().max(200).optional().describe('Display label'),
2136
2650
  options: z.array(z.string()).optional().describe('Options for select-type fields'),
2137
2651
  is_required: z.boolean().optional().describe('Whether the field is required'),
@@ -2149,7 +2663,10 @@ export function registerWriteTools(server, client) {
2149
2663
  id: z.string().uuid().describe('Custom field definition ID'),
2150
2664
  entity_type: z.string().min(1).max(100).optional().describe('Entity type'),
2151
2665
  field_name: z.string().min(1).max(200).optional().describe('Field name / key'),
2152
- field_type: z.enum(['text', 'number', 'date', 'boolean', 'select']).optional().describe('Field data type'),
2666
+ field_type: z
2667
+ .enum(['text', 'number', 'date', 'boolean', 'select'])
2668
+ .optional()
2669
+ .describe('Field data type'),
2153
2670
  field_label: z.string().max(200).optional().describe('Display label'),
2154
2671
  options: z.array(z.string()).optional().describe('Options for select-type fields'),
2155
2672
  is_required: z.boolean().optional().describe('Whether the field is required'),
@@ -2175,14 +2692,38 @@ export function registerWriteTools(server, client) {
2175
2692
  // ============================================================
2176
2693
  // 36. Custom Field Values (scope: custom_fields)
2177
2694
  // ============================================================
2178
- server.tool('create_custom_field_value', 'Create (upsert) a custom field value for an entity. The table stores values in typed columns — prefer setting the one that matches the field definition\'s field_type: value_text (text/select), value_number (number), value_date (date, ISO YYYY-MM-DD), value_boolean (boolean). Alternatively pass a single `value` string and the server will dispatch it to the right column based on field_type. Writes upsert on (entity_id, field_definition_id) so replaying a batch is idempotent. Requires custom_fields:write scope.', {
2695
+ server.tool('create_custom_field_value', "Create (upsert) a custom field value for an entity. The table stores values in typed columns — prefer setting the one that matches the field definition's field_type: value_text (text/select), value_number (number), value_date (date, ISO YYYY-MM-DD), value_boolean (boolean). Alternatively pass a single `value` string and the server will dispatch it to the right column based on field_type. Writes upsert on (entity_id, field_definition_id) so replaying a batch is idempotent. Requires custom_fields:write scope.", {
2179
2696
  entity_id: z.string().uuid().describe('Entity ID — e.g. asset.id, work_order.id (required)'),
2180
- field_definition_id: z.string().uuid().describe('Custom field definition ID — resolve via list_custom_field_definitions (required)'),
2181
- value_text: z.string().max(5000).nullable().optional().describe('Text value (use for field_type=text or select)'),
2182
- value_number: z.number().nullable().optional().describe('Numeric value (use for field_type=number)'),
2183
- value_date: z.string().nullable().optional().describe('Date value, ISO YYYY-MM-DD (use for field_type=date)'),
2184
- value_boolean: z.boolean().nullable().optional().describe('Boolean value (use for field_type=boolean)'),
2185
- value: z.string().max(5000).optional().describe('Legacy single-value shim — server dispatches to the correct typed column based on the field definition\'s field_type. Ignored if any value_* typed column is set.'),
2697
+ field_definition_id: z
2698
+ .string()
2699
+ .uuid()
2700
+ .describe('Custom field definition ID resolve via list_custom_field_definitions (required)'),
2701
+ value_text: z
2702
+ .string()
2703
+ .max(5000)
2704
+ .nullable()
2705
+ .optional()
2706
+ .describe('Text value (use for field_type=text or select)'),
2707
+ value_number: z
2708
+ .number()
2709
+ .nullable()
2710
+ .optional()
2711
+ .describe('Numeric value (use for field_type=number)'),
2712
+ value_date: z
2713
+ .string()
2714
+ .nullable()
2715
+ .optional()
2716
+ .describe('Date value, ISO YYYY-MM-DD (use for field_type=date)'),
2717
+ value_boolean: z
2718
+ .boolean()
2719
+ .nullable()
2720
+ .optional()
2721
+ .describe('Boolean value (use for field_type=boolean)'),
2722
+ value: z
2723
+ .string()
2724
+ .max(5000)
2725
+ .optional()
2726
+ .describe("Legacy single-value shim — server dispatches to the correct typed column based on the field definition's field_type. Ignored if any value_* typed column is set."),
2186
2727
  }, async (params) => {
2187
2728
  try {
2188
2729
  const result = await client.create('custom-field-values', buildBody(params));
@@ -2192,15 +2733,23 @@ export function registerWriteTools(server, client) {
2192
2733
  return formatError(err);
2193
2734
  }
2194
2735
  });
2195
- server.tool('update_custom_field_value', 'Update an existing custom field value by ID. Set whichever typed column matches the definition\'s field_type (value_text / value_number / value_date / value_boolean), or pass a single `value` and the server will dispatch it. Requires custom_fields:write scope.', {
2736
+ server.tool('update_custom_field_value', "Update an existing custom field value by ID. Set whichever typed column matches the definition's field_type (value_text / value_number / value_date / value_boolean), or pass a single `value` and the server will dispatch it. Requires custom_fields:write scope.", {
2196
2737
  id: z.string().uuid().describe('Custom field value ID'),
2197
2738
  entity_id: z.string().uuid().optional().describe('Entity ID'),
2198
- field_definition_id: z.string().uuid().optional().describe('Custom field definition ID — required when using the `value` fallback if you want to avoid the server fetching it'),
2739
+ field_definition_id: z
2740
+ .string()
2741
+ .uuid()
2742
+ .optional()
2743
+ .describe('Custom field definition ID — required when using the `value` fallback if you want to avoid the server fetching it'),
2199
2744
  value_text: z.string().max(5000).nullable().optional().describe('Text value'),
2200
2745
  value_number: z.number().nullable().optional().describe('Numeric value'),
2201
2746
  value_date: z.string().nullable().optional().describe('Date value, ISO YYYY-MM-DD'),
2202
2747
  value_boolean: z.boolean().nullable().optional().describe('Boolean value'),
2203
- value: z.string().max(5000).optional().describe('Legacy single-value shim — server dispatches based on field_type'),
2748
+ value: z
2749
+ .string()
2750
+ .max(5000)
2751
+ .optional()
2752
+ .describe('Legacy single-value shim — server dispatches based on field_type'),
2204
2753
  }, async ({ id, ...rest }) => {
2205
2754
  try {
2206
2755
  const result = await client.update('custom-field-values', id, buildBody(rest));
@@ -2226,11 +2775,27 @@ export function registerWriteTools(server, client) {
2226
2775
  name: z.string().min(1).max(500).describe('Part name (required)'),
2227
2776
  part_number: z.string().max(200).optional().describe('Part number / SKU'),
2228
2777
  category: z.string().max(200).optional().describe('Category label'),
2229
- supplier: z.string().max(500).optional().describe('DEPRECATED — legacy free-text supplier name. Use supplier_id instead.'),
2778
+ supplier: z
2779
+ .string()
2780
+ .max(500)
2781
+ .optional()
2782
+ .describe('DEPRECATED — legacy free-text supplier name. Use supplier_id instead.'),
2230
2783
  supplier_id: z.string().uuid().optional().describe('Vendor ID (resolve via list_vendors)'),
2231
2784
  cost: z.number().min(0).optional().describe('Unit cost'),
2232
- quantity: z.number().int().min(0).max(10_000_000).optional().describe('Current stock quantity'),
2233
- desired_quantity: z.number().int().min(0).max(10_000_000).optional().describe('Target / reorder quantity'),
2785
+ quantity: z
2786
+ .number()
2787
+ .int()
2788
+ .min(0)
2789
+ .max(10_000_000)
2790
+ .optional()
2791
+ .describe('Current stock quantity'),
2792
+ desired_quantity: z
2793
+ .number()
2794
+ .int()
2795
+ .min(0)
2796
+ .max(10_000_000)
2797
+ .optional()
2798
+ .describe('Target / reorder quantity'),
2234
2799
  specific_location: z.string().max(500).optional().describe('Storage location description'),
2235
2800
  site_id: z.string().uuid().optional().describe('Site ID'),
2236
2801
  building_id: z.string().uuid().optional().describe('Building ID'),
@@ -2249,11 +2814,27 @@ export function registerWriteTools(server, client) {
2249
2814
  name: z.string().min(1).max(500).optional().describe('Part name'),
2250
2815
  part_number: z.string().max(200).optional().describe('Part number / SKU'),
2251
2816
  category: z.string().max(200).optional().describe('Category label'),
2252
- supplier: z.string().max(500).optional().describe('DEPRECATED — legacy free-text supplier name. Use supplier_id instead.'),
2817
+ supplier: z
2818
+ .string()
2819
+ .max(500)
2820
+ .optional()
2821
+ .describe('DEPRECATED — legacy free-text supplier name. Use supplier_id instead.'),
2253
2822
  supplier_id: z.string().uuid().optional().describe('Vendor ID (resolve via list_vendors)'),
2254
2823
  cost: z.number().min(0).optional().describe('Unit cost'),
2255
- quantity: z.number().int().min(0).max(10_000_000).optional().describe('Current stock quantity'),
2256
- desired_quantity: z.number().int().min(0).max(10_000_000).optional().describe('Target / reorder quantity'),
2824
+ quantity: z
2825
+ .number()
2826
+ .int()
2827
+ .min(0)
2828
+ .max(10_000_000)
2829
+ .optional()
2830
+ .describe('Current stock quantity'),
2831
+ desired_quantity: z
2832
+ .number()
2833
+ .int()
2834
+ .min(0)
2835
+ .max(10_000_000)
2836
+ .optional()
2837
+ .describe('Target / reorder quantity'),
2257
2838
  specific_location: z.string().max(500).optional().describe('Storage location description'),
2258
2839
  site_id: z.string().uuid().optional().describe('Site ID'),
2259
2840
  building_id: z.string().uuid().optional().describe('Building ID'),
@@ -2280,7 +2861,15 @@ export function registerWriteTools(server, client) {
2280
2861
  // Upload URLs
2281
2862
  // ============================================================
2282
2863
  server.tool('create_upload_url', 'Generate a signed upload URL for uploading a file to AssetLab storage. Returns a signed_url to PUT the file to, a public_url for referencing, and the path. IMPORTANT — When a user wants to upload a file, always clarify the target. File upload paths: (1) Asset IMAGE: bucket "asset-images" → update_asset with image_url. (2) Asset DOCUMENT (O&M, warranty, spec): bucket "documents" → create_asset_document. (3) Work order IMAGE: bucket "attachments" → update_work_order with image_url. (4) Work order/request/PM ATTACHMENT: bucket "attachments" → create_attachment with the parent ID. (5) Project DOCUMENT: bucket "project-documents" → create_project_document. (6) Contract DOCUMENT: bucket "contract-documents" → create_contract_document. Always ask the user which type they mean if ambiguous. Requires upload_urls:write scope.', {
2283
- bucket: z.enum(['documents', 'attachments', 'project-documents', 'contract-documents', 'asset-images']).describe('Storage bucket (required). Use "asset-images" for asset photos.'),
2864
+ bucket: z
2865
+ .enum([
2866
+ 'documents',
2867
+ 'attachments',
2868
+ 'project-documents',
2869
+ 'contract-documents',
2870
+ 'asset-images',
2871
+ ])
2872
+ .describe('Storage bucket (required). Use "asset-images" for asset photos.'),
2284
2873
  file_name: z.string().min(1).max(500).describe('File name including extension (required)'),
2285
2874
  }, async (params) => {
2286
2875
  try {
@@ -2292,10 +2881,25 @@ export function registerWriteTools(server, client) {
2292
2881
  }
2293
2882
  });
2294
2883
  server.tool('upload_file', 'Upload a file to AssetLab storage by sending its bytes inline (base64). The AssetLab backend performs the storage upload server-side — use this tool when the client cannot PUT directly to Supabase Storage (e.g. Claude integrations whose outbound network blocks arbitrary supabase.co hosts). Returns { path, public_url, bucket, file_size, content_type }. After uploading, pass path or public_url to the appropriate record tool (update_asset image_url, create_asset_document file_path, update_work_order image_url, create_attachment file_path, create_project_document file_path, create_contract_document file_path). Server limit is ~10 MB decoded; MCP arg ceiling effectively caps file size around 700 KB–1 MB. For larger files, use create_upload_url instead. Requires upload_urls:write scope.', {
2295
- bucket: z.enum(['documents', 'attachments', 'project-documents', 'contract-documents', 'asset-images']).describe('Storage bucket (required). Use "asset-images" for asset photos, "attachments" for work-order/PM attachments.'),
2884
+ bucket: z
2885
+ .enum([
2886
+ 'documents',
2887
+ 'attachments',
2888
+ 'project-documents',
2889
+ 'contract-documents',
2890
+ 'asset-images',
2891
+ ])
2892
+ .describe('Storage bucket (required). Use "asset-images" for asset photos, "attachments" for work-order/PM attachments.'),
2296
2893
  file_name: z.string().min(1).max(500).describe('File name including extension (required)'),
2297
- content_base64: z.string().min(1).describe('File contents base64-encoded (required). Data URI prefixes like "data:image/png;base64," are stripped automatically.'),
2298
- content_type: z.string().max(200).optional().describe('MIME type (e.g. image/jpeg, application/pdf). Defaults to application/octet-stream.'),
2894
+ content_base64: z
2895
+ .string()
2896
+ .min(1)
2897
+ .describe('File contents base64-encoded (required). Data URI prefixes like "data:image/png;base64," are stripped automatically.'),
2898
+ content_type: z
2899
+ .string()
2900
+ .max(200)
2901
+ .optional()
2902
+ .describe('MIME type (e.g. image/jpeg, application/pdf). Defaults to application/octet-stream.'),
2299
2903
  }, async (params) => {
2300
2904
  try {
2301
2905
  const result = await client.create('upload-files', buildBody(params));
@@ -2310,9 +2914,16 @@ export function registerWriteTools(server, client) {
2310
2914
  // ============================================================
2311
2915
  server.tool('create_asset_document', 'Create an asset document record (after uploading the file via create_upload_url). Requires asset_documents:write scope.', {
2312
2916
  name: z.string().min(1).max(500).describe('Document name (required)'),
2313
- file_path: z.string().min(1).max(2000).describe('Storage path from upload URL response (required)'),
2917
+ file_path: z
2918
+ .string()
2919
+ .min(1)
2920
+ .max(2000)
2921
+ .describe('Storage path from upload URL response (required)'),
2314
2922
  asset_id: z.string().uuid().describe('Asset ID this document belongs to (required)'),
2315
- category: z.enum(['om', 'commissioning', 'warranty', 'installation', 'specification', 'other']).optional().describe('Document category'),
2923
+ category: z
2924
+ .enum(['om', 'commissioning', 'warranty', 'installation', 'specification', 'other'])
2925
+ .optional()
2926
+ .describe('Document category'),
2316
2927
  description: z.string().optional().describe('Description'),
2317
2928
  file_type: z.string().max(200).optional().describe('MIME type'),
2318
2929
  file_size: z.number().min(0).optional().describe('File size in bytes'),
@@ -2331,7 +2942,10 @@ export function registerWriteTools(server, client) {
2331
2942
  name: z.string().min(1).max(500).optional().describe('Document name'),
2332
2943
  file_path: z.string().max(2000).optional().describe('Storage path'),
2333
2944
  asset_id: z.string().uuid().optional().describe('Asset ID'),
2334
- category: z.enum(['om', 'commissioning', 'warranty', 'installation', 'specification', 'other']).optional().describe('Document category'),
2945
+ category: z
2946
+ .enum(['om', 'commissioning', 'warranty', 'installation', 'specification', 'other'])
2947
+ .optional()
2948
+ .describe('Document category'),
2335
2949
  description: z.string().optional().describe('Description'),
2336
2950
  file_type: z.string().max(200).optional().describe('MIME type'),
2337
2951
  file_size: z.number().min(0).optional().describe('File size in bytes'),
@@ -2364,10 +2978,26 @@ export function registerWriteTools(server, client) {
2364
2978
  file_type: z.string().max(200).optional().describe('MIME type'),
2365
2979
  uploaded_by: z.string().max(200).optional().describe('Uploader user ID'),
2366
2980
  description: z.string().optional().describe('Description'),
2367
- work_order_id: z.string().uuid().optional().describe('Work order ID (exactly one parent required)'),
2368
- work_request_id: z.string().uuid().optional().describe('Work request ID (exactly one parent required)'),
2369
- pm_schedule_id: z.string().uuid().optional().describe('PM schedule ID (exactly one parent required)'),
2370
- pm_template_id: z.string().uuid().optional().describe('PM template ID (exactly one parent required)'),
2981
+ work_order_id: z
2982
+ .string()
2983
+ .uuid()
2984
+ .optional()
2985
+ .describe('Work order ID (exactly one parent required)'),
2986
+ work_request_id: z
2987
+ .string()
2988
+ .uuid()
2989
+ .optional()
2990
+ .describe('Work request ID (exactly one parent required)'),
2991
+ pm_schedule_id: z
2992
+ .string()
2993
+ .uuid()
2994
+ .optional()
2995
+ .describe('PM schedule ID (exactly one parent required)'),
2996
+ pm_template_id: z
2997
+ .string()
2998
+ .uuid()
2999
+ .optional()
3000
+ .describe('PM template ID (exactly one parent required)'),
2371
3001
  }, async (params) => {
2372
3002
  try {
2373
3003
  const result = await client.create('attachments', buildBody(params));
@@ -2413,7 +3043,11 @@ export function registerWriteTools(server, client) {
2413
3043
  server.tool('create_project_document', 'Create a project document record. Requires project_documents:write scope.', {
2414
3044
  project_id: z.string().uuid().describe('Project ID (required)'),
2415
3045
  name: z.string().min(1).max(500).describe('Document name (required)'),
2416
- file_path: z.string().min(1).max(2000).describe('Storage path from upload URL response (required)'),
3046
+ file_path: z
3047
+ .string()
3048
+ .min(1)
3049
+ .max(2000)
3050
+ .describe('Storage path from upload URL response (required)'),
2417
3051
  uploaded_by: z.string().min(1).max(200).describe('Uploader user ID (required)'),
2418
3052
  folder_id: z.string().uuid().optional().describe('Folder ID'),
2419
3053
  description: z.string().optional().describe('Description'),
@@ -2462,7 +3096,11 @@ export function registerWriteTools(server, client) {
2462
3096
  server.tool('create_contract_document', 'Create a contract document record. Requires contract_documents:write scope.', {
2463
3097
  contract_id: z.string().uuid().describe('Contract ID (required)'),
2464
3098
  file_name: z.string().min(1).max(500).describe('File name (required)'),
2465
- file_path: z.string().min(1).max(2000).describe('Storage path from upload URL response (required)'),
3099
+ file_path: z
3100
+ .string()
3101
+ .min(1)
3102
+ .max(2000)
3103
+ .describe('Storage path from upload URL response (required)'),
2466
3104
  file_size: z.number().min(0).optional().describe('File size in bytes'),
2467
3105
  file_type: z.string().max(200).optional().describe('MIME type'),
2468
3106
  uploaded_by: z.string().max(200).optional().describe('Uploader user ID'),
@@ -2554,7 +3192,10 @@ export function registerWriteTools(server, client) {
2554
3192
  server.tool('create_project_task_dependency', 'Create a dependency between two project tasks. Requires project_task_dependencies:write scope.', {
2555
3193
  task_id: z.string().uuid().describe('Task ID (the dependent task, required)'),
2556
3194
  depends_on_task_id: z.string().uuid().describe('Task ID that must complete first (required)'),
2557
- dependency_type: z.enum(['finish_to_start', 'start_to_start', 'finish_to_finish', 'start_to_finish']).optional().describe('Dependency type (default: finish_to_start)'),
3195
+ dependency_type: z
3196
+ .enum(['finish_to_start', 'start_to_start', 'finish_to_finish', 'start_to_finish'])
3197
+ .optional()
3198
+ .describe('Dependency type (default: finish_to_start)'),
2558
3199
  }, async (params) => {
2559
3200
  try {
2560
3201
  const result = await client.create('project-task-dependencies', buildBody(params));
@@ -2579,9 +3220,20 @@ export function registerWriteTools(server, client) {
2579
3220
  server.tool('create_project_update', 'Create a periodic project status update. Requires project_updates:write scope.', {
2580
3221
  project_id: z.string().uuid().describe('Project ID (required)'),
2581
3222
  author_id: z.string().min(1).max(200).describe('Author Clerk user ID (required)'),
2582
- timeframe: z.enum(['monthly', 'quarterly', 'bi-annually', 'annually']).describe('Update timeframe (required)'),
2583
- period_year: z.number().int().min(2000).max(2100).describe('Year for this update period (required)'),
2584
- period_value: z.string().min(1).max(20).describe('Period value — 1-12 for monthly, 1-4 for quarterly, etc. (required)'),
3223
+ timeframe: z
3224
+ .enum(['monthly', 'quarterly', 'bi-annually', 'annually'])
3225
+ .describe('Update timeframe (required)'),
3226
+ period_year: z
3227
+ .number()
3228
+ .int()
3229
+ .min(2000)
3230
+ .max(2100)
3231
+ .describe('Year for this update period (required)'),
3232
+ period_value: z
3233
+ .string()
3234
+ .min(1)
3235
+ .max(20)
3236
+ .describe('Period value — 1-12 for monthly, 1-4 for quarterly, etc. (required)'),
2585
3237
  content: z.string().min(1).describe('Update content (required)'),
2586
3238
  title: z.string().max(500).optional().describe('Optional custom title'),
2587
3239
  }, async (params) => {
@@ -2597,8 +3249,17 @@ export function registerWriteTools(server, client) {
2597
3249
  id: z.string().uuid().describe('Project update ID'),
2598
3250
  project_id: z.string().uuid().optional().describe('Project ID'),
2599
3251
  author_id: z.string().min(1).max(200).optional().describe('Author Clerk user ID'),
2600
- timeframe: z.enum(['monthly', 'quarterly', 'bi-annually', 'annually']).optional().describe('Update timeframe'),
2601
- period_year: z.number().int().min(2000).max(2100).optional().describe('Year for this update period'),
3252
+ timeframe: z
3253
+ .enum(['monthly', 'quarterly', 'bi-annually', 'annually'])
3254
+ .optional()
3255
+ .describe('Update timeframe'),
3256
+ period_year: z
3257
+ .number()
3258
+ .int()
3259
+ .min(2000)
3260
+ .max(2100)
3261
+ .optional()
3262
+ .describe('Year for this update period'),
2602
3263
  period_value: z.string().min(1).max(20).optional().describe('Period value'),
2603
3264
  content: z.string().min(1).optional().describe('Update content'),
2604
3265
  title: z.string().max(500).optional().describe('Optional custom title'),
@@ -2629,7 +3290,12 @@ export function registerWriteTools(server, client) {
2629
3290
  total_budget: z.number().min(0).describe('Total budget amount (required)'),
2630
3291
  actual_cost: z.number().min(0).describe('Actual cost to date (required)'),
2631
3292
  forecasted_cost: z.number().min(0).optional().describe('Forecasted total cost'),
2632
- percent_complete: z.number().min(0).max(100).optional().describe('Completion percentage (0-100)'),
3293
+ percent_complete: z
3294
+ .number()
3295
+ .min(0)
3296
+ .max(100)
3297
+ .optional()
3298
+ .describe('Completion percentage (0-100)'),
2633
3299
  }, async (params) => {
2634
3300
  try {
2635
3301
  const result = await client.create('project-cost-snapshots', buildBody(params));
@@ -2799,10 +3465,16 @@ export function registerWriteTools(server, client) {
2799
3465
  project_id: z.string().uuid().describe('Project ID (required)'),
2800
3466
  title: z.string().min(1).max(500).describe('Risk title (required)'),
2801
3467
  description: z.string().optional().describe('Risk description'),
2802
- category: z.enum(['technical', 'financial', 'schedule', 'resource', 'external']).optional().describe('Risk category'),
3468
+ category: z
3469
+ .enum(['technical', 'financial', 'schedule', 'resource', 'external'])
3470
+ .optional()
3471
+ .describe('Risk category'),
2803
3472
  probability: z.enum(['low', 'medium', 'high']).optional().describe('Probability level'),
2804
3473
  impact: z.enum(['low', 'medium', 'high', 'critical']).optional().describe('Impact level'),
2805
- status: z.enum(['identified', 'analyzing', 'mitigating', 'resolved', 'accepted']).optional().describe('Risk status (default: identified)'),
3474
+ status: z
3475
+ .enum(['identified', 'analyzing', 'mitigating', 'resolved', 'accepted'])
3476
+ .optional()
3477
+ .describe('Risk status (default: identified)'),
2806
3478
  mitigation_plan: z.string().optional().describe('Mitigation plan'),
2807
3479
  contingency_plan: z.string().optional().describe('Contingency plan'),
2808
3480
  owner_id: z.string().max(200).optional().describe('Risk owner (Clerk user ID)'),
@@ -2822,10 +3494,16 @@ export function registerWriteTools(server, client) {
2822
3494
  project_id: z.string().uuid().optional().describe('Project ID'),
2823
3495
  title: z.string().min(1).max(500).optional().describe('Risk title'),
2824
3496
  description: z.string().optional().describe('Risk description'),
2825
- category: z.enum(['technical', 'financial', 'schedule', 'resource', 'external']).optional().describe('Risk category'),
3497
+ category: z
3498
+ .enum(['technical', 'financial', 'schedule', 'resource', 'external'])
3499
+ .optional()
3500
+ .describe('Risk category'),
2826
3501
  probability: z.enum(['low', 'medium', 'high']).optional().describe('Probability level'),
2827
3502
  impact: z.enum(['low', 'medium', 'high', 'critical']).optional().describe('Impact level'),
2828
- status: z.enum(['identified', 'analyzing', 'mitigating', 'resolved', 'accepted']).optional().describe('Risk status'),
3503
+ status: z
3504
+ .enum(['identified', 'analyzing', 'mitigating', 'resolved', 'accepted'])
3505
+ .optional()
3506
+ .describe('Risk status'),
2829
3507
  mitigation_plan: z.string().optional().describe('Mitigation plan'),
2830
3508
  contingency_plan: z.string().optional().describe('Contingency plan'),
2831
3509
  owner_id: z.string().max(200).optional().describe('Risk owner (Clerk user ID)'),
@@ -2881,7 +3559,10 @@ export function registerWriteTools(server, client) {
2881
3559
  icon: z.string().max(200).optional().describe('Icon name (e.g., "droplets" for water)'),
2882
3560
  color: z.string().max(50).optional().describe('Hex color code (e.g., "#3B82F6")'),
2883
3561
  sort_order: z.number().int().min(0).optional().describe('Sort order for display'),
2884
- is_active: z.boolean().optional().describe('Whether the service area is active (default: true)'),
3562
+ is_active: z
3563
+ .boolean()
3564
+ .optional()
3565
+ .describe('Whether the service area is active (default: true)'),
2885
3566
  }, async (params) => {
2886
3567
  try {
2887
3568
  const result = await client.create('service-areas', buildBody(params));
@@ -2971,24 +3652,65 @@ export function registerWriteTools(server, client) {
2971
3652
  server.tool('create_los_measure', 'Create a new LoS measure within a service area. Requires los_measures:write scope. Resolve service_area_id first via list_service_areas.', {
2972
3653
  service_area_id: z.string().uuid().describe('Service area ID (required)'),
2973
3654
  name: z.string().min(1).max(500).describe('Measure name (required, unique per service area)'),
2974
- category: z.enum(['quality', 'reliability', 'responsiveness', 'safety', 'sustainability', 'cost_efficiency', 'capacity']).describe('Measure category (required)'),
3655
+ category: z
3656
+ .enum([
3657
+ 'quality',
3658
+ 'reliability',
3659
+ 'responsiveness',
3660
+ 'safety',
3661
+ 'sustainability',
3662
+ 'cost_efficiency',
3663
+ 'capacity',
3664
+ ])
3665
+ .describe('Measure category (required)'),
2975
3666
  type: z.enum(['community', 'technical']).describe('Measure type (required)'),
2976
- data_source: z.enum([
2977
- 'manual', 'custom_formula', 'asset_condition_avg', 'asset_condition_pct_above',
2978
- 'asset_condition_pct_below', 'risk_score_avg', 'risk_pct_critical',
2979
- 'wo_response_time_avg', 'wo_completion_time_avg', 'wo_backlog_count', 'wo_overdue_count',
2980
- 'pm_compliance_rate', 'compliance_score', 'fci', 'deferred_maintenance_ratio',
3667
+ data_source: z
3668
+ .enum([
3669
+ 'manual',
3670
+ 'custom_formula',
3671
+ 'asset_condition_avg',
3672
+ 'asset_condition_pct_above',
3673
+ 'asset_condition_pct_below',
3674
+ 'risk_score_avg',
3675
+ 'risk_pct_critical',
3676
+ 'wo_response_time_avg',
3677
+ 'wo_completion_time_avg',
3678
+ 'wo_backlog_count',
3679
+ 'wo_overdue_count',
3680
+ 'pm_compliance_rate',
3681
+ 'compliance_score',
3682
+ 'fci',
3683
+ 'deferred_maintenance_ratio',
2981
3684
  'asset_past_useful_life_pct',
2982
- ]).describe('Data source type (required). Use "manual" if values will be entered by hand.'),
3685
+ ])
3686
+ .describe('Data source type (required). Use "manual" if values will be entered by hand.'),
2983
3687
  description: z.string().max(2000).optional().describe('Description'),
2984
- community_statement: z.string().max(2000).optional().describe('Community-facing statement (for community type measures)'),
2985
- unit: z.string().max(100).optional().describe('Unit of measurement (e.g., "%", "hours", "count")'),
2986
- trend_direction: z.enum(['higher_is_better', 'lower_is_better', 'target_is_optimal']).optional().describe('Which direction is better'),
3688
+ community_statement: z
3689
+ .string()
3690
+ .max(2000)
3691
+ .optional()
3692
+ .describe('Community-facing statement (for community type measures)'),
3693
+ unit: z
3694
+ .string()
3695
+ .max(100)
3696
+ .optional()
3697
+ .describe('Unit of measurement (e.g., "%", "hours", "count")'),
3698
+ trend_direction: z
3699
+ .enum(['higher_is_better', 'lower_is_better', 'target_is_optimal'])
3700
+ .optional()
3701
+ .describe('Which direction is better'),
2987
3702
  target_value: z.number().optional().describe('Target value'),
2988
3703
  minimum_acceptable: z.number().optional().describe('Minimum acceptable value'),
2989
3704
  stretch_goal: z.number().optional().describe('Stretch goal value'),
2990
- weight: z.number().min(0).optional().describe('Weight for composite score calculation (default: 1.0)'),
2991
- data_source_config: z.record(z.unknown()).optional().describe('Data source configuration (JSONB). E.g., {"threshold": 3} for pct_above/below, {"days_back": 90} for WO metrics.'),
3705
+ weight: z
3706
+ .number()
3707
+ .min(0)
3708
+ .optional()
3709
+ .describe('Weight for composite score calculation (default: 1.0)'),
3710
+ data_source_config: z
3711
+ .record(z.unknown())
3712
+ .optional()
3713
+ .describe('Data source configuration (JSONB). E.g., {"threshold": 3} for pct_above/below, {"days_back": 90} for WO metrics.'),
2992
3714
  is_active: z.boolean().optional().describe('Whether the measure is active (default: true)'),
2993
3715
  sort_order: z.number().int().min(0).optional().describe('Sort order for display'),
2994
3716
  }, async (params) => {
@@ -3003,24 +3725,55 @@ export function registerWriteTools(server, client) {
3003
3725
  server.tool('update_los_measure', 'Update an existing LoS measure by ID. Requires los_measures:write scope.', {
3004
3726
  id: z.string().uuid().describe('LoS measure ID'),
3005
3727
  name: z.string().min(1).max(500).optional().describe('Measure name'),
3006
- category: z.enum(['quality', 'reliability', 'responsiveness', 'safety', 'sustainability', 'cost_efficiency', 'capacity']).optional().describe('Measure category'),
3728
+ category: z
3729
+ .enum([
3730
+ 'quality',
3731
+ 'reliability',
3732
+ 'responsiveness',
3733
+ 'safety',
3734
+ 'sustainability',
3735
+ 'cost_efficiency',
3736
+ 'capacity',
3737
+ ])
3738
+ .optional()
3739
+ .describe('Measure category'),
3007
3740
  type: z.enum(['community', 'technical']).optional().describe('Measure type'),
3008
- data_source: z.enum([
3009
- 'manual', 'custom_formula', 'asset_condition_avg', 'asset_condition_pct_above',
3010
- 'asset_condition_pct_below', 'risk_score_avg', 'risk_pct_critical',
3011
- 'wo_response_time_avg', 'wo_completion_time_avg', 'wo_backlog_count', 'wo_overdue_count',
3012
- 'pm_compliance_rate', 'compliance_score', 'fci', 'deferred_maintenance_ratio',
3741
+ data_source: z
3742
+ .enum([
3743
+ 'manual',
3744
+ 'custom_formula',
3745
+ 'asset_condition_avg',
3746
+ 'asset_condition_pct_above',
3747
+ 'asset_condition_pct_below',
3748
+ 'risk_score_avg',
3749
+ 'risk_pct_critical',
3750
+ 'wo_response_time_avg',
3751
+ 'wo_completion_time_avg',
3752
+ 'wo_backlog_count',
3753
+ 'wo_overdue_count',
3754
+ 'pm_compliance_rate',
3755
+ 'compliance_score',
3756
+ 'fci',
3757
+ 'deferred_maintenance_ratio',
3013
3758
  'asset_past_useful_life_pct',
3014
- ]).optional().describe('Data source type'),
3759
+ ])
3760
+ .optional()
3761
+ .describe('Data source type'),
3015
3762
  description: z.string().max(2000).optional().describe('Description'),
3016
3763
  community_statement: z.string().max(2000).optional().describe('Community-facing statement'),
3017
3764
  unit: z.string().max(100).optional().describe('Unit of measurement'),
3018
- trend_direction: z.enum(['higher_is_better', 'lower_is_better', 'target_is_optimal']).optional().describe('Which direction is better'),
3765
+ trend_direction: z
3766
+ .enum(['higher_is_better', 'lower_is_better', 'target_is_optimal'])
3767
+ .optional()
3768
+ .describe('Which direction is better'),
3019
3769
  target_value: z.number().optional().describe('Target value'),
3020
3770
  minimum_acceptable: z.number().optional().describe('Minimum acceptable value'),
3021
3771
  stretch_goal: z.number().optional().describe('Stretch goal value'),
3022
3772
  weight: z.number().min(0).optional().describe('Weight for composite score calculation'),
3023
- data_source_config: z.record(z.unknown()).optional().describe('Data source configuration (JSONB)'),
3773
+ data_source_config: z
3774
+ .record(z.unknown())
3775
+ .optional()
3776
+ .describe('Data source configuration (JSONB)'),
3024
3777
  is_active: z.boolean().optional().describe('Active status'),
3025
3778
  sort_order: z.number().int().min(0).optional().describe('Sort order'),
3026
3779
  }, async ({ id, ...rest }) => {
@@ -3046,12 +3799,19 @@ export function registerWriteTools(server, client) {
3046
3799
  // ============================================================
3047
3800
  server.tool('create_los_measurement', 'Record a new LoS measurement value. Requires los_measurements:write scope. Resolve los_measure_id first via list_los_measures.', {
3048
3801
  los_measure_id: z.string().uuid().describe('LoS measure ID (required)'),
3049
- period_type: z.enum(['monthly', 'quarterly', 'semi_annual', 'annual']).describe('Period type (required)'),
3050
- period_start: z.string().describe('Period start date (ISO 8601, required, e.g., "2026-01-01")'),
3802
+ period_type: z
3803
+ .enum(['monthly', 'quarterly', 'semi_annual', 'annual'])
3804
+ .describe('Period type (required)'),
3805
+ period_start: z
3806
+ .string()
3807
+ .describe('Period start date (ISO 8601, required, e.g., "2026-01-01")'),
3051
3808
  period_end: z.string().describe('Period end date (ISO 8601, required, e.g., "2026-03-31")'),
3052
3809
  actual_value: z.number().describe('Measured value (required)'),
3053
3810
  notes: z.string().max(2000).optional().describe('Notes or context for this measurement'),
3054
- is_auto: z.boolean().optional().describe('Whether this is an auto-calculated value (default: false)'),
3811
+ is_auto: z
3812
+ .boolean()
3813
+ .optional()
3814
+ .describe('Whether this is an auto-calculated value (default: false)'),
3055
3815
  }, async (params) => {
3056
3816
  try {
3057
3817
  const result = await client.create('los-measurements', buildBody(params));
@@ -3065,7 +3825,10 @@ export function registerWriteTools(server, client) {
3065
3825
  id: z.string().uuid().describe('LoS measurement ID'),
3066
3826
  actual_value: z.number().optional().describe('Measured value'),
3067
3827
  notes: z.string().max(2000).optional().describe('Notes or context'),
3068
- period_type: z.enum(['monthly', 'quarterly', 'semi_annual', 'annual']).optional().describe('Period type'),
3828
+ period_type: z
3829
+ .enum(['monthly', 'quarterly', 'semi_annual', 'annual'])
3830
+ .optional()
3831
+ .describe('Period type'),
3069
3832
  period_start: z.string().optional().describe('Period start date (ISO 8601)'),
3070
3833
  period_end: z.string().optional().describe('Period end date (ISO 8601)'),
3071
3834
  is_auto: z.boolean().optional().describe('Whether this is auto-calculated'),
@@ -3097,14 +3860,39 @@ export function registerWriteTools(server, client) {
3097
3860
  .min(3)
3098
3861
  .describe('Polygon outline as an array of [x, y] points in normalized 0-1 coordinates (origin top-left). At least 3 points.');
3099
3862
  server.tool('create_floorplan', 'Create a floorplan row. Provide EXACTLY ONE of building_id (per-building floor) or site_id (site-level / campus plan). Typically the web app calls this per page of an uploaded PDF; MCP clients rarely need to call this directly since they do not upload the PDF itself. Requires floorplans:write scope.', {
3100
- building_id: z.string().uuid().optional().describe('Building this floor belongs to (omit if site-scoped)'),
3101
- site_id: z.string().uuid().optional().describe('Site this plan belongs to (use for site-level / campus plans; omit if building-scoped)'),
3102
- floor_label: z.string().max(200).describe('Human-readable label (e.g. "Ground Floor", "Mezzanine", "Site Plan")'),
3103
- floor_order: z.number().int().min(0).optional().describe('Sort order within the scope (lowest first)'),
3863
+ building_id: z
3864
+ .string()
3865
+ .uuid()
3866
+ .optional()
3867
+ .describe('Building this floor belongs to (omit if site-scoped)'),
3868
+ site_id: z
3869
+ .string()
3870
+ .uuid()
3871
+ .optional()
3872
+ .describe('Site this plan belongs to (use for site-level / campus plans; omit if building-scoped)'),
3873
+ floor_label: z
3874
+ .string()
3875
+ .max(200)
3876
+ .describe('Human-readable label (e.g. "Ground Floor", "Mezzanine", "Site Plan")'),
3877
+ floor_order: z
3878
+ .number()
3879
+ .int()
3880
+ .min(0)
3881
+ .optional()
3882
+ .describe('Sort order within the scope (lowest first)'),
3104
3883
  pdf_storage_path: z.string().max(1000).describe('Supabase Storage path to the PDF file'),
3105
3884
  pdf_filename: z.string().max(500).describe('Original filename for display'),
3106
- page_number: z.number().int().min(1).optional().describe('1-indexed page number within the PDF'),
3107
- page_width_pt: z.number().min(0).optional().describe('Page width in PDF points (discovered client-side)'),
3885
+ page_number: z
3886
+ .number()
3887
+ .int()
3888
+ .min(1)
3889
+ .optional()
3890
+ .describe('1-indexed page number within the PDF'),
3891
+ page_width_pt: z
3892
+ .number()
3893
+ .min(0)
3894
+ .optional()
3895
+ .describe('Page width in PDF points (discovered client-side)'),
3108
3896
  page_height_pt: z.number().min(0).optional().describe('Page height in PDF points'),
3109
3897
  status: z.enum(FLOORPLAN_STATUSES).optional().describe('Detection status (default: pending)'),
3110
3898
  }, async (params) => {
@@ -3149,10 +3937,25 @@ export function registerWriteTools(server, client) {
3149
3937
  floorplan_id: z.string().uuid().describe('Floorplan this region belongs to'),
3150
3938
  label: z.string().max(500).describe('Region label (e.g. "Boiler Room 2B")'),
3151
3939
  polygon: polygonSchema,
3152
- location_id: z.string().uuid().optional().describe('Linked Location ID (resolved via list_locations)'),
3153
- source: z.enum(REGION_SOURCES).optional().describe('"manual" (default) or "ai" for AI-detected'),
3154
- confidence: z.number().min(0).max(1).optional().describe('AI confidence score (0-1), only set when source=ai'),
3155
- reviewed: z.boolean().optional().describe('True if an admin has reviewed this region (default: true for manual, false for ai)'),
3940
+ location_id: z
3941
+ .string()
3942
+ .uuid()
3943
+ .optional()
3944
+ .describe('Linked Location ID (resolved via list_locations)'),
3945
+ source: z
3946
+ .enum(REGION_SOURCES)
3947
+ .optional()
3948
+ .describe('"manual" (default) or "ai" for AI-detected'),
3949
+ confidence: z
3950
+ .number()
3951
+ .min(0)
3952
+ .max(1)
3953
+ .optional()
3954
+ .describe('AI confidence score (0-1), only set when source=ai'),
3955
+ reviewed: z
3956
+ .boolean()
3957
+ .optional()
3958
+ .describe('True if an admin has reviewed this region (default: true for manual, false for ai)'),
3156
3959
  }, async (params) => {
3157
3960
  try {
3158
3961
  const result = await client.create('floorplan-regions', buildBody(params));
@@ -3166,7 +3969,11 @@ export function registerWriteTools(server, client) {
3166
3969
  id: z.string().uuid().describe('Floorplan region ID'),
3167
3970
  label: z.string().max(500).optional().describe('New label'),
3168
3971
  polygon: polygonSchema.optional(),
3169
- location_id: z.string().uuid().optional().describe('Linked Location ID (set to null to unlink)'),
3972
+ location_id: z
3973
+ .string()
3974
+ .uuid()
3975
+ .optional()
3976
+ .describe('Linked Location ID (set to null to unlink)'),
3170
3977
  confidence: z.number().min(0).max(1).optional(),
3171
3978
  reviewed: z.boolean().optional().describe('Mark region as reviewed by admin'),
3172
3979
  }, async ({ id, ...rest }) => {
@@ -3192,8 +3999,15 @@ export function registerWriteTools(server, client) {
3192
3999
  floorplan_id: z.string().uuid().describe('Target floorplan (resolve via list_floorplans)'),
3193
4000
  x: z.number().min(0).max(1).describe('Normalized x coordinate (0=left, 1=right)'),
3194
4001
  y: z.number().min(0).max(1).describe('Normalized y coordinate (0=top, 1=bottom)'),
3195
- region_id: z.string().uuid().optional().describe('Optional region the pin sits inside (usually auto-inferred)'),
3196
- source: z.enum(REGION_SOURCES).optional().describe('"manual" (default) or "ai" for AI-placed'),
4002
+ region_id: z
4003
+ .string()
4004
+ .uuid()
4005
+ .optional()
4006
+ .describe('Optional region the pin sits inside (usually auto-inferred)'),
4007
+ source: z
4008
+ .enum(REGION_SOURCES)
4009
+ .optional()
4010
+ .describe('"manual" (default) or "ai" for AI-placed'),
3197
4011
  }, async (params) => {
3198
4012
  try {
3199
4013
  const result = await client.create('asset-placements', buildBody(params));
@@ -3231,30 +4045,80 @@ export function registerWriteTools(server, client) {
3231
4045
  // Bulk operations
3232
4046
  // ============================================================
3233
4047
  const BULK_RESOURCES = [
3234
- 'assets', 'work-orders', 'work-requests', 'vendors', 'sites',
3235
- 'buildings', 'locations', 'systems', 'system-groups', 'system-classes',
3236
- 'pm-schedules', 'pm-templates', 'projects',
3237
- 'contracts', 'invoices', 'purchase-orders', 'expenses', 'budgets',
3238
- 'asset-types', 'asset-type-groups', 'asset-statuses', 'work-categories', 'manufacturers',
3239
- 'building-types', 'location-types', 'cost-categories',
3240
- 'compliance', 'compliance-records',
3241
- 'asset-comments', 'asset-costs', 'asset-replacement-plans',
3242
- 'work-order-comments', 'project-tasks', 'project-milestones',
3243
- 'project-phases', 'project-budget-items', 'project-time-entries',
3244
- 'project-comments', 'project-team-members', 'project-task-dependencies',
3245
- 'project-updates', 'project-cost-snapshots', 'project-locations',
3246
- 'project-sites', 'project-buildings', 'project-systems',
3247
- 'project-system-classes', 'project-system-groups', 'project-assets', 'project-risks',
3248
- 'parts', 'part-categories',
3249
- 'custom-field-definitions', 'custom-field-values',
3250
- 'vendor-site-assignments', 'contract-sites',
3251
- 'asset-documents', 'attachments', 'project-documents', 'contract-documents',
3252
- 'service-areas', 'los-measures', 'los-measurements',
3253
- 'floorplans', 'floorplan-regions', 'asset-placements',
4048
+ 'assets',
4049
+ 'work-orders',
4050
+ 'work-requests',
4051
+ 'vendors',
4052
+ 'sites',
4053
+ 'buildings',
4054
+ 'locations',
4055
+ 'systems',
4056
+ 'system-groups',
4057
+ 'system-classes',
4058
+ 'pm-schedules',
4059
+ 'pm-templates',
4060
+ 'projects',
4061
+ 'contracts',
4062
+ 'invoices',
4063
+ 'purchase-orders',
4064
+ 'expenses',
4065
+ 'budgets',
4066
+ 'asset-types',
4067
+ 'asset-type-groups',
4068
+ 'asset-statuses',
4069
+ 'work-categories',
4070
+ 'manufacturers',
4071
+ 'building-types',
4072
+ 'location-types',
4073
+ 'cost-categories',
4074
+ 'compliance',
4075
+ 'compliance-records',
4076
+ 'asset-comments',
4077
+ 'asset-costs',
4078
+ 'asset-replacement-plans',
4079
+ 'work-order-comments',
4080
+ 'project-tasks',
4081
+ 'project-milestones',
4082
+ 'project-phases',
4083
+ 'project-budget-items',
4084
+ 'project-time-entries',
4085
+ 'project-comments',
4086
+ 'project-team-members',
4087
+ 'project-task-dependencies',
4088
+ 'project-updates',
4089
+ 'project-cost-snapshots',
4090
+ 'project-locations',
4091
+ 'project-sites',
4092
+ 'project-buildings',
4093
+ 'project-systems',
4094
+ 'project-system-classes',
4095
+ 'project-system-groups',
4096
+ 'project-assets',
4097
+ 'project-risks',
4098
+ 'parts',
4099
+ 'part-categories',
4100
+ 'custom-field-definitions',
4101
+ 'custom-field-values',
4102
+ 'vendor-site-assignments',
4103
+ 'contract-sites',
4104
+ 'asset-documents',
4105
+ 'attachments',
4106
+ 'project-documents',
4107
+ 'contract-documents',
4108
+ 'service-areas',
4109
+ 'los-measures',
4110
+ 'los-measurements',
4111
+ 'floorplans',
4112
+ 'floorplan-regions',
4113
+ 'asset-placements',
3254
4114
  ];
3255
4115
  server.tool('bulk_create', 'Create multiple records of a resource type in one API call (max 100). Each item is processed independently — one failure does not affect others. Returns per-item results. Requires {resource}:write scope. Counts as 1 request for rate limiting.', {
3256
4116
  resource: z.enum(BULK_RESOURCES).describe('Resource type (e.g. "assets", "work-orders")'),
3257
- items: z.array(z.record(z.unknown())).min(1).max(100).describe('Array of objects to create (max 100). Each object uses the same fields as the single-create endpoint for that resource.'),
4117
+ items: z
4118
+ .array(z.record(z.unknown()))
4119
+ .min(1)
4120
+ .max(100)
4121
+ .describe('Array of objects to create (max 100). Each object uses the same fields as the single-create endpoint for that resource.'),
3258
4122
  }, async ({ resource, items }) => {
3259
4123
  try {
3260
4124
  const result = await client.bulkCreate(resource, items);
@@ -3308,7 +4172,12 @@ export function registerWriteTools(server, client) {
3308
4172
  name: z.string().max(500).describe('Compliance item name (required)'),
3309
4173
  description: z.string().optional().describe('Description'),
3310
4174
  regulation_reference: z.string().max(500).optional().describe('Regulation or code reference'),
3311
- compliance_period_months: z.number().int().min(1).optional().describe('Compliance period in months'),
4175
+ compliance_period_months: z
4176
+ .number()
4177
+ .int()
4178
+ .min(1)
4179
+ .optional()
4180
+ .describe('Compliance period in months'),
3312
4181
  status: z.enum(['active', 'archived']).optional().describe('Status'),
3313
4182
  system_id: z.string().uuid().optional().describe('Associated system ID'),
3314
4183
  }, async (params) => {
@@ -3325,7 +4194,12 @@ export function registerWriteTools(server, client) {
3325
4194
  name: z.string().max(500).optional().describe('Compliance item name'),
3326
4195
  description: z.string().optional().describe('Description'),
3327
4196
  regulation_reference: z.string().max(500).optional().describe('Regulation or code reference'),
3328
- compliance_period_months: z.number().int().min(1).optional().describe('Compliance period in months'),
4197
+ compliance_period_months: z
4198
+ .number()
4199
+ .int()
4200
+ .min(1)
4201
+ .optional()
4202
+ .describe('Compliance period in months'),
3329
4203
  status: z.enum(['active', 'archived']).optional().describe('Status'),
3330
4204
  system_id: z.string().uuid().optional().describe('Associated system ID'),
3331
4205
  }, async ({ id, ...rest }) => {
@@ -3355,8 +4229,17 @@ export function registerWriteTools(server, client) {
3355
4229
  work_order_id: z.string().uuid().describe('Work order ID (required)'),
3356
4230
  completed_at: z.string().describe('Completion date-time (ISO 8601, required)'),
3357
4231
  completed_by: z.string().max(200).optional().describe('User ID who completed'),
3358
- required_frequency_days: z.number().int().min(1).describe('Required frequency in days (required)'),
3359
- days_since_last_completion: z.number().int().min(0).optional().describe('Days since last completion'),
4232
+ required_frequency_days: z
4233
+ .number()
4234
+ .int()
4235
+ .min(1)
4236
+ .describe('Required frequency in days (required)'),
4237
+ days_since_last_completion: z
4238
+ .number()
4239
+ .int()
4240
+ .min(0)
4241
+ .optional()
4242
+ .describe('Days since last completion'),
3360
4243
  }, async (params) => {
3361
4244
  try {
3362
4245
  const result = await client.create('compliance-records', buildBody(params));
@@ -3370,8 +4253,18 @@ export function registerWriteTools(server, client) {
3370
4253
  id: z.string().uuid().describe('Compliance record ID'),
3371
4254
  completed_at: z.string().optional().describe('Completion date-time (ISO 8601)'),
3372
4255
  completed_by: z.string().max(200).optional().describe('User ID who completed'),
3373
- required_frequency_days: z.number().int().min(1).optional().describe('Required frequency in days'),
3374
- days_since_last_completion: z.number().int().min(0).optional().describe('Days since last completion'),
4256
+ required_frequency_days: z
4257
+ .number()
4258
+ .int()
4259
+ .min(1)
4260
+ .optional()
4261
+ .describe('Required frequency in days'),
4262
+ days_since_last_completion: z
4263
+ .number()
4264
+ .int()
4265
+ .min(0)
4266
+ .optional()
4267
+ .describe('Days since last completion'),
3375
4268
  }, async ({ id, ...rest }) => {
3376
4269
  try {
3377
4270
  const result = await client.update('compliance-records', id, buildBody(rest));
@@ -3395,7 +4288,11 @@ export function registerWriteTools(server, client) {
3395
4288
  // ============================================================
3396
4289
  server.tool('bulk_update', 'Update multiple records of a resource type in one API call (max 100). Each item must include an "id" field (UUID). Each item is processed independently — one failure does not affect others. Returns per-item results. Requires {resource}:write scope. Counts as 1 request for rate limiting.', {
3397
4290
  resource: z.enum(BULK_RESOURCES).describe('Resource type (e.g. "assets", "work-orders")'),
3398
- items: z.array(z.record(z.unknown())).min(1).max(100).describe('Array of objects to update (max 100). Each must include an "id" field (UUID) plus fields to change.'),
4291
+ items: z
4292
+ .array(z.record(z.unknown()))
4293
+ .min(1)
4294
+ .max(100)
4295
+ .describe('Array of objects to update (max 100). Each must include an "id" field (UUID) plus fields to change.'),
3399
4296
  }, async ({ resource, items }) => {
3400
4297
  try {
3401
4298
  const result = await client.bulkUpdate(resource, items);