@sealab/mcp-server 1.0.2 → 1.0.4
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/dist/client/api-client.js +8 -1
- package/dist/index.js +6 -2
- package/dist/schema-normalizer.js +74 -0
- package/dist/tools/ai-drawing-overlay.js +4 -0
- package/dist/tools/ai-drawing-session-review-renderer.js +964 -0
- package/dist/tools/ai-drawing.js +2642 -0
- package/dist/tools/cart-cross-check.js +182 -0
- package/dist/tools/elevation-run-segmentation.js +99 -0
- package/dist/tools/orders.js +6 -4
- package/dist/tools/placement-projection.js +119 -0
- package/dist/tools/projects.js +368 -0
- package/package.json +5 -3
- package/src/client/api-client.ts +15 -7
- package/src/index.ts +6 -2
- package/src/schema-normalizer.test.ts +107 -0
- package/src/schema-normalizer.ts +86 -0
- package/src/tools/ai-drawing-overlay.test.ts +9 -0
- package/src/tools/ai-drawing-overlay.ts +8 -0
- package/src/tools/ai-drawing-session-review-renderer.test.ts +516 -0
- package/src/tools/ai-drawing-session-review-renderer.ts +1297 -0
- package/src/tools/ai-drawing.test.ts +3481 -0
- package/src/tools/ai-drawing.ts +3159 -0
- package/src/tools/cart-cross-check.test.ts +84 -0
- package/src/tools/cart-cross-check.ts +267 -0
- package/src/tools/elevation-run-segmentation.test.ts +64 -0
- package/src/tools/elevation-run-segmentation.ts +175 -0
- package/src/tools/orders.ts +8 -4
- package/src/tools/placement-projection.test.ts +185 -0
- package/src/tools/placement-projection.ts +247 -0
- package/src/tools/projects.test.ts +135 -0
- package/src/tools/projects.ts +370 -0
|
@@ -0,0 +1,368 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.projectTools = void 0;
|
|
4
|
+
exports.listProjects = listProjects;
|
|
5
|
+
exports.getProject = getProject;
|
|
6
|
+
exports.createProject = createProject;
|
|
7
|
+
exports.updateProject = updateProject;
|
|
8
|
+
exports.deleteProject = deleteProject;
|
|
9
|
+
exports.attachOrderToProject = attachOrderToProject;
|
|
10
|
+
exports.detachOrderFromProject = detachOrderFromProject;
|
|
11
|
+
exports.updateProjectOrderQuantity = updateProjectOrderQuantity;
|
|
12
|
+
exports.updateProjectOrderOptions = updateProjectOrderOptions;
|
|
13
|
+
exports.updateProjectArticleMaterial = updateProjectArticleMaterial;
|
|
14
|
+
exports.updateProjectArticleMaterials = updateProjectArticleMaterials;
|
|
15
|
+
exports.shareProjectAccess = shareProjectAccess;
|
|
16
|
+
exports.updateProjectAccess = updateProjectAccess;
|
|
17
|
+
exports.revokeProjectAccess = revokeProjectAccess;
|
|
18
|
+
exports.listProjectAccess = listProjectAccess;
|
|
19
|
+
const zod_1 = require("zod");
|
|
20
|
+
const api_client_1 = require("../client/api-client");
|
|
21
|
+
const ProjectStatusSchema = zod_1.z.enum(['Active', 'On Hold', 'Complete']);
|
|
22
|
+
const PermissionTypeSchema = zod_1.z.enum(['VIEW', 'EDIT', 'ADMIN']);
|
|
23
|
+
const MaterialFieldSchema = zod_1.z.enum(['caseMaterial', 'frontMaterial', 'backPanelMaterial', 'innerCaseMaterial']);
|
|
24
|
+
const FinishingTypeSchema = zod_1.z.enum(['Matte', 'Satin', 'Primed']);
|
|
25
|
+
const ListProjectsSchema = zod_1.z.object({});
|
|
26
|
+
const GetProjectSchema = zod_1.z.object({
|
|
27
|
+
projectId: zod_1.z.number().int().positive().describe('Project ID. Use list_projects first if unknown.'),
|
|
28
|
+
});
|
|
29
|
+
const ProjectInputSchema = zod_1.z.object({
|
|
30
|
+
name: zod_1.z.string().min(1).describe('Project name.'),
|
|
31
|
+
address1: zod_1.z.string().optional().describe('Project address line 1.'),
|
|
32
|
+
address2: zod_1.z.string().optional().describe('Project address line 2.'),
|
|
33
|
+
city: zod_1.z.string().optional().describe('Project city.'),
|
|
34
|
+
state: zod_1.z.string().optional().describe('Project state.'),
|
|
35
|
+
zipcode: zod_1.z.string().optional().describe('Project ZIP/postal code.'),
|
|
36
|
+
status: ProjectStatusSchema.default('Active').describe('Project status.'),
|
|
37
|
+
phase: zod_1.z.string().optional().describe('Project phase, e.g. Design, Shop Drawings, Install.'),
|
|
38
|
+
clientName: zod_1.z.string().optional().describe('Client or end-customer name.'),
|
|
39
|
+
budget: zod_1.z.number().optional().describe('Optional project budget.'),
|
|
40
|
+
dueDate: zod_1.z.string().optional().describe('Optional due date in YYYY-MM-DD format.'),
|
|
41
|
+
taxExempt: zod_1.z.boolean().optional().describe('Whether project summary pricing should display without sales tax.'),
|
|
42
|
+
notes: zod_1.z.string().optional().describe('Project notes.'),
|
|
43
|
+
});
|
|
44
|
+
const CreateProjectSchema = ProjectInputSchema;
|
|
45
|
+
const UpdateProjectSchema = ProjectInputSchema.partial().extend({
|
|
46
|
+
projectId: zod_1.z.number().int().positive().describe('Project ID to update.'),
|
|
47
|
+
});
|
|
48
|
+
const DeleteProjectSchema = zod_1.z.object({
|
|
49
|
+
projectId: zod_1.z.number().int().positive().describe('Project ID to delete.'),
|
|
50
|
+
});
|
|
51
|
+
const ProjectOrderSchema = zod_1.z.object({
|
|
52
|
+
projectId: zod_1.z.number().int().positive().describe('Project ID.'),
|
|
53
|
+
orderId: zod_1.z.string().describe('Order ID to attach or detach.'),
|
|
54
|
+
});
|
|
55
|
+
const UpdateProjectOrderQuantitySchema = ProjectOrderSchema.extend({
|
|
56
|
+
quantity: zod_1.z.number().int().min(1).describe('Project quantity multiplier for this order. Does not change the order itself.'),
|
|
57
|
+
});
|
|
58
|
+
const UpdateProjectOrderOptionsSchema = ProjectOrderSchema.extend({
|
|
59
|
+
includeDrawerboxes: zod_1.z.boolean().optional().describe('Include drawer boxes for this order. Omit to leave unchanged.'),
|
|
60
|
+
includeAssembly: zod_1.z.boolean().optional().describe('Include assembly for this order. Omit to leave unchanged.'),
|
|
61
|
+
includeHardware: zod_1.z.boolean().optional().describe('Include hardware for this order. Omit to leave unchanged.'),
|
|
62
|
+
includeFinishing: zod_1.z.boolean().optional().describe('Include finishing for this order. Omit to leave unchanged.'),
|
|
63
|
+
finishingType: FinishingTypeSchema.optional().describe('Finishing type. Required when setting includeFinishing to true.'),
|
|
64
|
+
finishingColor: zod_1.z.string().optional().describe('Finishing color. Required when setting includeFinishing to true.'),
|
|
65
|
+
});
|
|
66
|
+
const UpdateProjectArticleMaterialSchema = ProjectOrderSchema.extend({
|
|
67
|
+
positionName: zod_1.z.string().describe('Article position name within the order, e.g. "Base_01".'),
|
|
68
|
+
field: MaterialFieldSchema.describe('Material field to update.'),
|
|
69
|
+
materialDescription: zod_1.z.string().min(1).describe('Exact material description from the Sealab material list.'),
|
|
70
|
+
});
|
|
71
|
+
const UpdateProjectArticleMaterialsSchema = ProjectOrderSchema.extend({
|
|
72
|
+
positionNames: zod_1.z.array(zod_1.z.string()).min(1).describe('Article position names to update.'),
|
|
73
|
+
field: MaterialFieldSchema.describe('Material field to update.'),
|
|
74
|
+
materialDescription: zod_1.z.string().min(1).describe('Exact material description from the Sealab material list.'),
|
|
75
|
+
});
|
|
76
|
+
const ProjectAccessSchema = zod_1.z.object({
|
|
77
|
+
projectId: zod_1.z.number().int().positive().describe('Project ID.'),
|
|
78
|
+
grantedToEmail: zod_1.z.string().email().describe('Email address to grant or update access for.'),
|
|
79
|
+
permissionType: PermissionTypeSchema.default('VIEW').describe('VIEW = read-only, EDIT = can update project/order options, ADMIN = admin-level project permission.'),
|
|
80
|
+
hidePrice: zod_1.z.boolean().default(false).describe('Whether to hide project pricing from this user.'),
|
|
81
|
+
});
|
|
82
|
+
const RevokeProjectAccessSchema = zod_1.z.object({
|
|
83
|
+
projectId: zod_1.z.number().int().positive().describe('Project ID.'),
|
|
84
|
+
email: zod_1.z.string().email().describe('Email address whose project access should be revoked.'),
|
|
85
|
+
});
|
|
86
|
+
const ListProjectAccessSchema = zod_1.z.object({
|
|
87
|
+
projectId: zod_1.z.number().int().positive().describe('Project ID.'),
|
|
88
|
+
});
|
|
89
|
+
function compactProject(project) {
|
|
90
|
+
return {
|
|
91
|
+
projectId: project.projectId,
|
|
92
|
+
name: project.name,
|
|
93
|
+
status: project.status,
|
|
94
|
+
phase: project.phase,
|
|
95
|
+
clientName: project.clientName,
|
|
96
|
+
address: [project.address1, project.address2, project.city, project.state, project.zipcode].filter(Boolean).join(', '),
|
|
97
|
+
shared: project.shared,
|
|
98
|
+
permissionType: project.permissionType,
|
|
99
|
+
hidePrice: project.hidePrice,
|
|
100
|
+
updatedAt: project.updatedAt,
|
|
101
|
+
orderCount: project.orders?.length ?? project.summary?.orderSummaries?.length,
|
|
102
|
+
projectTotalValue: project.hidePrice ? undefined : project.summary?.projectTotalValue,
|
|
103
|
+
};
|
|
104
|
+
}
|
|
105
|
+
function formatProjectList(projects) {
|
|
106
|
+
if (!projects || projects.length === 0)
|
|
107
|
+
return 'No projects found.';
|
|
108
|
+
return JSON.stringify(projects.map(compactProject), null, 2);
|
|
109
|
+
}
|
|
110
|
+
function formatProject(project) {
|
|
111
|
+
return JSON.stringify(project, null, 2);
|
|
112
|
+
}
|
|
113
|
+
async function apiError(error, fallback) {
|
|
114
|
+
try {
|
|
115
|
+
(0, api_client_1.handleAxiosError)(error);
|
|
116
|
+
}
|
|
117
|
+
catch (e) {
|
|
118
|
+
return e.message;
|
|
119
|
+
}
|
|
120
|
+
return fallback;
|
|
121
|
+
}
|
|
122
|
+
async function listProjects(_) {
|
|
123
|
+
try {
|
|
124
|
+
const { data } = await api_client_1.client.get('/projects');
|
|
125
|
+
return formatProjectList(data);
|
|
126
|
+
}
|
|
127
|
+
catch (error) {
|
|
128
|
+
return apiError(error, 'Unexpected error listing projects.');
|
|
129
|
+
}
|
|
130
|
+
}
|
|
131
|
+
async function getProject(input) {
|
|
132
|
+
try {
|
|
133
|
+
const { data } = await api_client_1.client.get(`/projects/${input.projectId}`);
|
|
134
|
+
return formatProject(data);
|
|
135
|
+
}
|
|
136
|
+
catch (error) {
|
|
137
|
+
return apiError(error, 'Unexpected error fetching project.');
|
|
138
|
+
}
|
|
139
|
+
}
|
|
140
|
+
async function createProject(input) {
|
|
141
|
+
try {
|
|
142
|
+
const { data } = await api_client_1.client.post('/projects', input);
|
|
143
|
+
return `Project created successfully. Project ID: ${data.projectId}. Name: ${data.name}.`;
|
|
144
|
+
}
|
|
145
|
+
catch (error) {
|
|
146
|
+
return apiError(error, 'Unexpected error creating project.');
|
|
147
|
+
}
|
|
148
|
+
}
|
|
149
|
+
async function updateProject(input) {
|
|
150
|
+
const { projectId, ...payload } = input;
|
|
151
|
+
try {
|
|
152
|
+
const { data: current } = await api_client_1.client.get(`/projects/${projectId}`);
|
|
153
|
+
const { data } = await api_client_1.client.put(`/projects/${projectId}`, { ...current, ...payload });
|
|
154
|
+
return `Project updated successfully.\n${formatProject(data)}`;
|
|
155
|
+
}
|
|
156
|
+
catch (error) {
|
|
157
|
+
return apiError(error, 'Unexpected error updating project.');
|
|
158
|
+
}
|
|
159
|
+
}
|
|
160
|
+
async function deleteProject(input) {
|
|
161
|
+
try {
|
|
162
|
+
await api_client_1.client.delete(`/projects/${input.projectId}`);
|
|
163
|
+
return `Project ${input.projectId} deleted successfully. Pending attached orders were detached, not deleted.`;
|
|
164
|
+
}
|
|
165
|
+
catch (error) {
|
|
166
|
+
return apiError(error, 'Unexpected error deleting project.');
|
|
167
|
+
}
|
|
168
|
+
}
|
|
169
|
+
async function attachOrderToProject(input) {
|
|
170
|
+
try {
|
|
171
|
+
await api_client_1.client.post(`/projects/${input.projectId}/orders/${input.orderId}`);
|
|
172
|
+
return `Order ${input.orderId} attached to project ${input.projectId}.`;
|
|
173
|
+
}
|
|
174
|
+
catch (error) {
|
|
175
|
+
return apiError(error, 'Unexpected error attaching order to project.');
|
|
176
|
+
}
|
|
177
|
+
}
|
|
178
|
+
async function detachOrderFromProject(input) {
|
|
179
|
+
try {
|
|
180
|
+
await api_client_1.client.delete(`/projects/${input.projectId}/orders/${input.orderId}`);
|
|
181
|
+
return `Order ${input.orderId} detached from project ${input.projectId}.`;
|
|
182
|
+
}
|
|
183
|
+
catch (error) {
|
|
184
|
+
return apiError(error, 'Unexpected error detaching order from project.');
|
|
185
|
+
}
|
|
186
|
+
}
|
|
187
|
+
async function updateProjectOrderQuantity(input) {
|
|
188
|
+
try {
|
|
189
|
+
const { data } = await api_client_1.client.put(`/projects/${input.projectId}/orders/${input.orderId}/quantity`, {
|
|
190
|
+
quantity: input.quantity,
|
|
191
|
+
});
|
|
192
|
+
return `Project order quantity updated.\n${formatProject(data)}`;
|
|
193
|
+
}
|
|
194
|
+
catch (error) {
|
|
195
|
+
return apiError(error, 'Unexpected error updating project order quantity.');
|
|
196
|
+
}
|
|
197
|
+
}
|
|
198
|
+
async function updateProjectOrderOptions(input) {
|
|
199
|
+
const { projectId, orderId, ...payload } = input;
|
|
200
|
+
try {
|
|
201
|
+
const { data } = await api_client_1.client.patch(`/projects/${projectId}/orders/${orderId}/options`, payload);
|
|
202
|
+
return `Project order options updated.\n${JSON.stringify(data, null, 2)}`;
|
|
203
|
+
}
|
|
204
|
+
catch (error) {
|
|
205
|
+
return apiError(error, 'Unexpected error updating project order options.');
|
|
206
|
+
}
|
|
207
|
+
}
|
|
208
|
+
async function updateProjectArticleMaterial(input) {
|
|
209
|
+
try {
|
|
210
|
+
const { data } = await api_client_1.client.patch(`/projects/${input.projectId}/orders/${input.orderId}/articles/${encodeURIComponent(input.positionName)}/materials`, { field: input.field, materialDescription: input.materialDescription });
|
|
211
|
+
return `Project article material updated.\n${JSON.stringify(data, null, 2)}`;
|
|
212
|
+
}
|
|
213
|
+
catch (error) {
|
|
214
|
+
return apiError(error, 'Unexpected error updating project article material.');
|
|
215
|
+
}
|
|
216
|
+
}
|
|
217
|
+
async function updateProjectArticleMaterials(input) {
|
|
218
|
+
try {
|
|
219
|
+
const { data } = await api_client_1.client.patch(`/projects/${input.projectId}/orders/${input.orderId}/articles/materials`, { positionNames: input.positionNames, field: input.field, materialDescription: input.materialDescription });
|
|
220
|
+
return `Project article materials updated.\n${JSON.stringify(data, null, 2)}`;
|
|
221
|
+
}
|
|
222
|
+
catch (error) {
|
|
223
|
+
return apiError(error, 'Unexpected error updating project article materials.');
|
|
224
|
+
}
|
|
225
|
+
}
|
|
226
|
+
async function shareProjectAccess(input) {
|
|
227
|
+
try {
|
|
228
|
+
await api_client_1.client.post(`/projects/${input.projectId}/permissions/grant`, {
|
|
229
|
+
grantedToEmail: input.grantedToEmail,
|
|
230
|
+
permissionType: input.permissionType,
|
|
231
|
+
hidePrice: input.hidePrice,
|
|
232
|
+
});
|
|
233
|
+
const priceNote = input.hidePrice ? ' Pricing will be hidden from them.' : '';
|
|
234
|
+
return `Access granted. ${input.grantedToEmail} now has ${input.permissionType} access to project ${input.projectId}.${priceNote}`;
|
|
235
|
+
}
|
|
236
|
+
catch (error) {
|
|
237
|
+
return apiError(error, 'Unexpected error granting project access.');
|
|
238
|
+
}
|
|
239
|
+
}
|
|
240
|
+
async function updateProjectAccess(input) {
|
|
241
|
+
try {
|
|
242
|
+
await api_client_1.client.put(`/projects/${input.projectId}/permissions/update`, {
|
|
243
|
+
grantedToEmail: input.grantedToEmail,
|
|
244
|
+
permissionType: input.permissionType,
|
|
245
|
+
hidePrice: input.hidePrice,
|
|
246
|
+
});
|
|
247
|
+
const priceNote = input.hidePrice ? ' Pricing is hidden from them.' : ' Pricing is visible to them.';
|
|
248
|
+
return `Permission updated. ${input.grantedToEmail} now has ${input.permissionType} access to project ${input.projectId}.${priceNote}`;
|
|
249
|
+
}
|
|
250
|
+
catch (error) {
|
|
251
|
+
return apiError(error, 'Unexpected error updating project access.');
|
|
252
|
+
}
|
|
253
|
+
}
|
|
254
|
+
async function revokeProjectAccess(input) {
|
|
255
|
+
try {
|
|
256
|
+
await api_client_1.client.delete(`/projects/${input.projectId}/permissions/revoke`, {
|
|
257
|
+
params: { email: input.email },
|
|
258
|
+
});
|
|
259
|
+
return `Access revoked. ${input.email} can no longer access project ${input.projectId}.`;
|
|
260
|
+
}
|
|
261
|
+
catch (error) {
|
|
262
|
+
return apiError(error, 'Unexpected error revoking project access.');
|
|
263
|
+
}
|
|
264
|
+
}
|
|
265
|
+
async function listProjectAccess(input) {
|
|
266
|
+
try {
|
|
267
|
+
const { data } = await api_client_1.client.get(`/projects/${input.projectId}/permissions`);
|
|
268
|
+
if (!data || data.length === 0) {
|
|
269
|
+
return `No shared access on project ${input.projectId}. Only the project owner can view it.`;
|
|
270
|
+
}
|
|
271
|
+
return JSON.stringify(data, null, 2);
|
|
272
|
+
}
|
|
273
|
+
catch (error) {
|
|
274
|
+
return apiError(error, 'Unexpected error listing project access.');
|
|
275
|
+
}
|
|
276
|
+
}
|
|
277
|
+
exports.projectTools = [
|
|
278
|
+
{
|
|
279
|
+
name: 'list_projects',
|
|
280
|
+
description: 'List projects available to the authenticated customer, including owned and shared projects. Use this before choosing a projectId.',
|
|
281
|
+
inputSchema: ListProjectsSchema,
|
|
282
|
+
handler: listProjects,
|
|
283
|
+
},
|
|
284
|
+
{
|
|
285
|
+
name: 'get_project',
|
|
286
|
+
description: 'Get full project details including attached orders, material/service/component summaries, custom line items, and permission metadata.',
|
|
287
|
+
inputSchema: GetProjectSchema,
|
|
288
|
+
handler: getProject,
|
|
289
|
+
},
|
|
290
|
+
{
|
|
291
|
+
name: 'create_project',
|
|
292
|
+
description: 'Create a new customer project. Use this when the customer wants a new job/project container before attaching or placing orders.',
|
|
293
|
+
inputSchema: CreateProjectSchema,
|
|
294
|
+
handler: createProject,
|
|
295
|
+
},
|
|
296
|
+
{
|
|
297
|
+
name: 'update_project',
|
|
298
|
+
description: 'Update project metadata such as name, address, status, phase, client, budget, due date, tax exemption, or notes. Omitted fields are left unchanged.',
|
|
299
|
+
inputSchema: UpdateProjectSchema,
|
|
300
|
+
handler: updateProject,
|
|
301
|
+
},
|
|
302
|
+
{
|
|
303
|
+
name: 'delete_project',
|
|
304
|
+
description: 'Delete a project owned by the customer. Production or paid attached orders block deletion; pending orders are detached and remain in order history.',
|
|
305
|
+
inputSchema: DeleteProjectSchema,
|
|
306
|
+
handler: deleteProject,
|
|
307
|
+
},
|
|
308
|
+
{
|
|
309
|
+
name: 'attach_order_to_project',
|
|
310
|
+
description: 'Attach one of the customer-owned orders to a project the customer can edit. This copies the project snapshot to the order.',
|
|
311
|
+
inputSchema: ProjectOrderSchema,
|
|
312
|
+
handler: attachOrderToProject,
|
|
313
|
+
},
|
|
314
|
+
{
|
|
315
|
+
name: 'detach_order_from_project',
|
|
316
|
+
description: 'Remove an order from a project without deleting the order.',
|
|
317
|
+
inputSchema: ProjectOrderSchema,
|
|
318
|
+
handler: detachOrderFromProject,
|
|
319
|
+
},
|
|
320
|
+
{
|
|
321
|
+
name: 'update_project_order_quantity',
|
|
322
|
+
description: 'Set the project quantity multiplier for an attached order. This affects project summaries only; it does not alter the actual order.',
|
|
323
|
+
inputSchema: UpdateProjectOrderQuantitySchema,
|
|
324
|
+
handler: updateProjectOrderQuantity,
|
|
325
|
+
},
|
|
326
|
+
{
|
|
327
|
+
name: 'update_project_order_options',
|
|
328
|
+
description: 'Update service options for an order within a project: drawerboxes, assembly, hardware, finishing type/color. Omit unchanged fields.',
|
|
329
|
+
inputSchema: UpdateProjectOrderOptionsSchema,
|
|
330
|
+
handler: updateProjectOrderOptions,
|
|
331
|
+
},
|
|
332
|
+
{
|
|
333
|
+
name: 'update_project_article_material',
|
|
334
|
+
description: 'Update a material field for one cabinet/article in an attached project order. Use exact material descriptions.',
|
|
335
|
+
inputSchema: UpdateProjectArticleMaterialSchema,
|
|
336
|
+
handler: updateProjectArticleMaterial,
|
|
337
|
+
},
|
|
338
|
+
{
|
|
339
|
+
name: 'update_project_article_materials',
|
|
340
|
+
description: 'Bulk update a material field for multiple cabinet/article positions in an attached project order. Use exact material descriptions.',
|
|
341
|
+
inputSchema: UpdateProjectArticleMaterialsSchema,
|
|
342
|
+
handler: updateProjectArticleMaterials,
|
|
343
|
+
},
|
|
344
|
+
{
|
|
345
|
+
name: 'share_project_access',
|
|
346
|
+
description: 'Grant another user access to a project. Project owners only. Permission levels are VIEW, EDIT, and ADMIN; hidePrice hides project pricing.',
|
|
347
|
+
inputSchema: ProjectAccessSchema,
|
|
348
|
+
handler: shareProjectAccess,
|
|
349
|
+
},
|
|
350
|
+
{
|
|
351
|
+
name: 'update_project_access',
|
|
352
|
+
description: 'Update an existing project permission level or price visibility. Project owners only.',
|
|
353
|
+
inputSchema: ProjectAccessSchema,
|
|
354
|
+
handler: updateProjectAccess,
|
|
355
|
+
},
|
|
356
|
+
{
|
|
357
|
+
name: 'revoke_project_access',
|
|
358
|
+
description: 'Revoke a user from a project. Project owners only.',
|
|
359
|
+
inputSchema: RevokeProjectAccessSchema,
|
|
360
|
+
handler: revokeProjectAccess,
|
|
361
|
+
},
|
|
362
|
+
{
|
|
363
|
+
name: 'list_project_access',
|
|
364
|
+
description: 'List users who currently have shared access to a project. Project owners only.',
|
|
365
|
+
inputSchema: ListProjectAccessSchema,
|
|
366
|
+
handler: listProjectAccess,
|
|
367
|
+
},
|
|
368
|
+
];
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@sealab/mcp-server",
|
|
3
|
-
"version": "1.0.
|
|
3
|
+
"version": "1.0.4",
|
|
4
4
|
"description": "MCP server for the Sealab cabinetry catalog",
|
|
5
5
|
"main": "dist/index.js",
|
|
6
6
|
"bin": {
|
|
@@ -17,14 +17,16 @@
|
|
|
17
17
|
"@modelcontextprotocol/sdk": "^1.0.0",
|
|
18
18
|
"axios": "^1.6.0",
|
|
19
19
|
"form-data": "^4.0.0",
|
|
20
|
+
"pngjs": "^7.0.0",
|
|
20
21
|
"zod": "^3.22.0",
|
|
21
22
|
"zod-to-json-schema": "^3.22.0"
|
|
22
23
|
},
|
|
23
24
|
"devDependencies": {
|
|
24
25
|
"@types/node": "^20.0.0",
|
|
26
|
+
"@types/pngjs": "^6.0.5",
|
|
27
|
+
"ts-node": "^10.9.0",
|
|
25
28
|
"typescript": "^5.3.0",
|
|
26
|
-
"vitest": "^1.0.0"
|
|
27
|
-
"ts-node": "^10.9.0"
|
|
29
|
+
"vitest": "^1.0.0"
|
|
28
30
|
},
|
|
29
31
|
"engines": {
|
|
30
32
|
"node": ">=18"
|
package/src/client/api-client.ts
CHANGED
|
@@ -7,13 +7,21 @@ if (!API_KEY) {
|
|
|
7
7
|
throw new Error('SEALAB_API_KEY environment variable is required');
|
|
8
8
|
}
|
|
9
9
|
|
|
10
|
-
export const client: AxiosInstance = axios.create({
|
|
11
|
-
baseURL: `${API_URL}/api/mcp/v1`,
|
|
12
|
-
headers: {
|
|
13
|
-
'X-API-Key': API_KEY,
|
|
14
|
-
},
|
|
15
|
-
timeout: 10000,
|
|
16
|
-
});
|
|
10
|
+
export const client: AxiosInstance = axios.create({
|
|
11
|
+
baseURL: `${API_URL}/api/mcp/v1`,
|
|
12
|
+
headers: {
|
|
13
|
+
'X-API-Key': API_KEY,
|
|
14
|
+
},
|
|
15
|
+
timeout: 10000,
|
|
16
|
+
});
|
|
17
|
+
|
|
18
|
+
export const aiDrawingClient: AxiosInstance = axios.create({
|
|
19
|
+
baseURL: `${API_URL}/api/ai-drawing/v1`,
|
|
20
|
+
headers: {
|
|
21
|
+
'X-API-Key': API_KEY,
|
|
22
|
+
},
|
|
23
|
+
timeout: 10000,
|
|
24
|
+
});
|
|
17
25
|
|
|
18
26
|
export class McpApiError extends Error {
|
|
19
27
|
constructor(public readonly status: number, message: string) {
|
package/src/index.ts
CHANGED
|
@@ -11,8 +11,12 @@ import { configurationInfoTools } from './tools/configuration-info';
|
|
|
11
11
|
import { savedSettingsTools } from './tools/saved-settings';
|
|
12
12
|
import { canvasTools } from './tools/canvas';
|
|
13
13
|
import { permissionTools } from './tools/permissions';
|
|
14
|
+
import { projectTools } from './tools/projects';
|
|
15
|
+
import { aiDrawingTools } from './tools/ai-drawing';
|
|
16
|
+
import { aiDrawingOverlayTools } from './tools/ai-drawing-overlay';
|
|
17
|
+
import { normalizeMcpJsonSchema } from './schema-normalizer';
|
|
14
18
|
|
|
15
|
-
const allTools = [...catalogTools, ...orderTools, ...configurationTools, ...configurationInfoTools, ...savedSettingsTools, ...canvasTools, ...permissionTools];
|
|
19
|
+
const allTools = [...catalogTools, ...orderTools, ...configurationTools, ...configurationInfoTools, ...savedSettingsTools, ...canvasTools, ...permissionTools, ...projectTools, ...aiDrawingTools, ...aiDrawingOverlayTools];
|
|
16
20
|
|
|
17
21
|
const server = new Server(
|
|
18
22
|
{ name: 'sealab', version: '1.0.0' },
|
|
@@ -23,7 +27,7 @@ server.setRequestHandler(ListToolsRequestSchema, async () => ({
|
|
|
23
27
|
tools: allTools.map((t) => ({
|
|
24
28
|
name: t.name,
|
|
25
29
|
description: t.description,
|
|
26
|
-
inputSchema: zodToJsonSchema(t.inputSchema as any, { target: 'openApi3' }),
|
|
30
|
+
inputSchema: normalizeMcpJsonSchema(zodToJsonSchema(t.inputSchema as any, { target: 'openApi3' })),
|
|
27
31
|
})),
|
|
28
32
|
}));
|
|
29
33
|
|
|
@@ -0,0 +1,107 @@
|
|
|
1
|
+
import { describe, expect, it } from 'vitest';
|
|
2
|
+
import { normalizeMcpJsonSchema } from './schema-normalizer';
|
|
3
|
+
|
|
4
|
+
describe('normalizeMcpJsonSchema', () => {
|
|
5
|
+
it('converts OpenAPI boolean exclusiveMinimum into JSON Schema numeric exclusiveMinimum', () => {
|
|
6
|
+
expect(normalizeMcpJsonSchema({
|
|
7
|
+
type: 'number',
|
|
8
|
+
minimum: 0,
|
|
9
|
+
exclusiveMinimum: true,
|
|
10
|
+
})).toEqual({
|
|
11
|
+
type: 'number',
|
|
12
|
+
exclusiveMinimum: 0,
|
|
13
|
+
});
|
|
14
|
+
});
|
|
15
|
+
|
|
16
|
+
it('converts OpenAPI boolean exclusiveMaximum into JSON Schema numeric exclusiveMaximum', () => {
|
|
17
|
+
expect(normalizeMcpJsonSchema({
|
|
18
|
+
type: 'number',
|
|
19
|
+
maximum: 1000,
|
|
20
|
+
exclusiveMaximum: true,
|
|
21
|
+
})).toEqual({
|
|
22
|
+
type: 'number',
|
|
23
|
+
exclusiveMaximum: 1000,
|
|
24
|
+
});
|
|
25
|
+
});
|
|
26
|
+
|
|
27
|
+
it('removes false exclusive bounds without changing inclusive bounds', () => {
|
|
28
|
+
expect(normalizeMcpJsonSchema({
|
|
29
|
+
type: 'number',
|
|
30
|
+
minimum: 0,
|
|
31
|
+
maximum: 1000,
|
|
32
|
+
exclusiveMinimum: false,
|
|
33
|
+
exclusiveMaximum: false,
|
|
34
|
+
})).toEqual({
|
|
35
|
+
type: 'number',
|
|
36
|
+
minimum: 0,
|
|
37
|
+
maximum: 1000,
|
|
38
|
+
});
|
|
39
|
+
});
|
|
40
|
+
|
|
41
|
+
it('normalizes nested placement item schemas', () => {
|
|
42
|
+
const normalized = normalizeMcpJsonSchema({
|
|
43
|
+
type: 'object',
|
|
44
|
+
properties: {
|
|
45
|
+
placements: {
|
|
46
|
+
type: 'array',
|
|
47
|
+
items: {
|
|
48
|
+
type: 'object',
|
|
49
|
+
properties: {
|
|
50
|
+
height: {
|
|
51
|
+
type: 'number',
|
|
52
|
+
minimum: 0,
|
|
53
|
+
exclusiveMinimum: true,
|
|
54
|
+
},
|
|
55
|
+
},
|
|
56
|
+
},
|
|
57
|
+
},
|
|
58
|
+
},
|
|
59
|
+
}) as any;
|
|
60
|
+
|
|
61
|
+
expect(normalized.properties.placements.items.properties.height).toEqual({
|
|
62
|
+
type: 'number',
|
|
63
|
+
exclusiveMinimum: 0,
|
|
64
|
+
});
|
|
65
|
+
});
|
|
66
|
+
|
|
67
|
+
it('collapses homogeneous tuple items into a Gemini-compatible item schema', () => {
|
|
68
|
+
const normalized = normalizeMcpJsonSchema({
|
|
69
|
+
type: 'array',
|
|
70
|
+
minItems: 4,
|
|
71
|
+
maxItems: 4,
|
|
72
|
+
items: [
|
|
73
|
+
{ type: 'number', minimum: 0, maximum: 1000 },
|
|
74
|
+
{ $ref: '#/properties/proposals/items/properties/bbox/items/0' },
|
|
75
|
+
{ $ref: '#/properties/proposals/items/properties/bbox/items/0' },
|
|
76
|
+
{ $ref: '#/properties/proposals/items/properties/bbox/items/0' },
|
|
77
|
+
],
|
|
78
|
+
}) as any;
|
|
79
|
+
|
|
80
|
+
expect(Array.isArray(normalized.items)).toBe(false);
|
|
81
|
+
expect(normalized).toEqual({
|
|
82
|
+
type: 'array',
|
|
83
|
+
minItems: 4,
|
|
84
|
+
maxItems: 4,
|
|
85
|
+
items: { type: 'number', minimum: 0, maximum: 1000 },
|
|
86
|
+
});
|
|
87
|
+
});
|
|
88
|
+
|
|
89
|
+
it('keeps heterogeneous tuple schemas valid by using anyOf item schemas', () => {
|
|
90
|
+
const normalized = normalizeMcpJsonSchema({
|
|
91
|
+
type: 'array',
|
|
92
|
+
minItems: 2,
|
|
93
|
+
maxItems: 2,
|
|
94
|
+
items: [
|
|
95
|
+
{ type: 'number' },
|
|
96
|
+
{ type: 'string' },
|
|
97
|
+
],
|
|
98
|
+
}) as any;
|
|
99
|
+
|
|
100
|
+
expect(normalized.items).toEqual({
|
|
101
|
+
anyOf: [
|
|
102
|
+
{ type: 'number' },
|
|
103
|
+
{ type: 'string' },
|
|
104
|
+
],
|
|
105
|
+
});
|
|
106
|
+
});
|
|
107
|
+
});
|
|
@@ -0,0 +1,86 @@
|
|
|
1
|
+
type JsonObject = { [key: string]: unknown };
|
|
2
|
+
|
|
3
|
+
function isJsonObject(value: unknown): value is JsonObject {
|
|
4
|
+
return typeof value === 'object' && value !== null && !Array.isArray(value);
|
|
5
|
+
}
|
|
6
|
+
|
|
7
|
+
function normalizeExclusiveBounds(schema: JsonObject): JsonObject {
|
|
8
|
+
const normalized = { ...schema };
|
|
9
|
+
|
|
10
|
+
if (normalized.exclusiveMinimum === true) {
|
|
11
|
+
if (typeof normalized.minimum === 'number') {
|
|
12
|
+
normalized.exclusiveMinimum = normalized.minimum;
|
|
13
|
+
delete normalized.minimum;
|
|
14
|
+
} else {
|
|
15
|
+
delete normalized.exclusiveMinimum;
|
|
16
|
+
}
|
|
17
|
+
} else if (normalized.exclusiveMinimum === false) {
|
|
18
|
+
delete normalized.exclusiveMinimum;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
if (normalized.exclusiveMaximum === true) {
|
|
22
|
+
if (typeof normalized.maximum === 'number') {
|
|
23
|
+
normalized.exclusiveMaximum = normalized.maximum;
|
|
24
|
+
delete normalized.maximum;
|
|
25
|
+
} else {
|
|
26
|
+
delete normalized.exclusiveMaximum;
|
|
27
|
+
}
|
|
28
|
+
} else if (normalized.exclusiveMaximum === false) {
|
|
29
|
+
delete normalized.exclusiveMaximum;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
return normalized;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
function schemaSignature(schema: unknown): string {
|
|
36
|
+
if (Array.isArray(schema)) {
|
|
37
|
+
return `[${schema.map((item) => schemaSignature(item)).join(',')}]`;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
if (!isJsonObject(schema)) {
|
|
41
|
+
return JSON.stringify(schema);
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
return `{${Object.keys(schema)
|
|
45
|
+
.sort()
|
|
46
|
+
.map((key) => `${JSON.stringify(key)}:${schemaSignature(schema[key])}`)
|
|
47
|
+
.join(',')}}`;
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
function isRefToFirstTupleItem(item: unknown): boolean {
|
|
51
|
+
return isJsonObject(item) && typeof item.$ref === 'string' && item.$ref.endsWith('/items/0');
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
function normalizeTupleItems(schema: JsonObject): JsonObject {
|
|
55
|
+
if (!Array.isArray(schema.items)) {
|
|
56
|
+
return schema;
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
const normalizedItems = schema.items.map((item) => normalizeMcpJsonSchema(item));
|
|
60
|
+
const firstItem = normalizedItems[0];
|
|
61
|
+
const allItemsMatchFirst = normalizedItems.every((item) => (
|
|
62
|
+
schemaSignature(item) === schemaSignature(firstItem) || isRefToFirstTupleItem(item)
|
|
63
|
+
));
|
|
64
|
+
|
|
65
|
+
return {
|
|
66
|
+
...schema,
|
|
67
|
+
items: allItemsMatchFirst ? firstItem : { anyOf: normalizedItems },
|
|
68
|
+
};
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
export function normalizeMcpJsonSchema(schema: unknown): unknown {
|
|
72
|
+
if (Array.isArray(schema)) {
|
|
73
|
+
return schema.map((item) => normalizeMcpJsonSchema(item));
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
if (!isJsonObject(schema)) {
|
|
77
|
+
return schema;
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
const normalized = normalizeTupleItems(normalizeExclusiveBounds(schema));
|
|
81
|
+
for (const [key, value] of Object.entries(normalized)) {
|
|
82
|
+
normalized[key] = normalizeMcpJsonSchema(value);
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
return normalized;
|
|
86
|
+
}
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
import { describe, expect, it } from 'vitest';
|
|
2
|
+
|
|
3
|
+
import { aiDrawingOverlayTools } from './ai-drawing-overlay';
|
|
4
|
+
|
|
5
|
+
describe('aiDrawingOverlayTools', () => {
|
|
6
|
+
it('does not register overlay extraction or placement-candidate tools in the MCP surface', () => {
|
|
7
|
+
expect(aiDrawingOverlayTools).toEqual([]);
|
|
8
|
+
});
|
|
9
|
+
});
|