@assetlab/mcp-server 1.16.1 → 1.18.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 +1 -1
- package/dist/tools-write.js +149 -0
- package/dist/tools-write.js.map +1 -1
- package/dist/tools.d.ts +1 -1
- package/dist/tools.js +122 -0
- package/dist/tools.js.map +1 -1
- package/package.json +1 -1
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 →
|
|
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
|
|
package/dist/tools-write.js
CHANGED
|
@@ -2252,6 +2252,20 @@ export function registerWriteTools(server, client) {
|
|
|
2252
2252
|
return formatError(err);
|
|
2253
2253
|
}
|
|
2254
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
|
+
});
|
|
2255
2269
|
// ============================================================
|
|
2256
2270
|
// Asset Documents (scope: asset_documents)
|
|
2257
2271
|
// ============================================================
|
|
@@ -3035,6 +3049,140 @@ export function registerWriteTools(server, client) {
|
|
|
3035
3049
|
}
|
|
3036
3050
|
});
|
|
3037
3051
|
// ============================================================
|
|
3052
|
+
// Floorplans (enterprise++ feature)
|
|
3053
|
+
// ============================================================
|
|
3054
|
+
const FLOORPLAN_STATUSES = ['pending', 'detecting', 'ready', 'failed'];
|
|
3055
|
+
const REGION_SOURCES = ['manual', 'ai'];
|
|
3056
|
+
const polygonSchema = z
|
|
3057
|
+
.array(z.tuple([z.number().min(0).max(1), z.number().min(0).max(1)]))
|
|
3058
|
+
.min(3)
|
|
3059
|
+
.describe('Polygon outline as an array of [x, y] points in normalized 0-1 coordinates (origin top-left). At least 3 points.');
|
|
3060
|
+
server.tool('create_floorplan', 'Create a floorplan row for one floor of a building. 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.', {
|
|
3061
|
+
building_id: z.string().uuid().describe('Building this floor belongs to'),
|
|
3062
|
+
floor_label: z.string().max(200).describe('Human-readable floor name (e.g. "Ground Floor", "Mezzanine")'),
|
|
3063
|
+
floor_order: z.number().int().min(0).optional().describe('Sort order within the building (lowest = lowest floor)'),
|
|
3064
|
+
pdf_storage_path: z.string().max(1000).describe('Supabase Storage path to the PDF file, e.g. "<tenant_id>/<building_id>/<uuid>.pdf"'),
|
|
3065
|
+
pdf_filename: z.string().max(500).describe('Original filename for display'),
|
|
3066
|
+
page_number: z.number().int().min(1).optional().describe('1-indexed page number within the PDF'),
|
|
3067
|
+
page_width_pt: z.number().min(0).optional().describe('Page width in PDF points (discovered client-side)'),
|
|
3068
|
+
page_height_pt: z.number().min(0).optional().describe('Page height in PDF points'),
|
|
3069
|
+
status: z.enum(FLOORPLAN_STATUSES).optional().describe('Detection status (default: pending)'),
|
|
3070
|
+
}, async (params) => {
|
|
3071
|
+
try {
|
|
3072
|
+
const result = await client.create('floorplans', buildBody(params));
|
|
3073
|
+
return formatResult(result);
|
|
3074
|
+
}
|
|
3075
|
+
catch (err) {
|
|
3076
|
+
return formatError(err);
|
|
3077
|
+
}
|
|
3078
|
+
});
|
|
3079
|
+
server.tool('update_floorplan', 'Update floorplan metadata (rename floor, reorder, set status). Requires floorplans:write scope.', {
|
|
3080
|
+
id: z.string().uuid().describe('Floorplan ID'),
|
|
3081
|
+
floor_label: z.string().max(200).optional().describe('New floor label'),
|
|
3082
|
+
floor_order: z.number().int().min(0).optional().describe('New sort order'),
|
|
3083
|
+
status: z.enum(FLOORPLAN_STATUSES).optional().describe('Detection status'),
|
|
3084
|
+
detection_error: z.string().max(2000).optional().describe('Error message if status=failed'),
|
|
3085
|
+
}, async ({ id, ...rest }) => {
|
|
3086
|
+
try {
|
|
3087
|
+
const result = await client.update('floorplans', id, buildBody(rest));
|
|
3088
|
+
return formatResult(result);
|
|
3089
|
+
}
|
|
3090
|
+
catch (err) {
|
|
3091
|
+
return formatError(err);
|
|
3092
|
+
}
|
|
3093
|
+
});
|
|
3094
|
+
server.tool('delete_floorplan', 'Delete a floorplan. WARNING: cascades to all regions and asset placements on this floor. Does NOT delete the underlying PDF file from storage (do that separately if no other floors reference it). Requires floorplans:write scope.', { id: z.string().uuid().describe('Floorplan ID') }, async ({ id }) => {
|
|
3095
|
+
try {
|
|
3096
|
+
const result = await client.remove('floorplans', id);
|
|
3097
|
+
return formatResult(result);
|
|
3098
|
+
}
|
|
3099
|
+
catch (err) {
|
|
3100
|
+
return formatError(err);
|
|
3101
|
+
}
|
|
3102
|
+
});
|
|
3103
|
+
server.tool('create_floorplan_region', 'Create a region (labeled room or zone) on a floorplan. Polygon coordinates are normalized 0-1 with origin top-left. Optionally link the region to an existing Location via location_id. Requires floorplan_regions:write scope.', {
|
|
3104
|
+
floorplan_id: z.string().uuid().describe('Floorplan this region belongs to'),
|
|
3105
|
+
label: z.string().max(500).describe('Region label (e.g. "Boiler Room 2B")'),
|
|
3106
|
+
polygon: polygonSchema,
|
|
3107
|
+
location_id: z.string().uuid().optional().describe('Linked Location ID (resolved via list_locations)'),
|
|
3108
|
+
source: z.enum(REGION_SOURCES).optional().describe('"manual" (default) or "ai" for AI-detected'),
|
|
3109
|
+
confidence: z.number().min(0).max(1).optional().describe('AI confidence score (0-1), only set when source=ai'),
|
|
3110
|
+
reviewed: z.boolean().optional().describe('True if an admin has reviewed this region (default: true for manual, false for ai)'),
|
|
3111
|
+
}, async (params) => {
|
|
3112
|
+
try {
|
|
3113
|
+
const result = await client.create('floorplan-regions', buildBody(params));
|
|
3114
|
+
return formatResult(result);
|
|
3115
|
+
}
|
|
3116
|
+
catch (err) {
|
|
3117
|
+
return formatError(err);
|
|
3118
|
+
}
|
|
3119
|
+
});
|
|
3120
|
+
server.tool('update_floorplan_region', 'Update a floorplan region — rename, reshape polygon, link to a Location, or mark as reviewed. Requires floorplan_regions:write scope.', {
|
|
3121
|
+
id: z.string().uuid().describe('Floorplan region ID'),
|
|
3122
|
+
label: z.string().max(500).optional().describe('New label'),
|
|
3123
|
+
polygon: polygonSchema.optional(),
|
|
3124
|
+
location_id: z.string().uuid().optional().describe('Linked Location ID (set to null to unlink)'),
|
|
3125
|
+
confidence: z.number().min(0).max(1).optional(),
|
|
3126
|
+
reviewed: z.boolean().optional().describe('Mark region as reviewed by admin'),
|
|
3127
|
+
}, async ({ id, ...rest }) => {
|
|
3128
|
+
try {
|
|
3129
|
+
const result = await client.update('floorplan-regions', id, buildBody(rest));
|
|
3130
|
+
return formatResult(result);
|
|
3131
|
+
}
|
|
3132
|
+
catch (err) {
|
|
3133
|
+
return formatError(err);
|
|
3134
|
+
}
|
|
3135
|
+
});
|
|
3136
|
+
server.tool('delete_floorplan_region', 'Delete a region. Asset placements that referenced this region will have region_id set to null but remain on the floorplan. Requires floorplan_regions:write scope.', { id: z.string().uuid().describe('Floorplan region ID') }, async ({ id }) => {
|
|
3137
|
+
try {
|
|
3138
|
+
const result = await client.remove('floorplan-regions', id);
|
|
3139
|
+
return formatResult(result);
|
|
3140
|
+
}
|
|
3141
|
+
catch (err) {
|
|
3142
|
+
return formatError(err);
|
|
3143
|
+
}
|
|
3144
|
+
});
|
|
3145
|
+
server.tool('create_asset_placement', 'Place an asset on a floorplan at the given (x, y) coordinate (normalized 0-1, origin top-left). UPSERTS by asset_id — an asset can have at most ONE placement globally, so calling this again just moves the pin. Use bulk_create with resource="asset-placements" to place many assets at once. Requires asset_placements:write scope.', {
|
|
3146
|
+
asset_id: z.string().uuid().describe('Asset to place (resolve via list_assets)'),
|
|
3147
|
+
floorplan_id: z.string().uuid().describe('Target floorplan (resolve via list_floorplans)'),
|
|
3148
|
+
x: z.number().min(0).max(1).describe('Normalized x coordinate (0=left, 1=right)'),
|
|
3149
|
+
y: z.number().min(0).max(1).describe('Normalized y coordinate (0=top, 1=bottom)'),
|
|
3150
|
+
region_id: z.string().uuid().optional().describe('Optional region the pin sits inside (usually auto-inferred)'),
|
|
3151
|
+
source: z.enum(REGION_SOURCES).optional().describe('"manual" (default) or "ai" for AI-placed'),
|
|
3152
|
+
}, async (params) => {
|
|
3153
|
+
try {
|
|
3154
|
+
const result = await client.create('asset-placements', buildBody(params));
|
|
3155
|
+
return formatResult(result);
|
|
3156
|
+
}
|
|
3157
|
+
catch (err) {
|
|
3158
|
+
return formatError(err);
|
|
3159
|
+
}
|
|
3160
|
+
});
|
|
3161
|
+
server.tool('update_asset_placement', 'Move an asset placement to new coordinates or a different floorplan. Requires asset_placements:write scope.', {
|
|
3162
|
+
id: z.string().uuid().describe('Asset placement ID'),
|
|
3163
|
+
x: z.number().min(0).max(1).optional().describe('New x coordinate'),
|
|
3164
|
+
y: z.number().min(0).max(1).optional().describe('New y coordinate'),
|
|
3165
|
+
region_id: z.string().uuid().optional().describe('New region (set to null to clear)'),
|
|
3166
|
+
floorplan_id: z.string().uuid().optional().describe('Move to a different floorplan'),
|
|
3167
|
+
}, async ({ id, ...rest }) => {
|
|
3168
|
+
try {
|
|
3169
|
+
const result = await client.update('asset-placements', id, buildBody(rest));
|
|
3170
|
+
return formatResult(result);
|
|
3171
|
+
}
|
|
3172
|
+
catch (err) {
|
|
3173
|
+
return formatError(err);
|
|
3174
|
+
}
|
|
3175
|
+
});
|
|
3176
|
+
server.tool('delete_asset_placement', 'Remove an asset placement. The asset itself is not affected — only the pin on the floorplan is removed. Requires asset_placements:write scope.', { id: z.string().uuid().describe('Asset placement ID') }, async ({ id }) => {
|
|
3177
|
+
try {
|
|
3178
|
+
const result = await client.remove('asset-placements', id);
|
|
3179
|
+
return formatResult(result);
|
|
3180
|
+
}
|
|
3181
|
+
catch (err) {
|
|
3182
|
+
return formatError(err);
|
|
3183
|
+
}
|
|
3184
|
+
});
|
|
3185
|
+
// ============================================================
|
|
3038
3186
|
// Bulk operations
|
|
3039
3187
|
// ============================================================
|
|
3040
3188
|
const BULK_RESOURCES = [
|
|
@@ -3057,6 +3205,7 @@ export function registerWriteTools(server, client) {
|
|
|
3057
3205
|
'vendor-site-assignments', 'contract-sites',
|
|
3058
3206
|
'asset-documents', 'attachments', 'project-documents', 'contract-documents',
|
|
3059
3207
|
'service-areas', 'los-measures', 'los-measurements',
|
|
3208
|
+
'floorplans', 'floorplan-regions', 'asset-placements',
|
|
3060
3209
|
];
|
|
3061
3210
|
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.', {
|
|
3062
3211
|
resource: z.enum(BULK_RESOURCES).describe('Resource type (e.g. "assets", "work-orders")'),
|