@sealab/mcp-server 1.0.2 → 1.0.3

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.
@@ -1,6 +1,6 @@
1
1
  "use strict";
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
- exports.orderTools = void 0;
3
+ exports.orderTools = exports.ArticleItemSchema = void 0;
4
4
  exports.getOrderBreakdown = getOrderBreakdown;
5
5
  exports.getPositionBreakdown = getPositionBreakdown;
6
6
  exports.createEditCart = createEditCart;
@@ -211,7 +211,7 @@ async function applySavedSettingToArticle(article) {
211
211
  async function applySavedSettingsToArticles(articles) {
212
212
  return Promise.all(articles.map(applySavedSettingToArticle));
213
213
  }
214
- const ArticleItemSchema = zod_1.z.object({
214
+ exports.ArticleItemSchema = zod_1.z.object({
215
215
  serialNumber: zod_1.z.string(),
216
216
  positionName: zod_1.z.string().max(25).describe('Unique label for this cabinet within the order. Must be unique across ALL articles in the order. ' +
217
217
  'Derive from the cabinet display name: "Upper 1125" → "Upper", "Base Cabinet" → "Base". ' +
@@ -364,7 +364,7 @@ const SubmitCartAsVersionSchema = zod_1.z.object({
364
364
  });
365
365
  const AddArticlesToCartSchema = zod_1.z.object({
366
366
  tempOrderId: zod_1.z.string().describe('Saved cart reference ID — a plain numeric string returned by create_saved_cart or list_saved_carts (e.g. "901787"). Do NOT add any prefix such as "SavedCart_".'),
367
- articles: flexArray(ArticleItemSchema).describe('Articles to add to the cart.'),
367
+ articles: flexArray(exports.ArticleItemSchema).describe('Articles to add to the cart.'),
368
368
  });
369
369
  const GetSavedCartSchema = zod_1.z.object({
370
370
  tempOrderId: zod_1.z.string().describe('Saved cart reference ID — a plain numeric string returned by create_saved_cart or list_saved_carts (e.g. "901787"). Do NOT add any prefix such as "SavedCart_".'),
@@ -411,7 +411,7 @@ const DeleteSavedCartArticlesSchema = zod_1.z.object({
411
411
  positionNames: flexArray(zod_1.z.string()).describe('Position names of the articles to delete.'),
412
412
  });
413
413
  const CreateOrderSchema = zod_1.z.object({
414
- articles: flexArray(ArticleItemSchema).describe('Line items to order.'),
414
+ articles: flexArray(exports.ArticleItemSchema).describe('Line items to order.'),
415
415
  projectName: zod_1.z.string().describe('Project name for this order'),
416
416
  purchaseOrder: zod_1.z.string().describe('Purchase order number'),
417
417
  projectAddress: ProjectAddressSchema.optional().describe('Project address (where cabinets will be installed)'),
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@sealab/mcp-server",
3
- "version": "1.0.2",
3
+ "version": "1.0.3",
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"
@@ -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,11 @@ 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 { aiDrawingTools } from './tools/ai-drawing';
15
+ import { aiDrawingOverlayTools } from './tools/ai-drawing-overlay';
16
+ import { normalizeMcpJsonSchema } from './schema-normalizer';
14
17
 
15
- const allTools = [...catalogTools, ...orderTools, ...configurationTools, ...configurationInfoTools, ...savedSettingsTools, ...canvasTools, ...permissionTools];
18
+ const allTools = [...catalogTools, ...orderTools, ...configurationTools, ...configurationInfoTools, ...savedSettingsTools, ...canvasTools, ...permissionTools, ...aiDrawingTools, ...aiDrawingOverlayTools];
16
19
 
17
20
  const server = new Server(
18
21
  { name: 'sealab', version: '1.0.0' },
@@ -23,7 +26,7 @@ server.setRequestHandler(ListToolsRequestSchema, async () => ({
23
26
  tools: allTools.map((t) => ({
24
27
  name: t.name,
25
28
  description: t.description,
26
- inputSchema: zodToJsonSchema(t.inputSchema as any, { target: 'openApi3' }),
29
+ inputSchema: normalizeMcpJsonSchema(zodToJsonSchema(t.inputSchema as any, { target: 'openApi3' })),
27
30
  })),
28
31
  }));
29
32
 
@@ -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
+ });
@@ -0,0 +1,8 @@
1
+ import { z } from 'zod';
2
+
3
+ export const aiDrawingOverlayTools: Array<{
4
+ name: string;
5
+ description: string;
6
+ inputSchema: z.ZodTypeAny;
7
+ handler: (input: unknown) => Promise<string>;
8
+ }> = [];