@assetlab/mcp-server 1.17.0 → 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.
@@ -3049,6 +3049,140 @@ export function registerWriteTools(server, client) {
3049
3049
  }
3050
3050
  });
3051
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
+ // ============================================================
3052
3186
  // Bulk operations
3053
3187
  // ============================================================
3054
3188
  const BULK_RESOURCES = [
@@ -3071,6 +3205,7 @@ export function registerWriteTools(server, client) {
3071
3205
  'vendor-site-assignments', 'contract-sites',
3072
3206
  'asset-documents', 'attachments', 'project-documents', 'contract-documents',
3073
3207
  'service-areas', 'los-measures', 'los-measurements',
3208
+ 'floorplans', 'floorplan-regions', 'asset-placements',
3074
3209
  ];
3075
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.', {
3076
3211
  resource: z.enum(BULK_RESOURCES).describe('Resource type (e.g. "assets", "work-orders")'),