@assetlab/mcp-server 1.19.6 → 1.20.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));
@@ -1239,7 +1670,9 @@ export function registerWriteTools(server, client) {
1239
1670
  // 18. Asset Costs (scope: asset_costs)
1240
1671
  // ============================================================
1241
1672
  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)'),
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'),
@@ -1258,7 +1691,10 @@ export function registerWriteTools(server, client) {
1258
1691
  });
1259
1692
  server.tool('update_asset_cost', 'Update an existing asset cost entry by ID. Requires asset_costs:write scope.', {
1260
1693
  id: z.string().uuid().describe('Asset cost ID'),
1261
- category: z.enum(['Repair', 'PM', 'Operation', 'Replacement', 'Decommission', 'Other']).optional().describe('Cost category'),
1694
+ category: z
1695
+ .enum(['Repair', 'PM', 'Operation', 'Replacement', 'Decommission', 'Other'])
1696
+ .optional()
1697
+ .describe('Cost category'),
1262
1698
  amount: z.number().min(0).optional().describe('Cost amount'),
1263
1699
  cost_date: z.string().optional().describe('Cost date (ISO 8601)'),
1264
1700
  asset_id: z.string().uuid().optional().describe('Asset ID'),
@@ -1289,10 +1725,18 @@ export function registerWriteTools(server, client) {
1289
1725
  // ============================================================
1290
1726
  server.tool('create_asset_replacement_plan', 'Create a new asset replacement plan. Requires asset_replacement_plans:write scope.', {
1291
1727
  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)'),
1728
+ planned_replacement_year: z
1729
+ .number()
1730
+ .int()
1731
+ .min(2000)
1732
+ .max(2100)
1733
+ .describe('Planned replacement year (required)'),
1293
1734
  estimated_cost: z.number().min(0).optional().describe('Estimated replacement cost'),
1294
1735
  priority: z.enum(['CRITICAL', 'HIGH', 'MEDIUM', 'LOW']).optional().describe('Priority'),
1295
- status: z.enum(['PLANNED', 'BUDGETED', 'APPROVED', 'COMPLETED', 'CANCELLED']).optional().describe('Status'),
1736
+ status: z
1737
+ .enum(['PLANNED', 'BUDGETED', 'APPROVED', 'COMPLETED', 'CANCELLED'])
1738
+ .optional()
1739
+ .describe('Status'),
1296
1740
  notes: z.string().optional().describe('Notes'),
1297
1741
  funding_source: z.string().max(200).optional().describe('Funding source'),
1298
1742
  }, async (params) => {
@@ -1307,10 +1751,19 @@ export function registerWriteTools(server, client) {
1307
1751
  server.tool('update_asset_replacement_plan', 'Update an existing asset replacement plan by ID. Requires asset_replacement_plans:write scope.', {
1308
1752
  id: z.string().uuid().describe('Asset replacement plan ID'),
1309
1753
  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'),
1754
+ planned_replacement_year: z
1755
+ .number()
1756
+ .int()
1757
+ .min(2000)
1758
+ .max(2100)
1759
+ .optional()
1760
+ .describe('Planned replacement year'),
1311
1761
  estimated_cost: z.number().min(0).optional().describe('Estimated replacement cost'),
1312
1762
  priority: z.enum(['CRITICAL', 'HIGH', 'MEDIUM', 'LOW']).optional().describe('Priority'),
1313
- status: z.enum(['PLANNED', 'BUDGETED', 'APPROVED', 'COMPLETED', 'CANCELLED']).optional().describe('Status'),
1763
+ status: z
1764
+ .enum(['PLANNED', 'BUDGETED', 'APPROVED', 'COMPLETED', 'CANCELLED'])
1765
+ .optional()
1766
+ .describe('Status'),
1314
1767
  notes: z.string().optional().describe('Notes'),
1315
1768
  funding_source: z.string().max(200).optional().describe('Funding source'),
1316
1769
  }, async ({ id, ...rest }) => {
@@ -1339,7 +1792,10 @@ export function registerWriteTools(server, client) {
1339
1792
  title: z.string().min(1).max(500).describe('Task title (required)'),
1340
1793
  description: z.string().optional().describe('Description'),
1341
1794
  phase_id: z.string().uuid().optional().describe('Phase ID'),
1342
- status: z.enum(['todo', 'in_progress', 'completed', 'blocked', 'cancelled']).optional().describe('Task status'),
1795
+ status: z
1796
+ .enum(['todo', 'in_progress', 'completed', 'blocked', 'cancelled'])
1797
+ .optional()
1798
+ .describe('Task status'),
1343
1799
  priority: z.enum(['low', 'medium', 'high', 'critical']).optional().describe('Priority'),
1344
1800
  assigned_to: z.string().max(200).optional().describe('Assigned user'),
1345
1801
  start_date: z.string().optional().describe('Start date (ISO 8601)'),
@@ -1361,7 +1817,10 @@ export function registerWriteTools(server, client) {
1361
1817
  title: z.string().min(1).max(500).optional().describe('Task title'),
1362
1818
  description: z.string().optional().describe('Description'),
1363
1819
  phase_id: z.string().uuid().optional().describe('Phase ID'),
1364
- status: z.enum(['todo', 'in_progress', 'completed', 'blocked', 'cancelled']).optional().describe('Task status'),
1820
+ status: z
1821
+ .enum(['todo', 'in_progress', 'completed', 'blocked', 'cancelled'])
1822
+ .optional()
1823
+ .describe('Task status'),
1365
1824
  priority: z.enum(['low', 'medium', 'high', 'critical']).optional().describe('Priority'),
1366
1825
  assigned_to: z.string().max(200).optional().describe('Assigned user'),
1367
1826
  start_date: z.string().optional().describe('Start date (ISO 8601)'),
@@ -1394,7 +1853,10 @@ export function registerWriteTools(server, client) {
1394
1853
  name: z.string().min(1).max(500).describe('Milestone name (required)'),
1395
1854
  due_date: z.string().describe('Due date (ISO 8601, required)'),
1396
1855
  description: z.string().optional().describe('Description'),
1397
- status: z.enum(['pending', 'completed', 'missed', 'at_risk']).optional().describe('Milestone status'),
1856
+ status: z
1857
+ .enum(['pending', 'completed', 'missed', 'at_risk'])
1858
+ .optional()
1859
+ .describe('Milestone status'),
1398
1860
  completed_date: z.string().optional().describe('Completed date (ISO 8601)'),
1399
1861
  }, async (params) => {
1400
1862
  try {
@@ -1411,7 +1873,10 @@ export function registerWriteTools(server, client) {
1411
1873
  name: z.string().min(1).max(500).optional().describe('Milestone name'),
1412
1874
  due_date: z.string().optional().describe('Due date (ISO 8601)'),
1413
1875
  description: z.string().optional().describe('Description'),
1414
- status: z.enum(['pending', 'completed', 'missed', 'at_risk']).optional().describe('Milestone status'),
1876
+ status: z
1877
+ .enum(['pending', 'completed', 'missed', 'at_risk'])
1878
+ .optional()
1879
+ .describe('Milestone status'),
1415
1880
  completed_date: z.string().optional().describe('Completed date (ISO 8601)'),
1416
1881
  }, async ({ id, ...rest }) => {
1417
1882
  try {
@@ -1438,7 +1903,10 @@ export function registerWriteTools(server, client) {
1438
1903
  project_id: z.string().uuid().describe('Project ID (required)'),
1439
1904
  name: z.string().min(1).max(500).describe('Phase name (required)'),
1440
1905
  description: z.string().optional().describe('Description'),
1441
- status: z.enum(['pending', 'in_progress', 'completed', 'skipped']).optional().describe('Phase status'),
1906
+ status: z
1907
+ .enum(['pending', 'in_progress', 'completed', 'skipped'])
1908
+ .optional()
1909
+ .describe('Phase status'),
1442
1910
  start_date: z.string().optional().describe('Start date (ISO 8601)'),
1443
1911
  end_date: z.string().optional().describe('End date (ISO 8601)'),
1444
1912
  sort_order: z.number().int().min(0).optional().describe('Sort order'),
@@ -1456,7 +1924,10 @@ export function registerWriteTools(server, client) {
1456
1924
  project_id: z.string().uuid().optional().describe('Project ID'),
1457
1925
  name: z.string().min(1).max(500).optional().describe('Phase name'),
1458
1926
  description: z.string().optional().describe('Description'),
1459
- status: z.enum(['pending', 'in_progress', 'completed', 'skipped']).optional().describe('Phase status'),
1927
+ status: z
1928
+ .enum(['pending', 'in_progress', 'completed', 'skipped'])
1929
+ .optional()
1930
+ .describe('Phase status'),
1460
1931
  start_date: z.string().optional().describe('Start date (ISO 8601)'),
1461
1932
  end_date: z.string().optional().describe('End date (ISO 8601)'),
1462
1933
  sort_order: z.number().int().min(0).optional().describe('Sort order'),
@@ -1483,7 +1954,17 @@ export function registerWriteTools(server, client) {
1483
1954
  // ============================================================
1484
1955
  server.tool('create_project_budget_item', 'Create a new project budget item. Requires project_budget_items:write scope.', {
1485
1956
  project_id: z.string().uuid().describe('Project ID (required)'),
1486
- category: z.enum(['labor', 'materials', 'equipment', 'subcontractors', 'permits', 'contingency', 'other']).describe('Budget category (required)'),
1957
+ category: z
1958
+ .enum([
1959
+ 'labor',
1960
+ 'materials',
1961
+ 'equipment',
1962
+ 'subcontractors',
1963
+ 'permits',
1964
+ 'contingency',
1965
+ 'other',
1966
+ ])
1967
+ .describe('Budget category (required)'),
1487
1968
  description: z.string().optional().describe('Description'),
1488
1969
  estimated_amount: z.number().min(0).optional().describe('Estimated amount'),
1489
1970
  actual_amount: z.number().min(0).optional().describe('Actual amount'),
@@ -1499,7 +1980,18 @@ export function registerWriteTools(server, client) {
1499
1980
  server.tool('update_project_budget_item', 'Update an existing project budget item by ID. Requires project_budget_items:write scope.', {
1500
1981
  id: z.string().uuid().describe('Project budget item ID'),
1501
1982
  project_id: z.string().uuid().optional().describe('Project ID'),
1502
- category: z.enum(['labor', 'materials', 'equipment', 'subcontractors', 'permits', 'contingency', 'other']).optional().describe('Budget category'),
1983
+ category: z
1984
+ .enum([
1985
+ 'labor',
1986
+ 'materials',
1987
+ 'equipment',
1988
+ 'subcontractors',
1989
+ 'permits',
1990
+ 'contingency',
1991
+ 'other',
1992
+ ])
1993
+ .optional()
1994
+ .describe('Budget category'),
1503
1995
  description: z.string().optional().describe('Description'),
1504
1996
  estimated_amount: z.number().min(0).optional().describe('Estimated amount'),
1505
1997
  actual_amount: z.number().min(0).optional().describe('Actual amount'),
@@ -1810,7 +2302,13 @@ export function registerWriteTools(server, client) {
1810
2302
  server.tool('create_project_phase_category', 'Create a new project phase category. Requires project_phase_categories:write scope.', {
1811
2303
  name: z.string().min(1).max(500).describe('Phase name (required), e.g. "Planning", "Design"'),
1812
2304
  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)'),
2305
+ sort_order: z
2306
+ .number()
2307
+ .int()
2308
+ .min(0)
2309
+ .max(10000)
2310
+ .optional()
2311
+ .describe('Display order (lower = first)'),
1814
2312
  }, async (params) => {
1815
2313
  try {
1816
2314
  const result = await client.create('project-phase-categories', buildBody(params));
@@ -1824,7 +2322,13 @@ export function registerWriteTools(server, client) {
1824
2322
  id: z.string().uuid().describe('Project phase category ID'),
1825
2323
  name: z.string().min(1).max(500).optional().describe('Phase name'),
1826
2324
  description: z.string().max(2000).optional().describe('Description'),
1827
- sort_order: z.number().int().min(0).max(10000).optional().describe('Display order (lower = first)'),
2325
+ sort_order: z
2326
+ .number()
2327
+ .int()
2328
+ .min(0)
2329
+ .max(10000)
2330
+ .optional()
2331
+ .describe('Display order (lower = first)'),
1828
2332
  }, async ({ id, ...rest }) => {
1829
2333
  try {
1830
2334
  const result = await client.update('project-phase-categories', id, buildBody(rest));
@@ -2129,9 +2633,15 @@ export function registerWriteTools(server, client) {
2129
2633
  // 35. Custom Field Definitions (scope: custom_fields)
2130
2634
  // ============================================================
2131
2635
  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)'),
2636
+ entity_type: z
2637
+ .string()
2638
+ .min(1)
2639
+ .max(100)
2640
+ .describe('Entity type this field applies to (required)'),
2133
2641
  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)'),
2642
+ field_type: z
2643
+ .enum(['text', 'number', 'date', 'boolean', 'select'])
2644
+ .describe('Field data type (required)'),
2135
2645
  field_label: z.string().max(200).optional().describe('Display label'),
2136
2646
  options: z.array(z.string()).optional().describe('Options for select-type fields'),
2137
2647
  is_required: z.boolean().optional().describe('Whether the field is required'),
@@ -2149,7 +2659,10 @@ export function registerWriteTools(server, client) {
2149
2659
  id: z.string().uuid().describe('Custom field definition ID'),
2150
2660
  entity_type: z.string().min(1).max(100).optional().describe('Entity type'),
2151
2661
  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'),
2662
+ field_type: z
2663
+ .enum(['text', 'number', 'date', 'boolean', 'select'])
2664
+ .optional()
2665
+ .describe('Field data type'),
2153
2666
  field_label: z.string().max(200).optional().describe('Display label'),
2154
2667
  options: z.array(z.string()).optional().describe('Options for select-type fields'),
2155
2668
  is_required: z.boolean().optional().describe('Whether the field is required'),
@@ -2175,14 +2688,38 @@ export function registerWriteTools(server, client) {
2175
2688
  // ============================================================
2176
2689
  // 36. Custom Field Values (scope: custom_fields)
2177
2690
  // ============================================================
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.', {
2691
+ 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
2692
  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.'),
2693
+ field_definition_id: z
2694
+ .string()
2695
+ .uuid()
2696
+ .describe('Custom field definition ID resolve via list_custom_field_definitions (required)'),
2697
+ value_text: z
2698
+ .string()
2699
+ .max(5000)
2700
+ .nullable()
2701
+ .optional()
2702
+ .describe('Text value (use for field_type=text or select)'),
2703
+ value_number: z
2704
+ .number()
2705
+ .nullable()
2706
+ .optional()
2707
+ .describe('Numeric value (use for field_type=number)'),
2708
+ value_date: z
2709
+ .string()
2710
+ .nullable()
2711
+ .optional()
2712
+ .describe('Date value, ISO YYYY-MM-DD (use for field_type=date)'),
2713
+ value_boolean: z
2714
+ .boolean()
2715
+ .nullable()
2716
+ .optional()
2717
+ .describe('Boolean value (use for field_type=boolean)'),
2718
+ value: z
2719
+ .string()
2720
+ .max(5000)
2721
+ .optional()
2722
+ .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
2723
  }, async (params) => {
2187
2724
  try {
2188
2725
  const result = await client.create('custom-field-values', buildBody(params));
@@ -2192,15 +2729,23 @@ export function registerWriteTools(server, client) {
2192
2729
  return formatError(err);
2193
2730
  }
2194
2731
  });
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.', {
2732
+ 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
2733
  id: z.string().uuid().describe('Custom field value ID'),
2197
2734
  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'),
2735
+ field_definition_id: z
2736
+ .string()
2737
+ .uuid()
2738
+ .optional()
2739
+ .describe('Custom field definition ID — required when using the `value` fallback if you want to avoid the server fetching it'),
2199
2740
  value_text: z.string().max(5000).nullable().optional().describe('Text value'),
2200
2741
  value_number: z.number().nullable().optional().describe('Numeric value'),
2201
2742
  value_date: z.string().nullable().optional().describe('Date value, ISO YYYY-MM-DD'),
2202
2743
  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'),
2744
+ value: z
2745
+ .string()
2746
+ .max(5000)
2747
+ .optional()
2748
+ .describe('Legacy single-value shim — server dispatches based on field_type'),
2204
2749
  }, async ({ id, ...rest }) => {
2205
2750
  try {
2206
2751
  const result = await client.update('custom-field-values', id, buildBody(rest));
@@ -2226,10 +2771,27 @@ export function registerWriteTools(server, client) {
2226
2771
  name: z.string().min(1).max(500).describe('Part name (required)'),
2227
2772
  part_number: z.string().max(200).optional().describe('Part number / SKU'),
2228
2773
  category: z.string().max(200).optional().describe('Category label'),
2229
- supplier: z.string().max(500).optional().describe('Supplier name'),
2774
+ supplier: z
2775
+ .string()
2776
+ .max(500)
2777
+ .optional()
2778
+ .describe('DEPRECATED — legacy free-text supplier name. Use supplier_id instead.'),
2779
+ supplier_id: z.string().uuid().optional().describe('Vendor ID (resolve via list_vendors)'),
2230
2780
  cost: z.number().min(0).optional().describe('Unit cost'),
2231
- quantity: z.number().int().min(0).max(10_000_000).optional().describe('Current stock quantity'),
2232
- desired_quantity: z.number().int().min(0).max(10_000_000).optional().describe('Target / reorder quantity'),
2781
+ quantity: z
2782
+ .number()
2783
+ .int()
2784
+ .min(0)
2785
+ .max(10_000_000)
2786
+ .optional()
2787
+ .describe('Current stock quantity'),
2788
+ desired_quantity: z
2789
+ .number()
2790
+ .int()
2791
+ .min(0)
2792
+ .max(10_000_000)
2793
+ .optional()
2794
+ .describe('Target / reorder quantity'),
2233
2795
  specific_location: z.string().max(500).optional().describe('Storage location description'),
2234
2796
  site_id: z.string().uuid().optional().describe('Site ID'),
2235
2797
  building_id: z.string().uuid().optional().describe('Building ID'),
@@ -2248,10 +2810,27 @@ export function registerWriteTools(server, client) {
2248
2810
  name: z.string().min(1).max(500).optional().describe('Part name'),
2249
2811
  part_number: z.string().max(200).optional().describe('Part number / SKU'),
2250
2812
  category: z.string().max(200).optional().describe('Category label'),
2251
- supplier: z.string().max(500).optional().describe('Supplier name'),
2813
+ supplier: z
2814
+ .string()
2815
+ .max(500)
2816
+ .optional()
2817
+ .describe('DEPRECATED — legacy free-text supplier name. Use supplier_id instead.'),
2818
+ supplier_id: z.string().uuid().optional().describe('Vendor ID (resolve via list_vendors)'),
2252
2819
  cost: z.number().min(0).optional().describe('Unit cost'),
2253
- quantity: z.number().int().min(0).max(10_000_000).optional().describe('Current stock quantity'),
2254
- desired_quantity: z.number().int().min(0).max(10_000_000).optional().describe('Target / reorder quantity'),
2820
+ quantity: z
2821
+ .number()
2822
+ .int()
2823
+ .min(0)
2824
+ .max(10_000_000)
2825
+ .optional()
2826
+ .describe('Current stock quantity'),
2827
+ desired_quantity: z
2828
+ .number()
2829
+ .int()
2830
+ .min(0)
2831
+ .max(10_000_000)
2832
+ .optional()
2833
+ .describe('Target / reorder quantity'),
2255
2834
  specific_location: z.string().max(500).optional().describe('Storage location description'),
2256
2835
  site_id: z.string().uuid().optional().describe('Site ID'),
2257
2836
  building_id: z.string().uuid().optional().describe('Building ID'),
@@ -2278,7 +2857,15 @@ export function registerWriteTools(server, client) {
2278
2857
  // Upload URLs
2279
2858
  // ============================================================
2280
2859
  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.', {
2281
- bucket: z.enum(['documents', 'attachments', 'project-documents', 'contract-documents', 'asset-images']).describe('Storage bucket (required). Use "asset-images" for asset photos.'),
2860
+ bucket: z
2861
+ .enum([
2862
+ 'documents',
2863
+ 'attachments',
2864
+ 'project-documents',
2865
+ 'contract-documents',
2866
+ 'asset-images',
2867
+ ])
2868
+ .describe('Storage bucket (required). Use "asset-images" for asset photos.'),
2282
2869
  file_name: z.string().min(1).max(500).describe('File name including extension (required)'),
2283
2870
  }, async (params) => {
2284
2871
  try {
@@ -2290,10 +2877,25 @@ export function registerWriteTools(server, client) {
2290
2877
  }
2291
2878
  });
2292
2879
  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.', {
2293
- 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.'),
2880
+ bucket: z
2881
+ .enum([
2882
+ 'documents',
2883
+ 'attachments',
2884
+ 'project-documents',
2885
+ 'contract-documents',
2886
+ 'asset-images',
2887
+ ])
2888
+ .describe('Storage bucket (required). Use "asset-images" for asset photos, "attachments" for work-order/PM attachments.'),
2294
2889
  file_name: z.string().min(1).max(500).describe('File name including extension (required)'),
2295
- content_base64: z.string().min(1).describe('File contents base64-encoded (required). Data URI prefixes like "data:image/png;base64," are stripped automatically.'),
2296
- content_type: z.string().max(200).optional().describe('MIME type (e.g. image/jpeg, application/pdf). Defaults to application/octet-stream.'),
2890
+ content_base64: z
2891
+ .string()
2892
+ .min(1)
2893
+ .describe('File contents base64-encoded (required). Data URI prefixes like "data:image/png;base64," are stripped automatically.'),
2894
+ content_type: z
2895
+ .string()
2896
+ .max(200)
2897
+ .optional()
2898
+ .describe('MIME type (e.g. image/jpeg, application/pdf). Defaults to application/octet-stream.'),
2297
2899
  }, async (params) => {
2298
2900
  try {
2299
2901
  const result = await client.create('upload-files', buildBody(params));
@@ -2308,9 +2910,16 @@ export function registerWriteTools(server, client) {
2308
2910
  // ============================================================
2309
2911
  server.tool('create_asset_document', 'Create an asset document record (after uploading the file via create_upload_url). Requires asset_documents:write scope.', {
2310
2912
  name: z.string().min(1).max(500).describe('Document name (required)'),
2311
- file_path: z.string().min(1).max(2000).describe('Storage path from upload URL response (required)'),
2913
+ file_path: z
2914
+ .string()
2915
+ .min(1)
2916
+ .max(2000)
2917
+ .describe('Storage path from upload URL response (required)'),
2312
2918
  asset_id: z.string().uuid().describe('Asset ID this document belongs to (required)'),
2313
- category: z.enum(['om', 'commissioning', 'warranty', 'installation', 'specification', 'other']).optional().describe('Document category'),
2919
+ category: z
2920
+ .enum(['om', 'commissioning', 'warranty', 'installation', 'specification', 'other'])
2921
+ .optional()
2922
+ .describe('Document category'),
2314
2923
  description: z.string().optional().describe('Description'),
2315
2924
  file_type: z.string().max(200).optional().describe('MIME type'),
2316
2925
  file_size: z.number().min(0).optional().describe('File size in bytes'),
@@ -2329,7 +2938,10 @@ export function registerWriteTools(server, client) {
2329
2938
  name: z.string().min(1).max(500).optional().describe('Document name'),
2330
2939
  file_path: z.string().max(2000).optional().describe('Storage path'),
2331
2940
  asset_id: z.string().uuid().optional().describe('Asset ID'),
2332
- category: z.enum(['om', 'commissioning', 'warranty', 'installation', 'specification', 'other']).optional().describe('Document category'),
2941
+ category: z
2942
+ .enum(['om', 'commissioning', 'warranty', 'installation', 'specification', 'other'])
2943
+ .optional()
2944
+ .describe('Document category'),
2333
2945
  description: z.string().optional().describe('Description'),
2334
2946
  file_type: z.string().max(200).optional().describe('MIME type'),
2335
2947
  file_size: z.number().min(0).optional().describe('File size in bytes'),
@@ -2362,10 +2974,26 @@ export function registerWriteTools(server, client) {
2362
2974
  file_type: z.string().max(200).optional().describe('MIME type'),
2363
2975
  uploaded_by: z.string().max(200).optional().describe('Uploader user ID'),
2364
2976
  description: z.string().optional().describe('Description'),
2365
- work_order_id: z.string().uuid().optional().describe('Work order ID (exactly one parent required)'),
2366
- work_request_id: z.string().uuid().optional().describe('Work request ID (exactly one parent required)'),
2367
- pm_schedule_id: z.string().uuid().optional().describe('PM schedule ID (exactly one parent required)'),
2368
- pm_template_id: z.string().uuid().optional().describe('PM template ID (exactly one parent required)'),
2977
+ work_order_id: z
2978
+ .string()
2979
+ .uuid()
2980
+ .optional()
2981
+ .describe('Work order ID (exactly one parent required)'),
2982
+ work_request_id: z
2983
+ .string()
2984
+ .uuid()
2985
+ .optional()
2986
+ .describe('Work request ID (exactly one parent required)'),
2987
+ pm_schedule_id: z
2988
+ .string()
2989
+ .uuid()
2990
+ .optional()
2991
+ .describe('PM schedule ID (exactly one parent required)'),
2992
+ pm_template_id: z
2993
+ .string()
2994
+ .uuid()
2995
+ .optional()
2996
+ .describe('PM template ID (exactly one parent required)'),
2369
2997
  }, async (params) => {
2370
2998
  try {
2371
2999
  const result = await client.create('attachments', buildBody(params));
@@ -2411,7 +3039,11 @@ export function registerWriteTools(server, client) {
2411
3039
  server.tool('create_project_document', 'Create a project document record. Requires project_documents:write scope.', {
2412
3040
  project_id: z.string().uuid().describe('Project ID (required)'),
2413
3041
  name: z.string().min(1).max(500).describe('Document name (required)'),
2414
- file_path: z.string().min(1).max(2000).describe('Storage path from upload URL response (required)'),
3042
+ file_path: z
3043
+ .string()
3044
+ .min(1)
3045
+ .max(2000)
3046
+ .describe('Storage path from upload URL response (required)'),
2415
3047
  uploaded_by: z.string().min(1).max(200).describe('Uploader user ID (required)'),
2416
3048
  folder_id: z.string().uuid().optional().describe('Folder ID'),
2417
3049
  description: z.string().optional().describe('Description'),
@@ -2460,7 +3092,11 @@ export function registerWriteTools(server, client) {
2460
3092
  server.tool('create_contract_document', 'Create a contract document record. Requires contract_documents:write scope.', {
2461
3093
  contract_id: z.string().uuid().describe('Contract ID (required)'),
2462
3094
  file_name: z.string().min(1).max(500).describe('File name (required)'),
2463
- file_path: z.string().min(1).max(2000).describe('Storage path from upload URL response (required)'),
3095
+ file_path: z
3096
+ .string()
3097
+ .min(1)
3098
+ .max(2000)
3099
+ .describe('Storage path from upload URL response (required)'),
2464
3100
  file_size: z.number().min(0).optional().describe('File size in bytes'),
2465
3101
  file_type: z.string().max(200).optional().describe('MIME type'),
2466
3102
  uploaded_by: z.string().max(200).optional().describe('Uploader user ID'),
@@ -2552,7 +3188,10 @@ export function registerWriteTools(server, client) {
2552
3188
  server.tool('create_project_task_dependency', 'Create a dependency between two project tasks. Requires project_task_dependencies:write scope.', {
2553
3189
  task_id: z.string().uuid().describe('Task ID (the dependent task, required)'),
2554
3190
  depends_on_task_id: z.string().uuid().describe('Task ID that must complete first (required)'),
2555
- dependency_type: z.enum(['finish_to_start', 'start_to_start', 'finish_to_finish', 'start_to_finish']).optional().describe('Dependency type (default: finish_to_start)'),
3191
+ dependency_type: z
3192
+ .enum(['finish_to_start', 'start_to_start', 'finish_to_finish', 'start_to_finish'])
3193
+ .optional()
3194
+ .describe('Dependency type (default: finish_to_start)'),
2556
3195
  }, async (params) => {
2557
3196
  try {
2558
3197
  const result = await client.create('project-task-dependencies', buildBody(params));
@@ -2577,9 +3216,20 @@ export function registerWriteTools(server, client) {
2577
3216
  server.tool('create_project_update', 'Create a periodic project status update. Requires project_updates:write scope.', {
2578
3217
  project_id: z.string().uuid().describe('Project ID (required)'),
2579
3218
  author_id: z.string().min(1).max(200).describe('Author Clerk user ID (required)'),
2580
- timeframe: z.enum(['monthly', 'quarterly', 'bi-annually', 'annually']).describe('Update timeframe (required)'),
2581
- period_year: z.number().int().min(2000).max(2100).describe('Year for this update period (required)'),
2582
- period_value: z.string().min(1).max(20).describe('Period value — 1-12 for monthly, 1-4 for quarterly, etc. (required)'),
3219
+ timeframe: z
3220
+ .enum(['monthly', 'quarterly', 'bi-annually', 'annually'])
3221
+ .describe('Update timeframe (required)'),
3222
+ period_year: z
3223
+ .number()
3224
+ .int()
3225
+ .min(2000)
3226
+ .max(2100)
3227
+ .describe('Year for this update period (required)'),
3228
+ period_value: z
3229
+ .string()
3230
+ .min(1)
3231
+ .max(20)
3232
+ .describe('Period value — 1-12 for monthly, 1-4 for quarterly, etc. (required)'),
2583
3233
  content: z.string().min(1).describe('Update content (required)'),
2584
3234
  title: z.string().max(500).optional().describe('Optional custom title'),
2585
3235
  }, async (params) => {
@@ -2595,8 +3245,17 @@ export function registerWriteTools(server, client) {
2595
3245
  id: z.string().uuid().describe('Project update ID'),
2596
3246
  project_id: z.string().uuid().optional().describe('Project ID'),
2597
3247
  author_id: z.string().min(1).max(200).optional().describe('Author Clerk user ID'),
2598
- timeframe: z.enum(['monthly', 'quarterly', 'bi-annually', 'annually']).optional().describe('Update timeframe'),
2599
- period_year: z.number().int().min(2000).max(2100).optional().describe('Year for this update period'),
3248
+ timeframe: z
3249
+ .enum(['monthly', 'quarterly', 'bi-annually', 'annually'])
3250
+ .optional()
3251
+ .describe('Update timeframe'),
3252
+ period_year: z
3253
+ .number()
3254
+ .int()
3255
+ .min(2000)
3256
+ .max(2100)
3257
+ .optional()
3258
+ .describe('Year for this update period'),
2600
3259
  period_value: z.string().min(1).max(20).optional().describe('Period value'),
2601
3260
  content: z.string().min(1).optional().describe('Update content'),
2602
3261
  title: z.string().max(500).optional().describe('Optional custom title'),
@@ -2627,7 +3286,12 @@ export function registerWriteTools(server, client) {
2627
3286
  total_budget: z.number().min(0).describe('Total budget amount (required)'),
2628
3287
  actual_cost: z.number().min(0).describe('Actual cost to date (required)'),
2629
3288
  forecasted_cost: z.number().min(0).optional().describe('Forecasted total cost'),
2630
- percent_complete: z.number().min(0).max(100).optional().describe('Completion percentage (0-100)'),
3289
+ percent_complete: z
3290
+ .number()
3291
+ .min(0)
3292
+ .max(100)
3293
+ .optional()
3294
+ .describe('Completion percentage (0-100)'),
2631
3295
  }, async (params) => {
2632
3296
  try {
2633
3297
  const result = await client.create('project-cost-snapshots', buildBody(params));
@@ -2797,10 +3461,16 @@ export function registerWriteTools(server, client) {
2797
3461
  project_id: z.string().uuid().describe('Project ID (required)'),
2798
3462
  title: z.string().min(1).max(500).describe('Risk title (required)'),
2799
3463
  description: z.string().optional().describe('Risk description'),
2800
- category: z.enum(['technical', 'financial', 'schedule', 'resource', 'external']).optional().describe('Risk category'),
3464
+ category: z
3465
+ .enum(['technical', 'financial', 'schedule', 'resource', 'external'])
3466
+ .optional()
3467
+ .describe('Risk category'),
2801
3468
  probability: z.enum(['low', 'medium', 'high']).optional().describe('Probability level'),
2802
3469
  impact: z.enum(['low', 'medium', 'high', 'critical']).optional().describe('Impact level'),
2803
- status: z.enum(['identified', 'analyzing', 'mitigating', 'resolved', 'accepted']).optional().describe('Risk status (default: identified)'),
3470
+ status: z
3471
+ .enum(['identified', 'analyzing', 'mitigating', 'resolved', 'accepted'])
3472
+ .optional()
3473
+ .describe('Risk status (default: identified)'),
2804
3474
  mitigation_plan: z.string().optional().describe('Mitigation plan'),
2805
3475
  contingency_plan: z.string().optional().describe('Contingency plan'),
2806
3476
  owner_id: z.string().max(200).optional().describe('Risk owner (Clerk user ID)'),
@@ -2820,10 +3490,16 @@ export function registerWriteTools(server, client) {
2820
3490
  project_id: z.string().uuid().optional().describe('Project ID'),
2821
3491
  title: z.string().min(1).max(500).optional().describe('Risk title'),
2822
3492
  description: z.string().optional().describe('Risk description'),
2823
- category: z.enum(['technical', 'financial', 'schedule', 'resource', 'external']).optional().describe('Risk category'),
3493
+ category: z
3494
+ .enum(['technical', 'financial', 'schedule', 'resource', 'external'])
3495
+ .optional()
3496
+ .describe('Risk category'),
2824
3497
  probability: z.enum(['low', 'medium', 'high']).optional().describe('Probability level'),
2825
3498
  impact: z.enum(['low', 'medium', 'high', 'critical']).optional().describe('Impact level'),
2826
- status: z.enum(['identified', 'analyzing', 'mitigating', 'resolved', 'accepted']).optional().describe('Risk status'),
3499
+ status: z
3500
+ .enum(['identified', 'analyzing', 'mitigating', 'resolved', 'accepted'])
3501
+ .optional()
3502
+ .describe('Risk status'),
2827
3503
  mitigation_plan: z.string().optional().describe('Mitigation plan'),
2828
3504
  contingency_plan: z.string().optional().describe('Contingency plan'),
2829
3505
  owner_id: z.string().max(200).optional().describe('Risk owner (Clerk user ID)'),
@@ -2879,7 +3555,10 @@ export function registerWriteTools(server, client) {
2879
3555
  icon: z.string().max(200).optional().describe('Icon name (e.g., "droplets" for water)'),
2880
3556
  color: z.string().max(50).optional().describe('Hex color code (e.g., "#3B82F6")'),
2881
3557
  sort_order: z.number().int().min(0).optional().describe('Sort order for display'),
2882
- is_active: z.boolean().optional().describe('Whether the service area is active (default: true)'),
3558
+ is_active: z
3559
+ .boolean()
3560
+ .optional()
3561
+ .describe('Whether the service area is active (default: true)'),
2883
3562
  }, async (params) => {
2884
3563
  try {
2885
3564
  const result = await client.create('service-areas', buildBody(params));
@@ -2969,24 +3648,65 @@ export function registerWriteTools(server, client) {
2969
3648
  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.', {
2970
3649
  service_area_id: z.string().uuid().describe('Service area ID (required)'),
2971
3650
  name: z.string().min(1).max(500).describe('Measure name (required, unique per service area)'),
2972
- category: z.enum(['quality', 'reliability', 'responsiveness', 'safety', 'sustainability', 'cost_efficiency', 'capacity']).describe('Measure category (required)'),
3651
+ category: z
3652
+ .enum([
3653
+ 'quality',
3654
+ 'reliability',
3655
+ 'responsiveness',
3656
+ 'safety',
3657
+ 'sustainability',
3658
+ 'cost_efficiency',
3659
+ 'capacity',
3660
+ ])
3661
+ .describe('Measure category (required)'),
2973
3662
  type: z.enum(['community', 'technical']).describe('Measure type (required)'),
2974
- data_source: z.enum([
2975
- 'manual', 'custom_formula', 'asset_condition_avg', 'asset_condition_pct_above',
2976
- 'asset_condition_pct_below', 'risk_score_avg', 'risk_pct_critical',
2977
- 'wo_response_time_avg', 'wo_completion_time_avg', 'wo_backlog_count', 'wo_overdue_count',
2978
- 'pm_compliance_rate', 'compliance_score', 'fci', 'deferred_maintenance_ratio',
3663
+ data_source: z
3664
+ .enum([
3665
+ 'manual',
3666
+ 'custom_formula',
3667
+ 'asset_condition_avg',
3668
+ 'asset_condition_pct_above',
3669
+ 'asset_condition_pct_below',
3670
+ 'risk_score_avg',
3671
+ 'risk_pct_critical',
3672
+ 'wo_response_time_avg',
3673
+ 'wo_completion_time_avg',
3674
+ 'wo_backlog_count',
3675
+ 'wo_overdue_count',
3676
+ 'pm_compliance_rate',
3677
+ 'compliance_score',
3678
+ 'fci',
3679
+ 'deferred_maintenance_ratio',
2979
3680
  'asset_past_useful_life_pct',
2980
- ]).describe('Data source type (required). Use "manual" if values will be entered by hand.'),
3681
+ ])
3682
+ .describe('Data source type (required). Use "manual" if values will be entered by hand.'),
2981
3683
  description: z.string().max(2000).optional().describe('Description'),
2982
- community_statement: z.string().max(2000).optional().describe('Community-facing statement (for community type measures)'),
2983
- unit: z.string().max(100).optional().describe('Unit of measurement (e.g., "%", "hours", "count")'),
2984
- trend_direction: z.enum(['higher_is_better', 'lower_is_better', 'target_is_optimal']).optional().describe('Which direction is better'),
3684
+ community_statement: z
3685
+ .string()
3686
+ .max(2000)
3687
+ .optional()
3688
+ .describe('Community-facing statement (for community type measures)'),
3689
+ unit: z
3690
+ .string()
3691
+ .max(100)
3692
+ .optional()
3693
+ .describe('Unit of measurement (e.g., "%", "hours", "count")'),
3694
+ trend_direction: z
3695
+ .enum(['higher_is_better', 'lower_is_better', 'target_is_optimal'])
3696
+ .optional()
3697
+ .describe('Which direction is better'),
2985
3698
  target_value: z.number().optional().describe('Target value'),
2986
3699
  minimum_acceptable: z.number().optional().describe('Minimum acceptable value'),
2987
3700
  stretch_goal: z.number().optional().describe('Stretch goal value'),
2988
- weight: z.number().min(0).optional().describe('Weight for composite score calculation (default: 1.0)'),
2989
- 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.'),
3701
+ weight: z
3702
+ .number()
3703
+ .min(0)
3704
+ .optional()
3705
+ .describe('Weight for composite score calculation (default: 1.0)'),
3706
+ data_source_config: z
3707
+ .record(z.unknown())
3708
+ .optional()
3709
+ .describe('Data source configuration (JSONB). E.g., {"threshold": 3} for pct_above/below, {"days_back": 90} for WO metrics.'),
2990
3710
  is_active: z.boolean().optional().describe('Whether the measure is active (default: true)'),
2991
3711
  sort_order: z.number().int().min(0).optional().describe('Sort order for display'),
2992
3712
  }, async (params) => {
@@ -3001,24 +3721,55 @@ export function registerWriteTools(server, client) {
3001
3721
  server.tool('update_los_measure', 'Update an existing LoS measure by ID. Requires los_measures:write scope.', {
3002
3722
  id: z.string().uuid().describe('LoS measure ID'),
3003
3723
  name: z.string().min(1).max(500).optional().describe('Measure name'),
3004
- category: z.enum(['quality', 'reliability', 'responsiveness', 'safety', 'sustainability', 'cost_efficiency', 'capacity']).optional().describe('Measure category'),
3724
+ category: z
3725
+ .enum([
3726
+ 'quality',
3727
+ 'reliability',
3728
+ 'responsiveness',
3729
+ 'safety',
3730
+ 'sustainability',
3731
+ 'cost_efficiency',
3732
+ 'capacity',
3733
+ ])
3734
+ .optional()
3735
+ .describe('Measure category'),
3005
3736
  type: z.enum(['community', 'technical']).optional().describe('Measure type'),
3006
- data_source: z.enum([
3007
- 'manual', 'custom_formula', 'asset_condition_avg', 'asset_condition_pct_above',
3008
- 'asset_condition_pct_below', 'risk_score_avg', 'risk_pct_critical',
3009
- 'wo_response_time_avg', 'wo_completion_time_avg', 'wo_backlog_count', 'wo_overdue_count',
3010
- 'pm_compliance_rate', 'compliance_score', 'fci', 'deferred_maintenance_ratio',
3737
+ data_source: z
3738
+ .enum([
3739
+ 'manual',
3740
+ 'custom_formula',
3741
+ 'asset_condition_avg',
3742
+ 'asset_condition_pct_above',
3743
+ 'asset_condition_pct_below',
3744
+ 'risk_score_avg',
3745
+ 'risk_pct_critical',
3746
+ 'wo_response_time_avg',
3747
+ 'wo_completion_time_avg',
3748
+ 'wo_backlog_count',
3749
+ 'wo_overdue_count',
3750
+ 'pm_compliance_rate',
3751
+ 'compliance_score',
3752
+ 'fci',
3753
+ 'deferred_maintenance_ratio',
3011
3754
  'asset_past_useful_life_pct',
3012
- ]).optional().describe('Data source type'),
3755
+ ])
3756
+ .optional()
3757
+ .describe('Data source type'),
3013
3758
  description: z.string().max(2000).optional().describe('Description'),
3014
3759
  community_statement: z.string().max(2000).optional().describe('Community-facing statement'),
3015
3760
  unit: z.string().max(100).optional().describe('Unit of measurement'),
3016
- trend_direction: z.enum(['higher_is_better', 'lower_is_better', 'target_is_optimal']).optional().describe('Which direction is better'),
3761
+ trend_direction: z
3762
+ .enum(['higher_is_better', 'lower_is_better', 'target_is_optimal'])
3763
+ .optional()
3764
+ .describe('Which direction is better'),
3017
3765
  target_value: z.number().optional().describe('Target value'),
3018
3766
  minimum_acceptable: z.number().optional().describe('Minimum acceptable value'),
3019
3767
  stretch_goal: z.number().optional().describe('Stretch goal value'),
3020
3768
  weight: z.number().min(0).optional().describe('Weight for composite score calculation'),
3021
- data_source_config: z.record(z.unknown()).optional().describe('Data source configuration (JSONB)'),
3769
+ data_source_config: z
3770
+ .record(z.unknown())
3771
+ .optional()
3772
+ .describe('Data source configuration (JSONB)'),
3022
3773
  is_active: z.boolean().optional().describe('Active status'),
3023
3774
  sort_order: z.number().int().min(0).optional().describe('Sort order'),
3024
3775
  }, async ({ id, ...rest }) => {
@@ -3044,12 +3795,19 @@ export function registerWriteTools(server, client) {
3044
3795
  // ============================================================
3045
3796
  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.', {
3046
3797
  los_measure_id: z.string().uuid().describe('LoS measure ID (required)'),
3047
- period_type: z.enum(['monthly', 'quarterly', 'semi_annual', 'annual']).describe('Period type (required)'),
3048
- period_start: z.string().describe('Period start date (ISO 8601, required, e.g., "2026-01-01")'),
3798
+ period_type: z
3799
+ .enum(['monthly', 'quarterly', 'semi_annual', 'annual'])
3800
+ .describe('Period type (required)'),
3801
+ period_start: z
3802
+ .string()
3803
+ .describe('Period start date (ISO 8601, required, e.g., "2026-01-01")'),
3049
3804
  period_end: z.string().describe('Period end date (ISO 8601, required, e.g., "2026-03-31")'),
3050
3805
  actual_value: z.number().describe('Measured value (required)'),
3051
3806
  notes: z.string().max(2000).optional().describe('Notes or context for this measurement'),
3052
- is_auto: z.boolean().optional().describe('Whether this is an auto-calculated value (default: false)'),
3807
+ is_auto: z
3808
+ .boolean()
3809
+ .optional()
3810
+ .describe('Whether this is an auto-calculated value (default: false)'),
3053
3811
  }, async (params) => {
3054
3812
  try {
3055
3813
  const result = await client.create('los-measurements', buildBody(params));
@@ -3063,7 +3821,10 @@ export function registerWriteTools(server, client) {
3063
3821
  id: z.string().uuid().describe('LoS measurement ID'),
3064
3822
  actual_value: z.number().optional().describe('Measured value'),
3065
3823
  notes: z.string().max(2000).optional().describe('Notes or context'),
3066
- period_type: z.enum(['monthly', 'quarterly', 'semi_annual', 'annual']).optional().describe('Period type'),
3824
+ period_type: z
3825
+ .enum(['monthly', 'quarterly', 'semi_annual', 'annual'])
3826
+ .optional()
3827
+ .describe('Period type'),
3067
3828
  period_start: z.string().optional().describe('Period start date (ISO 8601)'),
3068
3829
  period_end: z.string().optional().describe('Period end date (ISO 8601)'),
3069
3830
  is_auto: z.boolean().optional().describe('Whether this is auto-calculated'),
@@ -3095,14 +3856,39 @@ export function registerWriteTools(server, client) {
3095
3856
  .min(3)
3096
3857
  .describe('Polygon outline as an array of [x, y] points in normalized 0-1 coordinates (origin top-left). At least 3 points.');
3097
3858
  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.', {
3098
- building_id: z.string().uuid().optional().describe('Building this floor belongs to (omit if site-scoped)'),
3099
- site_id: z.string().uuid().optional().describe('Site this plan belongs to (use for site-level / campus plans; omit if building-scoped)'),
3100
- floor_label: z.string().max(200).describe('Human-readable label (e.g. "Ground Floor", "Mezzanine", "Site Plan")'),
3101
- floor_order: z.number().int().min(0).optional().describe('Sort order within the scope (lowest first)'),
3859
+ building_id: z
3860
+ .string()
3861
+ .uuid()
3862
+ .optional()
3863
+ .describe('Building this floor belongs to (omit if site-scoped)'),
3864
+ site_id: z
3865
+ .string()
3866
+ .uuid()
3867
+ .optional()
3868
+ .describe('Site this plan belongs to (use for site-level / campus plans; omit if building-scoped)'),
3869
+ floor_label: z
3870
+ .string()
3871
+ .max(200)
3872
+ .describe('Human-readable label (e.g. "Ground Floor", "Mezzanine", "Site Plan")'),
3873
+ floor_order: z
3874
+ .number()
3875
+ .int()
3876
+ .min(0)
3877
+ .optional()
3878
+ .describe('Sort order within the scope (lowest first)'),
3102
3879
  pdf_storage_path: z.string().max(1000).describe('Supabase Storage path to the PDF file'),
3103
3880
  pdf_filename: z.string().max(500).describe('Original filename for display'),
3104
- page_number: z.number().int().min(1).optional().describe('1-indexed page number within the PDF'),
3105
- page_width_pt: z.number().min(0).optional().describe('Page width in PDF points (discovered client-side)'),
3881
+ page_number: z
3882
+ .number()
3883
+ .int()
3884
+ .min(1)
3885
+ .optional()
3886
+ .describe('1-indexed page number within the PDF'),
3887
+ page_width_pt: z
3888
+ .number()
3889
+ .min(0)
3890
+ .optional()
3891
+ .describe('Page width in PDF points (discovered client-side)'),
3106
3892
  page_height_pt: z.number().min(0).optional().describe('Page height in PDF points'),
3107
3893
  status: z.enum(FLOORPLAN_STATUSES).optional().describe('Detection status (default: pending)'),
3108
3894
  }, async (params) => {
@@ -3147,10 +3933,25 @@ export function registerWriteTools(server, client) {
3147
3933
  floorplan_id: z.string().uuid().describe('Floorplan this region belongs to'),
3148
3934
  label: z.string().max(500).describe('Region label (e.g. "Boiler Room 2B")'),
3149
3935
  polygon: polygonSchema,
3150
- location_id: z.string().uuid().optional().describe('Linked Location ID (resolved via list_locations)'),
3151
- source: z.enum(REGION_SOURCES).optional().describe('"manual" (default) or "ai" for AI-detected'),
3152
- confidence: z.number().min(0).max(1).optional().describe('AI confidence score (0-1), only set when source=ai'),
3153
- reviewed: z.boolean().optional().describe('True if an admin has reviewed this region (default: true for manual, false for ai)'),
3936
+ location_id: z
3937
+ .string()
3938
+ .uuid()
3939
+ .optional()
3940
+ .describe('Linked Location ID (resolved via list_locations)'),
3941
+ source: z
3942
+ .enum(REGION_SOURCES)
3943
+ .optional()
3944
+ .describe('"manual" (default) or "ai" for AI-detected'),
3945
+ confidence: z
3946
+ .number()
3947
+ .min(0)
3948
+ .max(1)
3949
+ .optional()
3950
+ .describe('AI confidence score (0-1), only set when source=ai'),
3951
+ reviewed: z
3952
+ .boolean()
3953
+ .optional()
3954
+ .describe('True if an admin has reviewed this region (default: true for manual, false for ai)'),
3154
3955
  }, async (params) => {
3155
3956
  try {
3156
3957
  const result = await client.create('floorplan-regions', buildBody(params));
@@ -3164,7 +3965,11 @@ export function registerWriteTools(server, client) {
3164
3965
  id: z.string().uuid().describe('Floorplan region ID'),
3165
3966
  label: z.string().max(500).optional().describe('New label'),
3166
3967
  polygon: polygonSchema.optional(),
3167
- location_id: z.string().uuid().optional().describe('Linked Location ID (set to null to unlink)'),
3968
+ location_id: z
3969
+ .string()
3970
+ .uuid()
3971
+ .optional()
3972
+ .describe('Linked Location ID (set to null to unlink)'),
3168
3973
  confidence: z.number().min(0).max(1).optional(),
3169
3974
  reviewed: z.boolean().optional().describe('Mark region as reviewed by admin'),
3170
3975
  }, async ({ id, ...rest }) => {
@@ -3190,8 +3995,15 @@ export function registerWriteTools(server, client) {
3190
3995
  floorplan_id: z.string().uuid().describe('Target floorplan (resolve via list_floorplans)'),
3191
3996
  x: z.number().min(0).max(1).describe('Normalized x coordinate (0=left, 1=right)'),
3192
3997
  y: z.number().min(0).max(1).describe('Normalized y coordinate (0=top, 1=bottom)'),
3193
- region_id: z.string().uuid().optional().describe('Optional region the pin sits inside (usually auto-inferred)'),
3194
- source: z.enum(REGION_SOURCES).optional().describe('"manual" (default) or "ai" for AI-placed'),
3998
+ region_id: z
3999
+ .string()
4000
+ .uuid()
4001
+ .optional()
4002
+ .describe('Optional region the pin sits inside (usually auto-inferred)'),
4003
+ source: z
4004
+ .enum(REGION_SOURCES)
4005
+ .optional()
4006
+ .describe('"manual" (default) or "ai" for AI-placed'),
3195
4007
  }, async (params) => {
3196
4008
  try {
3197
4009
  const result = await client.create('asset-placements', buildBody(params));
@@ -3229,30 +4041,80 @@ export function registerWriteTools(server, client) {
3229
4041
  // Bulk operations
3230
4042
  // ============================================================
3231
4043
  const BULK_RESOURCES = [
3232
- 'assets', 'work-orders', 'work-requests', 'vendors', 'sites',
3233
- 'buildings', 'locations', 'systems', 'system-groups', 'system-classes',
3234
- 'pm-schedules', 'pm-templates', 'projects',
3235
- 'contracts', 'invoices', 'purchase-orders', 'expenses', 'budgets',
3236
- 'asset-types', 'asset-type-groups', 'asset-statuses', 'work-categories', 'manufacturers',
3237
- 'building-types', 'location-types', 'cost-categories',
3238
- 'compliance', 'compliance-records',
3239
- 'asset-comments', 'asset-costs', 'asset-replacement-plans',
3240
- 'work-order-comments', 'project-tasks', 'project-milestones',
3241
- 'project-phases', 'project-budget-items', 'project-time-entries',
3242
- 'project-comments', 'project-team-members', 'project-task-dependencies',
3243
- 'project-updates', 'project-cost-snapshots', 'project-locations',
3244
- 'project-sites', 'project-buildings', 'project-systems',
3245
- 'project-system-classes', 'project-system-groups', 'project-assets', 'project-risks',
3246
- 'parts', 'part-categories',
3247
- 'custom-field-definitions', 'custom-field-values',
3248
- 'vendor-site-assignments', 'contract-sites',
3249
- 'asset-documents', 'attachments', 'project-documents', 'contract-documents',
3250
- 'service-areas', 'los-measures', 'los-measurements',
3251
- 'floorplans', 'floorplan-regions', 'asset-placements',
4044
+ 'assets',
4045
+ 'work-orders',
4046
+ 'work-requests',
4047
+ 'vendors',
4048
+ 'sites',
4049
+ 'buildings',
4050
+ 'locations',
4051
+ 'systems',
4052
+ 'system-groups',
4053
+ 'system-classes',
4054
+ 'pm-schedules',
4055
+ 'pm-templates',
4056
+ 'projects',
4057
+ 'contracts',
4058
+ 'invoices',
4059
+ 'purchase-orders',
4060
+ 'expenses',
4061
+ 'budgets',
4062
+ 'asset-types',
4063
+ 'asset-type-groups',
4064
+ 'asset-statuses',
4065
+ 'work-categories',
4066
+ 'manufacturers',
4067
+ 'building-types',
4068
+ 'location-types',
4069
+ 'cost-categories',
4070
+ 'compliance',
4071
+ 'compliance-records',
4072
+ 'asset-comments',
4073
+ 'asset-costs',
4074
+ 'asset-replacement-plans',
4075
+ 'work-order-comments',
4076
+ 'project-tasks',
4077
+ 'project-milestones',
4078
+ 'project-phases',
4079
+ 'project-budget-items',
4080
+ 'project-time-entries',
4081
+ 'project-comments',
4082
+ 'project-team-members',
4083
+ 'project-task-dependencies',
4084
+ 'project-updates',
4085
+ 'project-cost-snapshots',
4086
+ 'project-locations',
4087
+ 'project-sites',
4088
+ 'project-buildings',
4089
+ 'project-systems',
4090
+ 'project-system-classes',
4091
+ 'project-system-groups',
4092
+ 'project-assets',
4093
+ 'project-risks',
4094
+ 'parts',
4095
+ 'part-categories',
4096
+ 'custom-field-definitions',
4097
+ 'custom-field-values',
4098
+ 'vendor-site-assignments',
4099
+ 'contract-sites',
4100
+ 'asset-documents',
4101
+ 'attachments',
4102
+ 'project-documents',
4103
+ 'contract-documents',
4104
+ 'service-areas',
4105
+ 'los-measures',
4106
+ 'los-measurements',
4107
+ 'floorplans',
4108
+ 'floorplan-regions',
4109
+ 'asset-placements',
3252
4110
  ];
3253
4111
  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.', {
3254
4112
  resource: z.enum(BULK_RESOURCES).describe('Resource type (e.g. "assets", "work-orders")'),
3255
- 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.'),
4113
+ items: z
4114
+ .array(z.record(z.unknown()))
4115
+ .min(1)
4116
+ .max(100)
4117
+ .describe('Array of objects to create (max 100). Each object uses the same fields as the single-create endpoint for that resource.'),
3256
4118
  }, async ({ resource, items }) => {
3257
4119
  try {
3258
4120
  const result = await client.bulkCreate(resource, items);
@@ -3306,7 +4168,12 @@ export function registerWriteTools(server, client) {
3306
4168
  name: z.string().max(500).describe('Compliance item name (required)'),
3307
4169
  description: z.string().optional().describe('Description'),
3308
4170
  regulation_reference: z.string().max(500).optional().describe('Regulation or code reference'),
3309
- compliance_period_months: z.number().int().min(1).optional().describe('Compliance period in months'),
4171
+ compliance_period_months: z
4172
+ .number()
4173
+ .int()
4174
+ .min(1)
4175
+ .optional()
4176
+ .describe('Compliance period in months'),
3310
4177
  status: z.enum(['active', 'archived']).optional().describe('Status'),
3311
4178
  system_id: z.string().uuid().optional().describe('Associated system ID'),
3312
4179
  }, async (params) => {
@@ -3323,7 +4190,12 @@ export function registerWriteTools(server, client) {
3323
4190
  name: z.string().max(500).optional().describe('Compliance item name'),
3324
4191
  description: z.string().optional().describe('Description'),
3325
4192
  regulation_reference: z.string().max(500).optional().describe('Regulation or code reference'),
3326
- compliance_period_months: z.number().int().min(1).optional().describe('Compliance period in months'),
4193
+ compliance_period_months: z
4194
+ .number()
4195
+ .int()
4196
+ .min(1)
4197
+ .optional()
4198
+ .describe('Compliance period in months'),
3327
4199
  status: z.enum(['active', 'archived']).optional().describe('Status'),
3328
4200
  system_id: z.string().uuid().optional().describe('Associated system ID'),
3329
4201
  }, async ({ id, ...rest }) => {
@@ -3353,8 +4225,17 @@ export function registerWriteTools(server, client) {
3353
4225
  work_order_id: z.string().uuid().describe('Work order ID (required)'),
3354
4226
  completed_at: z.string().describe('Completion date-time (ISO 8601, required)'),
3355
4227
  completed_by: z.string().max(200).optional().describe('User ID who completed'),
3356
- required_frequency_days: z.number().int().min(1).describe('Required frequency in days (required)'),
3357
- days_since_last_completion: z.number().int().min(0).optional().describe('Days since last completion'),
4228
+ required_frequency_days: z
4229
+ .number()
4230
+ .int()
4231
+ .min(1)
4232
+ .describe('Required frequency in days (required)'),
4233
+ days_since_last_completion: z
4234
+ .number()
4235
+ .int()
4236
+ .min(0)
4237
+ .optional()
4238
+ .describe('Days since last completion'),
3358
4239
  }, async (params) => {
3359
4240
  try {
3360
4241
  const result = await client.create('compliance-records', buildBody(params));
@@ -3368,8 +4249,18 @@ export function registerWriteTools(server, client) {
3368
4249
  id: z.string().uuid().describe('Compliance record ID'),
3369
4250
  completed_at: z.string().optional().describe('Completion date-time (ISO 8601)'),
3370
4251
  completed_by: z.string().max(200).optional().describe('User ID who completed'),
3371
- required_frequency_days: z.number().int().min(1).optional().describe('Required frequency in days'),
3372
- days_since_last_completion: z.number().int().min(0).optional().describe('Days since last completion'),
4252
+ required_frequency_days: z
4253
+ .number()
4254
+ .int()
4255
+ .min(1)
4256
+ .optional()
4257
+ .describe('Required frequency in days'),
4258
+ days_since_last_completion: z
4259
+ .number()
4260
+ .int()
4261
+ .min(0)
4262
+ .optional()
4263
+ .describe('Days since last completion'),
3373
4264
  }, async ({ id, ...rest }) => {
3374
4265
  try {
3375
4266
  const result = await client.update('compliance-records', id, buildBody(rest));
@@ -3393,7 +4284,11 @@ export function registerWriteTools(server, client) {
3393
4284
  // ============================================================
3394
4285
  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.', {
3395
4286
  resource: z.enum(BULK_RESOURCES).describe('Resource type (e.g. "assets", "work-orders")'),
3396
- 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.'),
4287
+ items: z
4288
+ .array(z.record(z.unknown()))
4289
+ .min(1)
4290
+ .max(100)
4291
+ .describe('Array of objects to update (max 100). Each must include an "id" field (UUID) plus fields to change.'),
3397
4292
  }, async ({ resource, items }) => {
3398
4293
  try {
3399
4294
  const result = await client.bulkUpdate(resource, items);