@assetlab/mcp-server 1.16.0 → 1.17.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.
package/README.md CHANGED
@@ -13,7 +13,7 @@ Works with **Claude**, **ChatGPT**, **Microsoft Copilot**, and any MCP-compatibl
13
13
  ### Claude.ai
14
14
 
15
15
  1. Create an API key in **AssetLab → Settings → API Keys**
16
- 2. In Claude.ai, go to **Settings → Integrations → Add integration**
16
+ 2. In Claude.ai, go to **Settings → Connectors → Add connector**
17
17
  3. Paste the connector URL: `https://mcp.assetlab.ca`
18
18
  4. When prompted for auth, paste your API key (`al_live_...`)
19
19
 
@@ -25,6 +25,59 @@ function buildBody(params) {
25
25
  }
26
26
  return body;
27
27
  }
28
+ const PM_RESOURCE_TYPES = ['TOOL', 'PART', 'MATERIAL', 'EQUIPMENT'];
29
+ function randomTaskId() {
30
+ const g = globalThis;
31
+ return g.crypto?.randomUUID?.() ?? `task_${Math.random().toString(36).slice(2, 12)}`;
32
+ }
33
+ function coerceNumber(value) {
34
+ if (typeof value === 'number' && Number.isFinite(value))
35
+ return value;
36
+ if (typeof value === 'string' && value.trim() !== '') {
37
+ const n = Number(value);
38
+ if (Number.isFinite(n))
39
+ return n;
40
+ }
41
+ return undefined;
42
+ }
43
+ /**
44
+ * Shape pm_template tasks/resources to match the UI form's zod schema so
45
+ * records written via MCP remain editable in the app. Unknown keys are dropped,
46
+ * resource types are uppercased to the allowed enum, and task ids are filled in.
47
+ */
48
+ function normalizePmTemplateBody(params) {
49
+ const out = { ...params };
50
+ if (Array.isArray(out.tasks)) {
51
+ out.tasks = out.tasks.map((raw) => {
52
+ const t = (raw && typeof raw === 'object') ? raw : {};
53
+ const id = typeof t.id === 'string' && t.id.trim() !== '' ? t.id : randomTaskId();
54
+ const description = typeof t.description === 'string' ? t.description : '';
55
+ const completed = typeof t.completed === 'boolean' ? t.completed : false;
56
+ return { id, description, completed };
57
+ });
58
+ }
59
+ if (Array.isArray(out.resources)) {
60
+ out.resources = out.resources.map((raw) => {
61
+ const r = (raw && typeof raw === 'object') ? raw : {};
62
+ const res = {};
63
+ if (typeof r.name === 'string')
64
+ res.name = r.name;
65
+ if (typeof r.type === 'string') {
66
+ const upper = r.type.toUpperCase();
67
+ if (PM_RESOURCE_TYPES.includes(upper))
68
+ res.type = upper;
69
+ }
70
+ const cost = coerceNumber(r.cost);
71
+ if (cost !== undefined)
72
+ res.cost = cost;
73
+ const quantity = coerceNumber(r.quantity);
74
+ if (quantity !== undefined)
75
+ res.quantity = quantity;
76
+ return res;
77
+ });
78
+ }
79
+ return out;
80
+ }
28
81
  // ---------------------------------------------------------------------------
29
82
  // Registration
30
83
  // ---------------------------------------------------------------------------
@@ -579,17 +632,22 @@ export function registerWriteTools(server, client) {
579
632
  estimated_cost: z.number().min(0).optional().describe('Estimated cost'),
580
633
  safety_requirements: z.string().optional().describe('Safety requirements'),
581
634
  tasks: z.array(z.object({
582
- id: z.string().describe('Unique task ID (use a random string)'),
635
+ id: z.string().optional().describe('Unique task ID (auto-generated if omitted)'),
583
636
  description: z.string().describe('Task description'),
584
- completed: z.boolean().describe('Whether the task is completed'),
637
+ completed: z.boolean().optional().describe('Whether the task is completed (defaults to false)'),
585
638
  })).optional().describe('Checklist of tasks baked into this template'),
586
- resources: z.array(z.record(z.unknown())).optional().describe('Resource references (parts, tools, documents)'),
639
+ resources: z.array(z.object({
640
+ name: z.string().optional().describe('Resource name'),
641
+ type: z.enum(PM_RESOURCE_TYPES).optional().describe('Resource type (TOOL, PART, MATERIAL, or EQUIPMENT)'),
642
+ quantity: z.number().optional().describe('Quantity required'),
643
+ cost: z.number().optional().describe('Unit cost'),
644
+ })).optional().describe('Resource references (parts, tools, materials, equipment)'),
587
645
  documents: z.array(z.record(z.unknown())).optional().describe('Document references'),
588
646
  asset_ids: z.array(z.string().uuid()).optional().describe('Default asset IDs to seed on derived schedules'),
589
647
  location_ids: z.array(z.string().uuid()).optional().describe('Default location IDs to seed on derived schedules'),
590
648
  }, async (params) => {
591
649
  try {
592
- const result = await client.create('pm-templates', buildBody(params));
650
+ const result = await client.create('pm-templates', buildBody(normalizePmTemplateBody(params)));
593
651
  return formatResult(result);
594
652
  }
595
653
  catch (err) {
@@ -608,17 +666,22 @@ export function registerWriteTools(server, client) {
608
666
  estimated_cost: z.number().min(0).optional().describe('Estimated cost'),
609
667
  safety_requirements: z.string().optional().describe('Safety requirements'),
610
668
  tasks: z.array(z.object({
611
- id: z.string().describe('Unique task ID'),
669
+ id: z.string().optional().describe('Unique task ID (auto-generated if omitted)'),
612
670
  description: z.string().describe('Task description'),
613
- completed: z.boolean().describe('Whether the task is completed'),
671
+ completed: z.boolean().optional().describe('Whether the task is completed (defaults to false)'),
614
672
  })).optional().describe('Checklist of tasks baked into this template'),
615
- resources: z.array(z.record(z.unknown())).optional().describe('Resource references'),
673
+ resources: z.array(z.object({
674
+ name: z.string().optional().describe('Resource name'),
675
+ type: z.enum(PM_RESOURCE_TYPES).optional().describe('Resource type (TOOL, PART, MATERIAL, or EQUIPMENT)'),
676
+ quantity: z.number().optional().describe('Quantity required'),
677
+ cost: z.number().optional().describe('Unit cost'),
678
+ })).optional().describe('Resource references (parts, tools, materials, equipment)'),
616
679
  documents: z.array(z.record(z.unknown())).optional().describe('Document references'),
617
680
  asset_ids: z.array(z.string().uuid()).optional().describe('Default asset IDs'),
618
681
  location_ids: z.array(z.string().uuid()).optional().describe('Default location IDs'),
619
682
  }, async ({ id, ...rest }) => {
620
683
  try {
621
- const result = await client.update('pm-templates', id, buildBody(rest));
684
+ const result = await client.update('pm-templates', id, buildBody(normalizePmTemplateBody(rest)));
622
685
  return formatResult(result);
623
686
  }
624
687
  catch (err) {
@@ -2189,6 +2252,20 @@ export function registerWriteTools(server, client) {
2189
2252
  return formatError(err);
2190
2253
  }
2191
2254
  });
2255
+ 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.', {
2256
+ 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.'),
2257
+ file_name: z.string().min(1).max(500).describe('File name including extension (required)'),
2258
+ content_base64: z.string().min(1).describe('File contents base64-encoded (required). Data URI prefixes like "data:image/png;base64," are stripped automatically.'),
2259
+ content_type: z.string().max(200).optional().describe('MIME type (e.g. image/jpeg, application/pdf). Defaults to application/octet-stream.'),
2260
+ }, async (params) => {
2261
+ try {
2262
+ const result = await client.create('upload-files', buildBody(params));
2263
+ return formatResult(result);
2264
+ }
2265
+ catch (err) {
2266
+ return formatError(err);
2267
+ }
2268
+ });
2192
2269
  // ============================================================
2193
2270
  // Asset Documents (scope: asset_documents)
2194
2271
  // ============================================================