@sealab/mcp-server 1.0.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.
Files changed (48) hide show
  1. package/PROPOSED-CHANGES-INSERTION-POINTS.md +220 -0
  2. package/SEALAB_MCP_DOCUMENTATION.md +1136 -0
  3. package/dist/client/api-client.js +44 -0
  4. package/dist/index.js +42 -0
  5. package/dist/tools/canvas.js +446 -0
  6. package/dist/tools/catalog.js +95 -0
  7. package/dist/tools/configuration-info.js +299 -0
  8. package/dist/tools/configuration.js +32 -0
  9. package/dist/tools/orders.js +1267 -0
  10. package/dist/tools/saved-settings.js +271 -0
  11. package/package.json +32 -0
  12. package/resources/tooltips/backPanel.txt +17 -0
  13. package/resources/tooltips/backPanelMaterial.txt +29 -0
  14. package/resources/tooltips/caseEdge.txt +18 -0
  15. package/resources/tooltips/caseMaterial.txt +31 -0
  16. package/resources/tooltips/depth.txt +11 -0
  17. package/resources/tooltips/drawerType.txt +12 -0
  18. package/resources/tooltips/edgeBandingType.txt +18 -0
  19. package/resources/tooltips/excludeFronts.txt +5 -0
  20. package/resources/tooltips/frontEdge.txt +18 -0
  21. package/resources/tooltips/frontMaterial.txt +35 -0
  22. package/resources/tooltips/gapBottom.txt +2 -0
  23. package/resources/tooltips/gapCenter.txt +2 -0
  24. package/resources/tooltips/gapLeft.txt +15 -0
  25. package/resources/tooltips/gapRight.txt +15 -0
  26. package/resources/tooltips/gapTop.txt +2 -0
  27. package/resources/tooltips/height.txt +6 -0
  28. package/resources/tooltips/hingePlate.txt +11 -0
  29. package/resources/tooltips/includeLegLevelers.txt +8 -0
  30. package/resources/tooltips/jointMethod.txt +7 -0
  31. package/resources/tooltips/leftCornerWidth.txt +2 -0
  32. package/resources/tooltips/numOfShelves.txt +6 -0
  33. package/resources/tooltips/positionName.txt +3 -0
  34. package/resources/tooltips/rightCornerDepth.txt +2 -0
  35. package/resources/tooltips/topDrwrHeight.txt +8 -0
  36. package/resources/tooltips/width.txt +5 -0
  37. package/src/client/api-client.ts +37 -0
  38. package/src/index.ts +52 -0
  39. package/src/tools/canvas.ts +442 -0
  40. package/src/tools/catalog.test.ts +61 -0
  41. package/src/tools/catalog.ts +80 -0
  42. package/src/tools/configuration-info.ts +274 -0
  43. package/src/tools/configuration.test.ts +43 -0
  44. package/src/tools/configuration.ts +25 -0
  45. package/src/tools/orders.test.ts +260 -0
  46. package/src/tools/orders.ts +1229 -0
  47. package/src/tools/saved-settings.ts +241 -0
  48. package/tsconfig.json +15 -0
@@ -0,0 +1,44 @@
1
+ "use strict";
2
+ var __importDefault = (this && this.__importDefault) || function (mod) {
3
+ return (mod && mod.__esModule) ? mod : { "default": mod };
4
+ };
5
+ Object.defineProperty(exports, "__esModule", { value: true });
6
+ exports.McpApiError = exports.client = void 0;
7
+ exports.handleAxiosError = handleAxiosError;
8
+ const axios_1 = __importDefault(require("axios"));
9
+ const API_KEY = process.env.SEALAB_API_KEY;
10
+ const API_URL = process.env.SEALAB_API_URL ?? 'https://thesealab.com';
11
+ if (!API_KEY) {
12
+ throw new Error('SEALAB_API_KEY environment variable is required');
13
+ }
14
+ exports.client = axios_1.default.create({
15
+ baseURL: `${API_URL}/api/mcp/v1`,
16
+ headers: {
17
+ 'X-API-Key': API_KEY,
18
+ },
19
+ timeout: 10000,
20
+ });
21
+ class McpApiError extends Error {
22
+ status;
23
+ constructor(status, message) {
24
+ super(message);
25
+ this.status = status;
26
+ this.name = 'McpApiError';
27
+ }
28
+ }
29
+ exports.McpApiError = McpApiError;
30
+ function handleAxiosError(error) {
31
+ if (axios_1.default.isAxiosError(error)) {
32
+ const axiosErr = error;
33
+ const status = axiosErr.response?.status ?? 0;
34
+ const message = axiosErr.response?.data?.error ?? axiosErr.message;
35
+ if (status === 401)
36
+ throw new McpApiError(401, 'Invalid API key');
37
+ if (status === 404)
38
+ throw new McpApiError(404, message);
39
+ if (status === 422)
40
+ throw new McpApiError(422, message);
41
+ throw new McpApiError(status, `API error: ${message}`);
42
+ }
43
+ throw new McpApiError(0, 'Unable to reach Sealab API — check SEALAB_API_URL');
44
+ }
package/dist/index.js ADDED
@@ -0,0 +1,42 @@
1
+ #!/usr/bin/env node
2
+ "use strict";
3
+ Object.defineProperty(exports, "__esModule", { value: true });
4
+ const index_js_1 = require("@modelcontextprotocol/sdk/server/index.js");
5
+ const stdio_js_1 = require("@modelcontextprotocol/sdk/server/stdio.js");
6
+ const types_js_1 = require("@modelcontextprotocol/sdk/types.js");
7
+ const zod_to_json_schema_1 = require("zod-to-json-schema");
8
+ const catalog_1 = require("./tools/catalog");
9
+ const orders_1 = require("./tools/orders");
10
+ const configuration_1 = require("./tools/configuration");
11
+ const configuration_info_1 = require("./tools/configuration-info");
12
+ const saved_settings_1 = require("./tools/saved-settings");
13
+ const canvas_1 = require("./tools/canvas");
14
+ const allTools = [...catalog_1.catalogTools, ...orders_1.orderTools, ...configuration_1.configurationTools, ...configuration_info_1.configurationInfoTools, ...saved_settings_1.savedSettingsTools, ...canvas_1.canvasTools];
15
+ const server = new index_js_1.Server({ name: 'sealab', version: '1.0.0' }, { capabilities: { tools: {} } });
16
+ server.setRequestHandler(types_js_1.ListToolsRequestSchema, async () => ({
17
+ tools: allTools.map((t) => ({
18
+ name: t.name,
19
+ description: t.description,
20
+ inputSchema: (0, zod_to_json_schema_1.zodToJsonSchema)(t.inputSchema, { target: 'openApi3' }),
21
+ })),
22
+ }));
23
+ server.setRequestHandler(types_js_1.CallToolRequestSchema, async (request) => {
24
+ const tool = allTools.find((t) => t.name === request.params.name);
25
+ if (!tool) {
26
+ return { content: [{ type: 'text', text: `Unknown tool: ${request.params.name}` }], isError: true };
27
+ }
28
+ const parsed = tool.inputSchema.safeParse(request.params.arguments ?? {});
29
+ if (!parsed.success) {
30
+ return {
31
+ content: [{ type: 'text', text: `Invalid input: ${parsed.error.message}` }],
32
+ isError: true,
33
+ };
34
+ }
35
+ const result = await tool.handler(parsed.data);
36
+ return { content: [{ type: 'text', text: result }] };
37
+ });
38
+ async function main() {
39
+ const transport = new stdio_js_1.StdioServerTransport();
40
+ await server.connect(transport);
41
+ }
42
+ main().catch(console.error);
@@ -0,0 +1,446 @@
1
+ "use strict";
2
+ var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
3
+ if (k2 === undefined) k2 = k;
4
+ var desc = Object.getOwnPropertyDescriptor(m, k);
5
+ if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
6
+ desc = { enumerable: true, get: function() { return m[k]; } };
7
+ }
8
+ Object.defineProperty(o, k2, desc);
9
+ }) : (function(o, m, k, k2) {
10
+ if (k2 === undefined) k2 = k;
11
+ o[k2] = m[k];
12
+ }));
13
+ var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
14
+ Object.defineProperty(o, "default", { enumerable: true, value: v });
15
+ }) : function(o, v) {
16
+ o["default"] = v;
17
+ });
18
+ var __importStar = (this && this.__importStar) || (function () {
19
+ var ownKeys = function(o) {
20
+ ownKeys = Object.getOwnPropertyNames || function (o) {
21
+ var ar = [];
22
+ for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
23
+ return ar;
24
+ };
25
+ return ownKeys(o);
26
+ };
27
+ return function (mod) {
28
+ if (mod && mod.__esModule) return mod;
29
+ var result = {};
30
+ if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
31
+ __setModuleDefault(result, mod);
32
+ return result;
33
+ };
34
+ })();
35
+ var __importDefault = (this && this.__importDefault) || function (mod) {
36
+ return (mod && mod.__esModule) ? mod : { "default": mod };
37
+ };
38
+ Object.defineProperty(exports, "__esModule", { value: true });
39
+ exports.canvasTools = void 0;
40
+ const zod_1 = require("zod");
41
+ const api_client_1 = require("../client/api-client");
42
+ const fs = __importStar(require("fs"));
43
+ const path = __importStar(require("path"));
44
+ const form_data_1 = __importDefault(require("form-data"));
45
+ // ---------------------------------------------------------------------------
46
+ // Schemas
47
+ // ---------------------------------------------------------------------------
48
+ const GetDrawingToolStateSchema = zod_1.z.object({
49
+ orderId: zod_1.z.string().describe('Order ID to retrieve full drawing tool state for (metadata + images + coordinates).'),
50
+ });
51
+ const GetCanvasMetadataSchema = zod_1.z.object({
52
+ orderId: zod_1.z.string().describe('Order ID to retrieve canvas metadata for.'),
53
+ });
54
+ const GetCanvasImagesSchema = zod_1.z.object({
55
+ orderId: zod_1.z.string().describe('Order ID to retrieve canvas image records for.'),
56
+ });
57
+ const GetCabinetCoordinatesSchema = zod_1.z.object({
58
+ orderId: zod_1.z.string().describe('Order ID to retrieve cabinet coordinates for.'),
59
+ });
60
+ const CoordinateItemSchema = zod_1.z.object({
61
+ positionName: zod_1.z.string().describe('Position name of the cabinet as stored in the order (e.g. "Base_01", "Upper_01"). ' +
62
+ 'Must match the positionName used in the order articles exactly. ' +
63
+ 'MULTI-QUANTITY: if a cabinet was added with quantity > 1, the server expanded it into individual ' +
64
+ 'articles with incrementing suffixes (e.g. "DR_B2_01" qty=3 → "DR_B2_01", "DR_B2_02", "DR_B2_03"). ' +
65
+ 'You must include a separate coordinate entry for EACH expanded name — one entry for "DR_B2_01" ' +
66
+ 'alone does not set coordinates for "DR_B2_02" or "DR_B2_03".'),
67
+ x: zod_1.z.number().describe('Center X coordinate in decimal inches. Absolute room coordinate: 0 = west wall face, room_width = east wall face.\n' +
68
+ 'North/South wall (rotation=0/180): variable x — position along the wall.\n' +
69
+ ' x = D_adj_west + (sum of widths of cabinets to the LEFT on this wall + any filler) + this_cabinet_width / 2.\n' +
70
+ ' D_adj_west = depth of west-wall cabinets if west wall cabinets exist in this layout, else 0.\n' +
71
+ ' CORNER CLEARANCE: north/south wall cabinets must start at x=D_adj_west, NOT x=0.\n' +
72
+ ' SCOPE: D_adj_west applies ONLY to north/south wall x — it does NOT affect east/west wall x.\n' +
73
+ 'West wall (rotation=270): back-edge based — x is the BACK-EDGE coordinate (distance from west wall to cabinet BACK).\n' +
74
+ ' Base cabinet: x = 0 (back edge against west wall).\n' +
75
+ ' Shallower cabinet (e.g. upper) front-flush with base: x = base_depth - upper_depth.\n' +
76
+ ' Example: 24" base + 15" upper → x = 24 - 15 = 9".\n' +
77
+ ' Verification: base front edge at 0+24=24", upper front edge at 9+15=24" ✅ MATCH.\n' +
78
+ 'East wall (rotation=90): back-edge based — x is the BACK-EDGE coordinate (distance from west wall to cabinet BACK).\n' +
79
+ ' Base cabinet: x = room_width (back edge against east wall). Requires room_width from drawing.\n' +
80
+ ' Shallower cabinet front-flush: x = base_x - (base_depth - upper_depth).\n' +
81
+ ' Example: base x=120 (24" deep) + 15" upper → upper x = 120 - 9 = 111".\n' +
82
+ ' Verification: base front at 120-24=96", upper front at 111-15=96" ✅ MATCH.\n' +
83
+ ' CRITICAL: Upper x = base_x - (base_depth - upper_depth). Just offset from base — no room dimension math.\n' +
84
+ ' IF room_width IS UNKNOWN: ask the user for room dimensions from the drawing to set base x.\n' +
85
+ 'Values from get_cabinet_coordinates are already center X; pass directly for round-trip operations.'),
86
+ y: zod_1.z.number().describe('Back-edge Y coordinate in decimal inches. Absolute room coordinate: 0 = south wall face, room_depth = north wall face.\n' +
87
+ 'CRITICAL: Y is the BACK-EDGE coordinate (distance from south wall face to cabinet BACK), NOT center.\n' +
88
+ 'South wall (rotation=180): back-edge based — y is distance from south wall face to cabinet BACK.\n' +
89
+ ' Base cabinet: y = 0 (back edge against south wall).\n' +
90
+ ' Shallower cabinet (e.g. upper) front-flush with base: y = base_depth - upper_depth.\n' +
91
+ ' Example: 24" base + 15" upper → y = 24 - 15 = 9". Front edges align at 24".\n' +
92
+ 'North wall (rotation=0): back-edge based — y is distance from south wall face to cabinet BACK.\n' +
93
+ ' Base cabinet: y = room_depth (back edge against north wall). Requires room_depth from drawing.\n' +
94
+ ' Shallower cabinet front-flush: y = base_y - (base_depth - upper_depth).\n' +
95
+ ' Example: base y=120 (24" deep) + 15" upper → upper y = 120 - 9 = 111".\n' +
96
+ ' Verification: base front at 120-24=96", upper front at 111-15=96" ✅ MATCH.\n' +
97
+ ' CRITICAL: Upper y = base_y - (base_depth - upper_depth). Just offset from base — no room dimension math.\n' +
98
+ ' IF room_depth IS UNKNOWN: ask the user for room dimensions from the drawing to set base y.\n' +
99
+ 'East wall (rotation=90): variable y — position along the wall (north-south).\n' +
100
+ ' "Width" = cabinet\'s own width dimension, running north-south after rotation.\n' +
101
+ ' East/west wall cabinets run from the north wall SOUTHWARD (decreasing y). List them north-to-south.\n' +
102
+ ' D_adj_north = the catalog DEPTH of the north-wall BASE cabinet (the "depth" used in north wall y = room_depth).\n' +
103
+ ' Example: north wall has 24"-deep base cabinets → D_adj_north = 24.\n' +
104
+ ' NEVER set D_adj_north = 0 when north wall cabinets exist — that places east/west cabinet inside the north wall footprint.\n' +
105
+ ' Northernmost cabinet (1st in list): y = room_depth - D_adj_north - this_width / 2.\n' +
106
+ ' Each next cabinet going south: subtract all preceding widths.\n' +
107
+ ' General: y = room_depth - D_adj_north - (sum of widths of all cabinets listed before this one) - this_width / 2.\n' +
108
+ ' y DECREASES with each cabinet going south. NEVER add widths — adding goes north (wrong direction).\n' +
109
+ 'West wall (rotation=270): y = same formula as east wall.\n' +
110
+ 'EAST/WEST UPPER CABINETS: DO NOT compute y for uppers — copy the base cabinet y value exactly. No formula. No D_adj_north.\n' +
111
+ 'Front-flush on east/west walls is achieved by changing X only — y is unchanged from the base.\n' +
112
+ 'Do NOT apply the north/south front-flush y formulas to east/west wall cabinets.'),
113
+ z: zod_1.z.number().describe('Height from floor in decimal inches. ' +
114
+ '0 for base cabinets sitting on the floor. ' +
115
+ 'Stacked units: sum of the heights of all cabinets below this one.'),
116
+ rotation: zod_1.z.number().describe('Identifies which wall this cabinet is on and its orientation in CAD. ' +
117
+ '0 = north wall, 90 = east wall, 180 = south wall, 270 = west wall. ' +
118
+ 'If the drawing has no compass rose, assume top = north, bottom = south, left = west, right = east. ' +
119
+ 'All cabinets on the same wall share the same rotation value. ' +
120
+ 'Default 0 for single-wall or north-wall installations.'),
121
+ });
122
+ const UpdateCabinetCoordinatesSchema = zod_1.z.object({
123
+ orderId: zod_1.z.string().describe('Order ID or saved cart tempOrderId whose coordinates will be replaced. ' +
124
+ 'Accepts both placed order IDs (e.g. "307104_v2") and saved cart reference IDs (e.g. "623265"). ' +
125
+ 'Use the saved cart tempOrderId to set coordinates on a cart BEFORE submitting — ' +
126
+ 'this is the correct workflow so coordinates are passed directly to the XML at submit time.'),
127
+ coordinates: zod_1.z.array(CoordinateItemSchema).describe('Full replacement list of cabinet coordinates. ALL existing coordinates for this order or cart are deleted ' +
128
+ 'and replaced with this list. Omit a cabinet from this list to remove its stored coordinates.'),
129
+ });
130
+ const PatchCabinetCoordinatesSchema = zod_1.z.object({
131
+ orderId: zod_1.z.string().describe('Order ID or saved cart tempOrderId whose coordinates will be partially updated. ' +
132
+ 'Accepts both placed order IDs (e.g. "307104_v2") and saved cart reference IDs (e.g. "623265").'),
133
+ coordinates: zod_1.z.array(CoordinateItemSchema).min(1).describe('Partial list of cabinet coordinates to update. Only the positionNames included here are affected — ' +
134
+ 'all other cabinets keep their existing coordinates unchanged. ' +
135
+ 'Use this tool instead of update_cabinet_coordinates when you need to move or reposition one or a few ' +
136
+ 'cabinets without disturbing the rest of the layout.'),
137
+ });
138
+ const UploadCanvasBackgroundSchema = zod_1.z.object({
139
+ orderId: zod_1.z.string().describe('Order ID whose canvas background image will be replaced.'),
140
+ canvasType: zod_1.z.enum(['plan', 'north', 'south', 'east', 'west']).describe('Canvas type to upload the background image for. ' +
141
+ '"plan" is the floor plan view; "north", "south", "east", "west" are elevation views.'),
142
+ imagePath: zod_1.z.string().describe('Absolute path to the image file on the local filesystem. ' +
143
+ 'Supported formats: .png, .jpg / .jpeg, .webp. ' +
144
+ 'The MCP server reads and encodes the file — do not convert it yourself. ' +
145
+ 'The existing background image for this canvas type is automatically deleted from S3 and replaced.'),
146
+ });
147
+ const CanvasMetadataFieldsSchema = zod_1.z.object({
148
+ orderId: zod_1.z.string().describe('Order ID whose canvas metadata will be replaced.'),
149
+ canvasMeta: zod_1.z.record(zod_1.z.unknown()).optional().describe('Per-canvas display metadata keyed by canvas type (plan, north, south, east, west). ' +
150
+ 'Each entry contains scale (ratio, unit), position (x, y), rotation, zoom, and backgroundImage (S3 URL).'),
151
+ z0References: zod_1.z.record(zod_1.z.unknown()).optional().describe('Floor reference line calibration per elevation canvas. ' +
152
+ 'Each entry contains imagePercentFromBottom, originalImageDimensions, realWorldHeight, and timestamp.'),
153
+ verticalCalibration: zod_1.z.record(zod_1.z.unknown()).optional().describe('Vertical height mapping calibration per elevation canvas. ' +
154
+ 'Each entry contains scale, offset, unit, and timestamp.'),
155
+ elevationVisibility: zod_1.z.record(zod_1.z.unknown()).optional().describe('Item visibility per elevation, keyed by itemId. ' +
156
+ 'Each entry is a map of elevation name to boolean (e.g. {"north": true, "south": false}).'),
157
+ calibrationOffsets: zod_1.z.record(zod_1.z.unknown()).optional().describe('Coordinate positioning offsets per canvas type. ' +
158
+ 'Each entry contains x and y offset values in pixels.'),
159
+ itemRotations: zod_1.z.record(zod_1.z.unknown()).optional().describe('Item-specific rotation values keyed by itemId. Values are rotation in degrees.'),
160
+ activeCanvases: zod_1.z.array(zod_1.z.string()).optional().describe('List of active canvas types. Valid values: "plan", "north", "south", "east", "west".'),
161
+ fixedElevationPositions: zod_1.z.record(zod_1.z.unknown()).optional().describe('Pinned cabinet positions after global calibration, keyed by elevation then positionName. ' +
162
+ 'Each position entry contains x and y pixel coordinates.'),
163
+ elevationCalibrated: zod_1.z.record(zod_1.z.unknown()).optional().describe('Global calibration status per elevation. Keys are elevation names, values are booleans.'),
164
+ globalElevationOffsets: zod_1.z.record(zod_1.z.unknown()).optional().describe('Coordinate system transformations per elevation (maps plan coordinates to elevation coordinates). ' +
165
+ 'Each entry contains x and y offset values in inches.'),
166
+ calibratedCabinets: zod_1.z.record(zod_1.z.unknown()).optional().describe('Which cabinets have been globally calibrated per elevation. ' +
167
+ 'Keyed by elevation then positionName, values are booleans.'),
168
+ });
169
+ // ---------------------------------------------------------------------------
170
+ // Handlers
171
+ // ---------------------------------------------------------------------------
172
+ async function getDrawingToolState(input) {
173
+ try {
174
+ const { data } = await api_client_1.client.get(`/canvas/${input.orderId}`);
175
+ return JSON.stringify(data, null, 2);
176
+ }
177
+ catch (error) {
178
+ try {
179
+ (0, api_client_1.handleAxiosError)(error);
180
+ }
181
+ catch (e) {
182
+ return e.message;
183
+ }
184
+ return 'Unexpected error fetching drawing tool state.';
185
+ }
186
+ }
187
+ async function getCanvasMetadata(input) {
188
+ try {
189
+ const { data } = await api_client_1.client.get(`/canvas/${input.orderId}/metadata`);
190
+ return JSON.stringify(data, null, 2);
191
+ }
192
+ catch (error) {
193
+ try {
194
+ (0, api_client_1.handleAxiosError)(error);
195
+ }
196
+ catch (e) {
197
+ return e.message;
198
+ }
199
+ return 'Unexpected error fetching canvas metadata.';
200
+ }
201
+ }
202
+ async function updateCanvasMetadata(input) {
203
+ const { orderId, ...fields } = input;
204
+ try {
205
+ const { data } = await api_client_1.client.put(`/canvas/${orderId}/metadata`, fields);
206
+ return JSON.stringify(data, null, 2);
207
+ }
208
+ catch (error) {
209
+ try {
210
+ (0, api_client_1.handleAxiosError)(error);
211
+ }
212
+ catch (e) {
213
+ return e.message;
214
+ }
215
+ return 'Unexpected error updating canvas metadata.';
216
+ }
217
+ }
218
+ async function getCanvasImages(input) {
219
+ try {
220
+ const { data } = await api_client_1.client.get(`/canvas/${input.orderId}/images`);
221
+ return JSON.stringify(data, null, 2);
222
+ }
223
+ catch (error) {
224
+ try {
225
+ (0, api_client_1.handleAxiosError)(error);
226
+ }
227
+ catch (e) {
228
+ return e.message;
229
+ }
230
+ return 'Unexpected error fetching canvas images.';
231
+ }
232
+ }
233
+ async function getCabinetCoordinates(input) {
234
+ try {
235
+ const { data } = await api_client_1.client.get(`/canvas/${input.orderId}/coordinates`);
236
+ return JSON.stringify(data, null, 2);
237
+ }
238
+ catch (error) {
239
+ try {
240
+ (0, api_client_1.handleAxiosError)(error);
241
+ }
242
+ catch (e) {
243
+ return e.message;
244
+ }
245
+ return 'Unexpected error fetching cabinet coordinates.';
246
+ }
247
+ }
248
+ async function updateCabinetCoordinates(input) {
249
+ try {
250
+ const { data } = await api_client_1.client.put(`/canvas/${input.orderId}/coordinates`, input.coordinates);
251
+ return JSON.stringify(data, null, 2);
252
+ }
253
+ catch (error) {
254
+ try {
255
+ (0, api_client_1.handleAxiosError)(error);
256
+ }
257
+ catch (e) {
258
+ return e.message;
259
+ }
260
+ return 'Unexpected error updating cabinet coordinates.';
261
+ }
262
+ }
263
+ async function patchCabinetCoordinates(input) {
264
+ try {
265
+ const { data } = await api_client_1.client.patch(`/canvas/${input.orderId}/coordinates`, input.coordinates);
266
+ return JSON.stringify(data, null, 2);
267
+ }
268
+ catch (error) {
269
+ try {
270
+ (0, api_client_1.handleAxiosError)(error);
271
+ }
272
+ catch (e) {
273
+ return e.message;
274
+ }
275
+ return 'Unexpected error patching cabinet coordinates.';
276
+ }
277
+ }
278
+ const MIME_TYPES = {
279
+ '.png': 'image/png',
280
+ '.jpg': 'image/jpeg',
281
+ '.jpeg': 'image/jpeg',
282
+ '.webp': 'image/webp',
283
+ };
284
+ async function uploadCanvasBackground(input) {
285
+ const ext = path.extname(input.imagePath).toLowerCase();
286
+ const mimeType = MIME_TYPES[ext];
287
+ if (!mimeType) {
288
+ return `Unsupported file extension "${ext}". Supported: .png, .jpg, .jpeg, .webp`;
289
+ }
290
+ if (!fs.existsSync(input.imagePath)) {
291
+ return `File not found at "${input.imagePath}"`;
292
+ }
293
+ const form = new form_data_1.default();
294
+ form.append('file', fs.createReadStream(input.imagePath), {
295
+ filename: path.basename(input.imagePath),
296
+ contentType: mimeType,
297
+ });
298
+ try {
299
+ const { data } = await api_client_1.client.post(`/canvas/${input.orderId}/images/${input.canvasType}/background`, form, { headers: form.getHeaders() });
300
+ return JSON.stringify(data, null, 2);
301
+ }
302
+ catch (error) {
303
+ try {
304
+ (0, api_client_1.handleAxiosError)(error);
305
+ }
306
+ catch (e) {
307
+ return e.message;
308
+ }
309
+ return 'Unexpected error uploading canvas background image.';
310
+ }
311
+ }
312
+ // ---------------------------------------------------------------------------
313
+ // Exports
314
+ // ---------------------------------------------------------------------------
315
+ exports.canvasTools = [
316
+ {
317
+ name: 'get_drawing_tool_state',
318
+ description: 'Retrieve the full drawing tool state for an order in a single call. ' +
319
+ 'Returns three sections: ' +
320
+ '(1) metadata — canvas scale, calibration, Z0 floor references, elevation visibility, active canvases, and all other drawing tool state; ' +
321
+ '(2) images — S3 keys for background and snapshot images per canvas type (plan, north, south, east, west); ' +
322
+ '(3) coordinates — all cabinet positions (center X, back-edge Y, Z height, rotation) keyed by positionName. ' +
323
+ 'Returns 404 if no drawing tool state exists for this order.',
324
+ inputSchema: GetDrawingToolStateSchema,
325
+ handler: getDrawingToolState,
326
+ },
327
+ {
328
+ name: 'get_canvas_metadata',
329
+ description: 'Retrieve canvas metadata for an order. ' +
330
+ 'Includes: activeCanvases, canvasMeta (scale/position/zoom per canvas), z0References (floor calibration per elevation), ' +
331
+ 'verticalCalibration, elevationVisibility (which cabinets are visible on which elevations), ' +
332
+ 'calibrationOffsets, itemRotations, fixedElevationPositions, elevationCalibrated, ' +
333
+ 'globalElevationOffsets, and calibratedCabinets. ' +
334
+ 'Use get_drawing_tool_state if you also need images and coordinates.',
335
+ inputSchema: GetCanvasMetadataSchema,
336
+ handler: getCanvasMetadata,
337
+ },
338
+ {
339
+ name: 'update_canvas_metadata',
340
+ description: 'Replace all canvas metadata fields for an order. ' +
341
+ 'All existing metadata is overwritten with the provided values. ' +
342
+ 'Fields not included in the request are set to null — provide all fields you want to preserve. ' +
343
+ 'Use get_canvas_metadata first to read the current state before making targeted updates.',
344
+ inputSchema: CanvasMetadataFieldsSchema,
345
+ handler: updateCanvasMetadata,
346
+ },
347
+ {
348
+ name: 'get_canvas_images',
349
+ description: 'Retrieve canvas image records for an order. ' +
350
+ 'Returns a list of entries each containing canvasType (plan/north/south/east/west), ' +
351
+ 'imageType (background/snapshot), and s3Key (the S3 path to the image file). ' +
352
+ 'Background images are the floor plan or elevation photos used for cabinet placement. ' +
353
+ 'Snapshot images are rendered exports of the canvas used in order submission.',
354
+ inputSchema: GetCanvasImagesSchema,
355
+ handler: getCanvasImages,
356
+ },
357
+ {
358
+ name: 'get_cabinet_coordinates',
359
+ description: 'Retrieve all stored cabinet coordinates for an order. ' +
360
+ 'Each entry contains positionName, x (center X in inches), y (back-edge Y / depth-direction in inches), ' +
361
+ 'z (height from floor in inches), and rotation (degrees). ' +
362
+ 'Note: x is center X, not bottom-left. Y is back-edge coordinate (distance from south wall to cabinet BACK). ' +
363
+ 'Use these values directly in update_cabinet_coordinates for round-trip operations.',
364
+ inputSchema: GetCabinetCoordinatesSchema,
365
+ handler: getCabinetCoordinates,
366
+ },
367
+ {
368
+ name: 'update_cabinet_coordinates',
369
+ description: 'Replace all cabinet coordinates for a placed order OR a saved cart. ' +
370
+ 'Accepts both placed order IDs and saved cart tempOrderIds. ' +
371
+ '\n\n' +
372
+ 'CORRECT WORKFLOW FOR SAVED CARTS:\n' +
373
+ '1. create_edit_cart (or use existing saved cart tempOrderId)\n' +
374
+ '2. update_cabinet_coordinates(tempOrderId, [...]) ← call this tool with the cart tempOrderId\n' +
375
+ '3. submit_cart_as_version — coordinates stored on the cart are picked up and written to the XML\n' +
376
+ 'Do NOT submit first and update coordinates after — that requires a second redundant version.\n' +
377
+ '\n' +
378
+ 'MULTI-QUANTITY ARTICLES: When a cabinet was added with quantity > 1, the server expanded it into\n' +
379
+ 'individual articles with incrementing suffixes. Example: positionName "DR_B2_01" with quantity=3\n' +
380
+ 'produces three separate cabinets: "DR_B2_01", "DR_B2_02", "DR_B2_03". This tool does NOT\n' +
381
+ 'auto-expand — you must list every expanded name explicitly. Setting coordinates only for\n' +
382
+ '"DR_B2_01" leaves "DR_B2_02" and "DR_B2_03" without coordinates in the CAD file.\n' +
383
+ 'Rule: for each article with quantity=N, include N entries — one per expanded positionName.\n' +
384
+ '\n' +
385
+ 'All existing coordinates are deleted and replaced with the provided list.\n' +
386
+ 'x = back-edge X in inches:\n' +
387
+ ' North/South wall: x = D_adj_west + cumulative_widths_left + this_width/2 (position along wall). D_adj_west = west wall cabinet depth, else 0.\n' +
388
+ ' D_adj_west applies ONLY to north/south wall x — NEVER to east/west wall x.\n' +
389
+ ' West wall — base: x = 0 (back against west wall). Upper front-flush: x = base_depth - upper_depth.\n' +
390
+ ' East wall — base: x = room_width (back against east wall). Upper front-flush: x = base_x - (base_depth - upper_depth).\n' +
391
+ ' CRITICAL: For upper cabinets, use x = base_x - (base_depth - upper_depth). For west wall where base_x=0, simplifies to x = base_depth - upper_depth.\n' +
392
+ 'y = back-edge Y in inches — ABSOLUTE room coordinate (0 = south wall face, room_depth = north wall face):\n' +
393
+ ' South wall — base: y = 0 (back against south wall). Upper front-flush: y = base_depth - upper_depth.\n' +
394
+ ' North wall — base: y = room_depth (back against north wall). Upper front-flush: y = base_y - (base_depth - upper_depth).\n' +
395
+ ' CRITICAL: For upper cabinets, use y = base_y - (base_depth - upper_depth). For south wall where base_y=0, simplifies to y = base_depth - upper_depth.\n' +
396
+ ' East/West wall: cabinets run north-to-south (decreasing y). List them northernmost first.\n' +
397
+ ' D_adj_north = north wall cabinet depth if present, else 0.\n' +
398
+ ' Northernmost cabinet: y = room_depth - D_adj_north - this_width/2.\n' +
399
+ ' Each next going south: y = room_depth - D_adj_north - (sum of widths of all preceding cabinets) - this_width/2.\n' +
400
+ ' y DECREASES going south. NEVER add widths — adding goes north (wrong direction).\n' +
401
+ ' UPPER cabinets: y = same as the base cabinet at that position. Front-flush = x change only, NOT y.\n' +
402
+ ' Do NOT apply north/south front-flush y formulas to east/west wall cabinets.\n' +
403
+ 'z = height from floor in inches (0 for base cabinets on the floor).\n' +
404
+ 'rotation: 0=north wall, 90=east wall, 180=south wall, 270=west wall.\n' +
405
+ 'To remove a cabinet\'s coordinates, omit it from the list.\n' +
406
+ 'Call get_cabinet_coordinates first to read current positions before making targeted changes.',
407
+ inputSchema: UpdateCabinetCoordinatesSchema,
408
+ handler: updateCabinetCoordinates,
409
+ },
410
+ {
411
+ name: 'patch_cabinet_coordinates',
412
+ description: 'Partially update cabinet coordinates for specific cabinets only. ' +
413
+ 'Only the positionNames you include are affected — all other cabinets keep their existing coordinates unchanged. ' +
414
+ '\n\n' +
415
+ 'Use this tool when:\n' +
416
+ '- You need to move or reposition one or a few cabinets without disturbing the rest of the layout\n' +
417
+ '- The user says "move cabinet X to position Y" or "adjust the coordinates for Base_01"\n' +
418
+ '\n' +
419
+ 'Use update_cabinet_coordinates (PUT) instead when:\n' +
420
+ '- You are setting coordinates for the first time on a new cart or order (full replacement is correct)\n' +
421
+ '- You want to remove a cabinet\'s coordinates (omit it from the full replacement list)\n' +
422
+ '\n' +
423
+ 'The response includes "patched" (count of positions you sent), "total" (all coordinates now stored), ' +
424
+ 'and the full updated coordinate list is broadcast to the frontend in real time via WebSocket.\n' +
425
+ '\n' +
426
+ 'MULTI-QUANTITY ARTICLES: Same rule as update_cabinet_coordinates — if a cabinet was expanded server-side ' +
427
+ 'from qty>1 (e.g. "DR_B2_01" qty=3 → "DR_B2_01", "DR_B2_02", "DR_B2_03"), each expanded name is a ' +
428
+ 'separate entry. To move all three, include all three in the coordinates list.',
429
+ inputSchema: PatchCabinetCoordinatesSchema,
430
+ handler: patchCabinetCoordinates,
431
+ },
432
+ {
433
+ name: 'upload_canvas_background_image',
434
+ description: 'Upload a background image for a specific canvas (plan or elevation view) from a local file path. ' +
435
+ 'Provide the absolute path to the image file — the MCP server reads and encodes it automatically. ' +
436
+ 'Supported formats: .png, .jpg / .jpeg, .webp. ' +
437
+ 'The existing background image for this canvas type is automatically removed from S3 and replaced. ' +
438
+ 'Canvas defaults (scale ratio 1, zoom 1, position 0/0) are initialized automatically for the uploaded ' +
439
+ 'canvas type. The canvas is added to activeCanvases so the user sees it when they open the order — ' +
440
+ 'no separate initialization step is needed. Call this once per canvas type you want to populate. ' +
441
+ 'After upload the drawing tool is notified in real time via WebSocket. ' +
442
+ 'Use get_canvas_images to check the current image before replacing.',
443
+ inputSchema: UploadCanvasBackgroundSchema,
444
+ handler: uploadCanvasBackground,
445
+ },
446
+ ];
@@ -0,0 +1,95 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.catalogTools = void 0;
4
+ exports.searchCatalog = searchCatalog;
5
+ exports.getArticle = getArticle;
6
+ exports.listFilterTags = listFilterTags;
7
+ const zod_1 = require("zod");
8
+ const api_client_1 = require("../client/api-client");
9
+ // --- Schemas ---
10
+ const SearchSchema = zod_1.z.object({
11
+ q: zod_1.z.string().optional().describe('Keyword search query'),
12
+ tags: zod_1.z.array(zod_1.z.string()).optional().describe('Filter by tag (e.g. BASE, WALL, PANTRY)'),
13
+ minH: zod_1.z.number().optional().describe('Minimum height in inches'),
14
+ maxH: zod_1.z.number().optional().describe('Maximum height in inches'),
15
+ minW: zod_1.z.number().optional().describe('Minimum width in inches'),
16
+ maxW: zod_1.z.number().optional().describe('Maximum width in inches'),
17
+ minD: zod_1.z.number().optional().describe('Minimum depth in inches'),
18
+ maxD: zod_1.z.number().optional().describe('Maximum depth in inches'),
19
+ });
20
+ const GetArticleSchema = zod_1.z.object({
21
+ serialNumber: zod_1.z.string().describe('Article serial number, e.g. B30'),
22
+ });
23
+ const ListFilterTagsSchema = zod_1.z.object({});
24
+ // --- Handlers ---
25
+ async function searchCatalog(input) {
26
+ try {
27
+ const { data } = await api_client_1.client.get('/catalog/search', {
28
+ params: { ...input },
29
+ paramsSerializer: { indexes: null }, // produces tags=BASE&tags=WALL (not tags[]=)
30
+ });
31
+ if (!data || data.length === 0)
32
+ return 'No articles found matching your search.';
33
+ return JSON.stringify(data, null, 2);
34
+ }
35
+ catch (error) {
36
+ try {
37
+ (0, api_client_1.handleAxiosError)(error);
38
+ }
39
+ catch (e) {
40
+ return e.message;
41
+ }
42
+ return 'Unexpected error searching catalog.';
43
+ }
44
+ }
45
+ async function getArticle(input) {
46
+ try {
47
+ const { data } = await api_client_1.client.get(`/catalog/${input.serialNumber}`);
48
+ return JSON.stringify(data, null, 2);
49
+ }
50
+ catch (error) {
51
+ try {
52
+ (0, api_client_1.handleAxiosError)(error);
53
+ }
54
+ catch (e) {
55
+ return e.message;
56
+ }
57
+ return 'Unexpected error fetching article.';
58
+ }
59
+ }
60
+ async function listFilterTags(_input) {
61
+ try {
62
+ const { data } = await api_client_1.client.get('/catalog/filter-tags');
63
+ return data.join('\n');
64
+ }
65
+ catch (error) {
66
+ try {
67
+ (0, api_client_1.handleAxiosError)(error);
68
+ }
69
+ catch (e) {
70
+ return e.message;
71
+ }
72
+ return 'Unexpected error fetching filter tags.';
73
+ }
74
+ }
75
+ // --- Tool definitions (consumed by index.ts) ---
76
+ exports.catalogTools = [
77
+ {
78
+ name: 'search_catalog',
79
+ description: 'Search the Sealab cabinetry catalog by keyword, filter tags, or dimension ranges. Returns matching articles with serial numbers, dimensions, and feature flags.',
80
+ inputSchema: SearchSchema,
81
+ handler: searchCatalog,
82
+ },
83
+ {
84
+ name: 'get_article',
85
+ description: 'Get full details for a single cabinet article by its serial number.',
86
+ inputSchema: GetArticleSchema,
87
+ handler: getArticle,
88
+ },
89
+ {
90
+ name: 'list_filter_tags',
91
+ description: 'List all available filter tag categories in the catalog (e.g. BASE, WALL, PANTRY, TALL).',
92
+ inputSchema: ListFilterTagsSchema,
93
+ handler: listFilterTags,
94
+ },
95
+ ];