@mentagen/mcp 0.5.0 → 0.7.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.
@@ -1,5 +1,6 @@
1
1
  import { z } from 'zod';
2
2
  import { findCollidingNodes, findSuggestedPosition, } from '../utils/collision.js';
3
+ import { pixelsToUnits, unitsToPixels } from '../utils/units.js';
3
4
  const GRID_SIZE = 16;
4
5
  export const gridCalcSchema = z
5
6
  .object({
@@ -9,23 +10,23 @@ export const gridCalcSchema = z
9
10
  value: z
10
11
  .number()
11
12
  .optional()
12
- .describe('Value to snap to the grid (for snap operation)'),
13
+ .describe('Value to snap to the grid in units (for snap operation)'),
13
14
  position: z
14
15
  .number()
15
16
  .optional()
16
- .describe('Current x or y position (for next_x/next_y operations)'),
17
+ .describe('Current x or y position in units (for next_x/next_y operations)'),
17
18
  size: z
18
19
  .number()
19
20
  .optional()
20
- .describe('Width or height of the current node (for next_x/next_y)'),
21
+ .describe('Width or height of the current node in units (for next_x/next_y)'),
21
22
  gap: z
22
23
  .number()
23
24
  .optional()
24
- .describe('Gap between nodes in pixels (default: 16)'),
25
+ .describe('Gap between nodes in grid units. Use 6+ for connected nodes, 1 for unrelated (default: 1)'),
25
26
  units: z
26
27
  .number()
27
28
  .optional()
28
- .describe('Number of grid units to convert to pixels (for grid_units)'),
29
+ .describe('Number of grid units (for grid_units operation - deprecated, returns same value)'),
29
30
  // Collision-aware params (optional)
30
31
  boardId: z
31
32
  .string()
@@ -34,19 +35,19 @@ export const gridCalcSchema = z
34
35
  nodeWidth: z
35
36
  .number()
36
37
  .optional()
37
- .describe('Width of node being placed (required with boardId)'),
38
+ .describe('Width of node being placed in grid units (required with boardId)'),
38
39
  nodeHeight: z
39
40
  .number()
40
41
  .optional()
41
- .describe('Height of node being placed (required with boardId)'),
42
+ .describe('Height of node being placed in grid units (required with boardId)'),
42
43
  nodeY: z
43
44
  .number()
44
45
  .optional()
45
- .describe('Y position of node (required for next_x collision check)'),
46
+ .describe('Y position of node in grid units (required for next_x collision check)'),
46
47
  nodeX: z
47
48
  .number()
48
49
  .optional()
49
- .describe('X position of node (required for next_y collision check)'),
50
+ .describe('X position of node in grid units (required for next_y collision check)'),
50
51
  excludeNodeId: z
51
52
  .string()
52
53
  .optional()
@@ -85,30 +86,26 @@ export function snapToGrid(value) {
85
86
  }
86
87
  /**
87
88
  * Calculate the next position after a node, with a gap.
88
- * Result is snapped to the grid.
89
+ * All inputs and output are in grid units.
89
90
  */
90
91
  export function calculateNextPosition(position, size, gap) {
91
- return snapToGrid(position + size + gap);
92
- }
93
- /**
94
- * Convert grid units to pixels.
95
- * 1 grid unit = 16 pixels.
96
- */
97
- export function gridUnitsToPixels(units) {
98
- return units * GRID_SIZE;
92
+ return position + size + gap;
99
93
  }
100
94
  /**
101
95
  * Calculate result based on operation type.
96
+ * All operations now work with grid units.
102
97
  */
103
98
  function calculateResult(params) {
104
99
  if (params.operation === 'snap') {
105
- return snapToGrid(params.value);
100
+ // Snap to nearest integer unit
101
+ return Math.round(params.value);
106
102
  }
107
103
  if (params.operation === 'grid_units') {
108
- return gridUnitsToPixels(params.units);
104
+ // Deprecated: just return the units value (identity operation)
105
+ return params.units;
109
106
  }
110
107
  // next_x or next_y
111
- return calculateNextPosition(params.position, params.size, params.gap ?? GRID_SIZE);
108
+ return calculateNextPosition(params.position, params.size, params.gap ?? 1);
112
109
  }
113
110
  /**
114
111
  * Build input object for result output.
@@ -156,18 +153,38 @@ function supportsCollisionCheck(operation) {
156
153
  */
157
154
  async function checkPositionCollision(params, baseResult, client) {
158
155
  const nodes = await client.listNodes({ boardId: params.boardId, limit: 500 });
159
- const x = params.operation === 'next_x' ? baseResult.result : (params.nodeX ?? 0);
160
- const y = params.operation === 'next_y' ? baseResult.result : (params.nodeY ?? 0);
161
- const rect = { x, y, width: params.nodeWidth, height: params.nodeHeight };
162
- const collisionResult = findCollidingNodes(rect, nodes, params.excludeNodeId);
156
+ // Convert units to pixels for collision checking
157
+ const xPixels = params.operation === 'next_x'
158
+ ? unitsToPixels(baseResult.result)
159
+ : unitsToPixels(params.nodeX ?? 0);
160
+ const yPixels = params.operation === 'next_y'
161
+ ? unitsToPixels(baseResult.result)
162
+ : unitsToPixels(params.nodeY ?? 0);
163
+ const rectPixels = {
164
+ x: xPixels,
165
+ y: yPixels,
166
+ width: unitsToPixels(params.nodeWidth),
167
+ height: unitsToPixels(params.nodeHeight),
168
+ };
169
+ const collisionResult = findCollidingNodes(rectPixels, nodes, params.excludeNodeId);
163
170
  const collision = {
164
171
  checked: true,
165
172
  collides: collisionResult.collides,
166
- collidingNodes: collisionResult.collidingNodes,
173
+ collidingNodes: collisionResult.collidingNodes.map(node => ({
174
+ ...node,
175
+ rect: {
176
+ x: pixelsToUnits(node.rect.x),
177
+ y: pixelsToUnits(node.rect.y),
178
+ width: pixelsToUnits(node.rect.width),
179
+ height: pixelsToUnits(node.rect.height),
180
+ },
181
+ })),
167
182
  };
168
183
  if (collisionResult.collides) {
169
184
  const direction = params.operation === 'next_x' ? 'x' : 'y';
170
- collision.suggestedPosition = findSuggestedPosition(x, y, params.nodeWidth, params.nodeHeight, nodes, direction, params.excludeNodeId);
185
+ const suggestedPixels = findSuggestedPosition(xPixels, yPixels, rectPixels.width, rectPixels.height, nodes, direction, params.excludeNodeId);
186
+ collision.suggestedPosition =
187
+ suggestedPixels !== null ? pixelsToUnits(suggestedPixels) : null;
171
188
  }
172
189
  return collision;
173
190
  }
@@ -5,7 +5,6 @@ import { CallToolRequestSchema, ListToolsRequestSchema, } from '@modelcontextpro
5
5
  import { batchUpdateSchema, handleBatchUpdate } from './batch-update.js';
6
6
  import { boardsSchema, handleBoards } from './boards.js';
7
7
  import { checkCollisionSchema, handleCheckCollision, } from './check-collision.js';
8
- import { colorsSchema, handleColors } from './colors.js';
9
8
  import { createSchema, handleCreate } from './create.js';
10
9
  import { createBoardSchema, handleCreateBoard } from './create-board.js';
11
10
  import { createEdgeSchema, handleCreateEdge } from './create-edge.js';
@@ -22,11 +21,9 @@ import { handleListNodes, listNodesSchema } from './list-nodes.js';
22
21
  import { handleListOrganizations, listOrganizationsSchema, } from './list-organizations.js';
23
22
  import { handleListPositions, listPositionsSchema } from './list-positions.js';
24
23
  import { handleListTeams, listTeamsSchema } from './list-teams.js';
25
- import { handleNodeTypes, nodeTypesSchema } from './node-types.js';
26
24
  import { handlePatchContent, patchContentSchema } from './patch-content.js';
27
25
  import { handleRead, readSchema } from './read.js';
28
26
  import { handleSearch, searchSchema } from './search.js';
29
- import { handleSearchBoard, searchBoardSchema } from './search-board.js';
30
27
  import { handleSearchBoards, searchBoardsSchema } from './search-boards.js';
31
28
  import { handleSizeCalc, sizeCalcSchema } from './size-calc.js';
32
29
  import { handleUpdate, updateSchema } from './update.js';
@@ -204,7 +201,7 @@ export function registerTools(server, client, config) {
204
201
  },
205
202
  {
206
203
  name: 'mentagen_search_nodes',
207
- description: 'Search for nodes across ALL your boards using semantic search. Returns matching nodes with truncated content previews and direct links. Use mentagen_search_board to search within a specific board, or mentagen_read_node with the node ID to retrieve full content.',
204
+ description: 'Search for nodes using semantic search. Returns matching nodes with truncated content previews and direct links. Use mentagen_read_node with the node ID to retrieve full content.',
208
205
  inputSchema: {
209
206
  type: 'object',
210
207
  properties: {
@@ -234,34 +231,6 @@ export function registerTools(server, client, config) {
234
231
  required: ['query'],
235
232
  },
236
233
  },
237
- {
238
- name: 'mentagen_search_board',
239
- description: 'Search for nodes within a SPECIFIC board using semantic search. Use this when you know which board to search in. Returns matching nodes with truncated content previews and direct links. Use mentagen_read_node with the node ID to retrieve full content.',
240
- inputSchema: {
241
- type: 'object',
242
- properties: {
243
- boardId: {
244
- type: 'string',
245
- description: 'The board ID to search within (required)',
246
- },
247
- query: {
248
- type: 'string',
249
- description: 'The search query - can be a question or keywords',
250
- },
251
- limit: {
252
- type: 'number',
253
- description: 'Maximum number of results (default: 10)',
254
- default: 10,
255
- },
256
- semanticRatio: {
257
- type: 'number',
258
- description: 'Balance between semantic (1.0) and keyword (0.0) search',
259
- default: 0.5,
260
- },
261
- },
262
- required: ['boardId', 'query'],
263
- },
264
- },
265
234
  {
266
235
  name: 'mentagen_read_node',
267
236
  description: 'Read a single node by ID. Returns the full node content without truncation, unlike search results. Includes a direct link to the node.',
@@ -313,11 +282,11 @@ export function registerTools(server, client, config) {
313
282
  },
314
283
  x: {
315
284
  type: 'number',
316
- description: 'X position (snapped to 16px grid, default: 96)',
285
+ description: 'X position in grid units (1 unit = 16px, default: 6)',
317
286
  },
318
287
  y: {
319
288
  type: 'number',
320
- description: 'Y position (snapped to 16px grid, default: 96)',
289
+ description: 'Y position in grid units (1 unit = 16px, default: 6)',
321
290
  },
322
291
  width: {
323
292
  type: 'number',
@@ -359,23 +328,23 @@ export function registerTools(server, client, config) {
359
328
  },
360
329
  color: {
361
330
  type: 'string',
362
- description: 'Node color name (use mentagen_colors to see available colors)',
331
+ description: 'Node color name (see mentagen://colors resource for available colors)',
363
332
  },
364
333
  x: {
365
334
  type: 'number',
366
- description: 'X position on the board canvas (multiple of 16)',
335
+ description: 'X position in grid units (1 unit = 16px)',
367
336
  },
368
337
  y: {
369
338
  type: 'number',
370
- description: 'Y position on the board canvas (multiple of 16)',
339
+ description: 'Y position in grid units (1 unit = 16px)',
371
340
  },
372
341
  width: {
373
342
  type: 'number',
374
- description: 'Node width in pixels (multiple of 16)',
343
+ description: 'Node width in grid units (1 unit = 16px)',
375
344
  },
376
345
  height: {
377
346
  type: 'number',
378
- description: 'Node height in pixels (multiple of 16)',
347
+ description: 'Node height in grid units (1 unit = 16px)',
379
348
  },
380
349
  autoSize: {
381
350
  type: 'boolean',
@@ -423,10 +392,22 @@ export function registerTools(server, client, config) {
423
392
  name: { type: 'string', description: 'New name' },
424
393
  content: { type: 'string', description: 'New content' },
425
394
  color: { type: 'string', description: 'Node color' },
426
- x: { type: 'number', description: 'X position' },
427
- y: { type: 'number', description: 'Y position' },
428
- width: { type: 'number', description: 'Width' },
429
- height: { type: 'number', description: 'Height' },
395
+ x: {
396
+ type: 'number',
397
+ description: 'X position in grid units (1 unit = 16px)',
398
+ },
399
+ y: {
400
+ type: 'number',
401
+ description: 'Y position in grid units (1 unit = 16px)',
402
+ },
403
+ width: {
404
+ type: 'number',
405
+ description: 'Width in grid units (1 unit = 16px)',
406
+ },
407
+ height: {
408
+ type: 'number',
409
+ description: 'Height in grid units (1 unit = 16px)',
410
+ },
430
411
  },
431
412
  required: ['nodeId'],
432
413
  },
@@ -563,15 +544,21 @@ export function registerTools(server, client, config) {
563
544
  description: 'Node type (default: text).',
564
545
  },
565
546
  color: { type: 'string', description: 'Node color' },
566
- x: { type: 'number', description: 'X position' },
567
- y: { type: 'number', description: 'Y position' },
547
+ x: {
548
+ type: 'number',
549
+ description: 'X position in grid units (1 unit = 16px)',
550
+ },
551
+ y: {
552
+ type: 'number',
553
+ description: 'Y position in grid units (1 unit = 16px)',
554
+ },
568
555
  width: {
569
556
  type: 'number',
570
- description: 'DO NOT USE - auto-calculated from name',
557
+ description: 'Width in grid units (1 unit = 16px, DO NOT USE - auto-calculated from name)',
571
558
  },
572
559
  height: {
573
560
  type: 'number',
574
- description: 'DO NOT USE - auto-calculated from name',
561
+ description: 'Height in grid units (1 unit = 16px, DO NOT USE - auto-calculated from name)',
575
562
  },
576
563
  },
577
564
  required: ['tempId', 'name'],
@@ -715,25 +702,9 @@ export function registerTools(server, client, config) {
715
702
  required: ['boardId'],
716
703
  },
717
704
  },
718
- {
719
- name: 'mentagen_colors',
720
- description: 'Get the list of available colors for nodes. Returns color names and their RGB values.',
721
- inputSchema: {
722
- type: 'object',
723
- properties: {},
724
- },
725
- },
726
- {
727
- name: 'mentagen_node_types',
728
- description: 'Get the list of supported node types with descriptions and usage guidance. Use this to understand what types of nodes can be created.',
729
- inputSchema: {
730
- type: 'object',
731
- properties: {},
732
- },
733
- },
734
705
  {
735
706
  name: 'mentagen_grid_calc',
736
- description: 'Math helper for calculating node positions on the 16px grid. Use gap=96+ for connected nodes, gap=16 for unrelated nodes. Operations: "snap" rounds to 16px, "next_x"/"next_y" calculate position after a node, "grid_units" converts units to pixels.',
707
+ description: 'Math helper for calculating node positions in grid units (1 unit = 16px). Use gap=6+ for connected nodes, gap=1 for unrelated nodes. Operations: "snap" rounds to nearest integer unit, "next_x"/"next_y" calculate position after a node, "grid_units" is deprecated (returns same value).',
737
708
  inputSchema: {
738
709
  type: 'object',
739
710
  properties: {
@@ -744,23 +715,23 @@ export function registerTools(server, client, config) {
744
715
  },
745
716
  value: {
746
717
  type: 'number',
747
- description: 'Value to snap to the grid (for snap operation)',
718
+ description: 'Value to snap to the grid in units (for snap operation)',
748
719
  },
749
720
  position: {
750
721
  type: 'number',
751
- description: 'Current x or y position (for next_x/next_y operations)',
722
+ description: 'Current x or y position in units (for next_x/next_y operations)',
752
723
  },
753
724
  size: {
754
725
  type: 'number',
755
- description: 'Width or height of the current node (for next_x/next_y)',
726
+ description: 'Width or height of the current node in units (for next_x/next_y)',
756
727
  },
757
728
  gap: {
758
729
  type: 'number',
759
- description: 'Gap in pixels. Use 96+ for connected nodes, 16 for unrelated (default: 16)',
730
+ description: 'Gap in grid units (1 unit = 16px). Use 6+ for connected nodes, 1 for unrelated (default: 1)',
760
731
  },
761
732
  units: {
762
733
  type: 'number',
763
- description: 'Number of grid units to convert to pixels (for grid_units)',
734
+ description: 'Number of grid units (1 unit = 16px, for grid_units operation - deprecated, returns same value)',
764
735
  },
765
736
  boardId: {
766
737
  type: 'string',
@@ -768,19 +739,19 @@ export function registerTools(server, client, config) {
768
739
  },
769
740
  nodeWidth: {
770
741
  type: 'number',
771
- description: 'Width of node being placed (required with boardId)',
742
+ description: 'Width of node being placed in grid units (1 unit = 16px, required with boardId)',
772
743
  },
773
744
  nodeHeight: {
774
745
  type: 'number',
775
- description: 'Height of node being placed (required with boardId)',
746
+ description: 'Height of node being placed in grid units (1 unit = 16px, required with boardId)',
776
747
  },
777
748
  nodeY: {
778
749
  type: 'number',
779
- description: 'Y position of node (for next_x collision check)',
750
+ description: 'Y position of node in grid units (1 unit = 16px, for next_x collision check)',
780
751
  },
781
752
  nodeX: {
782
753
  type: 'number',
783
- description: 'X position of node (for next_y collision check)',
754
+ description: 'X position of node in grid units (1 unit = 16px, for next_y collision check)',
784
755
  },
785
756
  excludeNodeId: {
786
757
  type: 'string',
@@ -802,19 +773,19 @@ export function registerTools(server, client, config) {
802
773
  },
803
774
  x: {
804
775
  type: 'number',
805
- description: 'Left position of the rectangle',
776
+ description: 'Left position of the rectangle in grid units (1 unit = 16px)',
806
777
  },
807
778
  y: {
808
779
  type: 'number',
809
- description: 'Top position of the rectangle',
780
+ description: 'Top position of the rectangle in grid units (1 unit = 16px)',
810
781
  },
811
782
  width: {
812
783
  type: 'number',
813
- description: 'Width of the rectangle',
784
+ description: 'Width of the rectangle in grid units (1 unit = 16px)',
814
785
  },
815
786
  height: {
816
787
  type: 'number',
817
- description: 'Height of the rectangle',
788
+ description: 'Height of the rectangle in grid units (1 unit = 16px)',
818
789
  },
819
790
  excludeNodeId: {
820
791
  type: 'string',
@@ -840,7 +811,7 @@ export function registerTools(server, client, config) {
840
811
  },
841
812
  {
842
813
  name: 'mentagen_find_position',
843
- description: 'Find a valid non-colliding position for a new node. IMPORTANT: Use gap=96 or more when nodes will be connected by edges, otherwise gap=16 is fine for unrelated nodes.',
814
+ description: 'Find a valid non-colliding position for a new node. IMPORTANT: Use gap=6+ (96px+) when nodes will be connected by edges, otherwise gap=1 (16px) is fine for unrelated nodes. All positions are in grid units (1 unit = 16px).',
844
815
  inputSchema: {
845
816
  type: 'object',
846
817
  properties: {
@@ -850,11 +821,11 @@ export function registerTools(server, client, config) {
850
821
  },
851
822
  width: {
852
823
  type: 'number',
853
- description: 'Width of the node to place',
824
+ description: 'Width of the node to place in grid units (1 unit = 16px)',
854
825
  },
855
826
  height: {
856
827
  type: 'number',
857
- description: 'Height of the node to place',
828
+ description: 'Height of the node to place in grid units (1 unit = 16px)',
858
829
  },
859
830
  anchorNodeId: {
860
831
  type: 'string',
@@ -867,15 +838,15 @@ export function registerTools(server, client, config) {
867
838
  },
868
839
  gap: {
869
840
  type: 'number',
870
- description: 'Gap between nodes in pixels. Use 96+ for connected nodes (edges), 16 for unrelated nodes',
841
+ description: 'Gap between nodes in grid units (1 unit = 16px). Use 6+ for connected nodes (edges), 1 for unrelated nodes',
871
842
  },
872
843
  startX: {
873
844
  type: 'number',
874
- description: 'Explicit start X (ignored if anchorNodeId provided)',
845
+ description: 'Explicit start X in grid units (1 unit = 16px, ignored if anchorNodeId provided)',
875
846
  },
876
847
  startY: {
877
848
  type: 'number',
878
- description: 'Explicit start Y (ignored if anchorNodeId provided)',
849
+ description: 'Explicit start Y in grid units (1 unit = 16px, ignored if anchorNodeId provided)',
879
850
  },
880
851
  },
881
852
  required: ['boardId', 'width', 'height'],
@@ -883,7 +854,7 @@ export function registerTools(server, client, config) {
883
854
  },
884
855
  {
885
856
  name: 'mentagen_size_calc',
886
- description: 'Calculate recommended node dimensions based on name and type. Returns width and height values (multiples of 16) sized to fit the title. Content is intentionally ignored — nodes are sized for their title with content scrollable inside.',
857
+ description: 'Calculate recommended node dimensions based on name and type. Returns width and height values in grid units (1 unit = 16px) sized to fit the title. Content is intentionally ignored — nodes are sized for their title with content scrollable inside.',
887
858
  inputSchema: {
888
859
  type: 'object',
889
860
  properties: {
@@ -966,10 +937,6 @@ export function registerTools(server, client, config) {
966
937
  const params = searchSchema.parse(args);
967
938
  return handleSearch(client, params, config.mentagenUrl);
968
939
  },
969
- mentagen_search_board: async (args) => {
970
- const params = searchBoardSchema.parse(args);
971
- return handleSearchBoard(client, params, config.mentagenUrl);
972
- },
973
940
  mentagen_read_node: async (args) => {
974
941
  const params = readSchema.parse(args);
975
942
  return handleRead(client, params, config.mentagenUrl);
@@ -1020,14 +987,6 @@ export function registerTools(server, client, config) {
1020
987
  const params = linkSchema.parse(args);
1021
988
  return handleLink(params, config);
1022
989
  },
1023
- mentagen_colors: async (args) => {
1024
- colorsSchema.parse(args);
1025
- return handleColors();
1026
- },
1027
- mentagen_node_types: async (args) => {
1028
- nodeTypesSchema.parse(args);
1029
- return handleNodeTypes();
1030
- },
1031
990
  mentagen_grid_calc: async (args) => {
1032
991
  const params = gridCalcSchema.parse(args);
1033
992
  return handleGridCalc(params, client);
@@ -2,6 +2,7 @@ import Papa from 'papaparse';
2
2
  import { z } from 'zod';
3
3
  import { formatDate } from '../utils/ejson.js';
4
4
  import { formatError } from '../utils/errors.js';
5
+ import { pixelsToUnits } from '../utils/units.js';
5
6
  import { getNodeUrl } from '../utils/urls.js';
6
7
  export const listNodesSchema = z.object({
7
8
  boardId: z.string().describe('The board ID to list nodes from'),
@@ -27,10 +28,10 @@ function formatNodesTsv(nodes) {
27
28
  id: n._id,
28
29
  name: n.name,
29
30
  type: n.type,
30
- x: n.x,
31
- y: n.y,
32
- width: n.width,
33
- height: n.height,
31
+ x: pixelsToUnits(n.x),
32
+ y: pixelsToUnits(n.y),
33
+ width: pixelsToUnits(n.width),
34
+ height: pixelsToUnits(n.height),
34
35
  content: truncateContent(n.content ?? null, MAX_CONTENT_LENGTH),
35
36
  updatedAt: formatDate(n.updatedAt),
36
37
  link: n.link,
@@ -1,6 +1,7 @@
1
1
  import Papa from 'papaparse';
2
2
  import { z } from 'zod';
3
3
  import { formatError } from '../utils/errors.js';
4
+ import { pixelsToUnits } from '../utils/units.js';
4
5
  export const listPositionsSchema = z.object({
5
6
  boardId: z.string().describe('The board ID to list node positions from'),
6
7
  limit: z
@@ -13,10 +14,10 @@ function formatPositionsTsv(nodes) {
13
14
  const rows = nodes.map(n => ({
14
15
  id: n._id,
15
16
  name: n.name,
16
- x: n.x,
17
- y: n.y,
18
- w: n.width,
19
- h: n.height,
17
+ x: pixelsToUnits(n.x),
18
+ y: pixelsToUnits(n.y),
19
+ w: pixelsToUnits(n.width),
20
+ h: pixelsToUnits(n.height),
20
21
  }));
21
22
  return Papa.unparse(rows, {
22
23
  delimiter: '\t',
@@ -1,5 +1,6 @@
1
1
  import { z } from 'zod';
2
2
  import { formatError } from '../utils/errors.js';
3
+ import { pixelsToUnits } from '../utils/units.js';
3
4
  import { getNodeUrl } from '../utils/urls.js';
4
5
  export const readSchema = z.object({
5
6
  boardId: z.string().describe('The board ID containing the node'),
@@ -13,9 +14,21 @@ export async function handleRead(client, params, baseUrl) {
13
14
  });
14
15
  // Exclude large fields that aren't useful for AI consumption
15
16
  const { _vectors, __v, textDetections, ...nodeData } = node;
16
- // Add link to the response
17
+ // Convert position fields from pixels to units
17
18
  const response = {
18
19
  ...nodeData,
20
+ x: 'x' in nodeData && typeof nodeData.x === 'number'
21
+ ? pixelsToUnits(nodeData.x)
22
+ : nodeData.x,
23
+ y: 'y' in nodeData && typeof nodeData.y === 'number'
24
+ ? pixelsToUnits(nodeData.y)
25
+ : nodeData.y,
26
+ width: 'width' in nodeData && typeof nodeData.width === 'number'
27
+ ? pixelsToUnits(nodeData.width)
28
+ : nodeData.width,
29
+ height: 'height' in nodeData && typeof nodeData.height === 'number'
30
+ ? pixelsToUnits(nodeData.height)
31
+ : nodeData.height,
19
32
  link: getNodeUrl(baseUrl, params.boardId, params.nodeId),
20
33
  };
21
34
  return {
@@ -1,10 +1,12 @@
1
1
  import { z } from 'zod';
2
+ import { pixelsToUnits } from '../utils/units.js';
2
3
  // Grid and constraint constants (from src/common/boards/constraints.ts)
4
+ // Values in pixels (internal calculations)
3
5
  const GRID = 16;
4
- const MIN_WIDTH = 128;
5
- const MIN_HEIGHT = 64;
6
- const MAX_WIDTH = 512; // Reasonable max for auto-sizing (not the 4096 absolute max)
7
- const MAX_HEIGHT = 512;
6
+ const MIN_WIDTH = 128; // 8 units
7
+ const MIN_HEIGHT = 64; // 4 units
8
+ const MAX_WIDTH = 512; // 32 units - reasonable max for auto-sizing (not the 4096 absolute max)
9
+ const MAX_HEIGHT = 512; // 32 units
8
10
  // Text measurement constants (from measure-node-size.ts)
9
11
  const MAX_AUTO_WIDTH = 400;
10
12
  const PADDING_X = 12;
@@ -96,7 +98,10 @@ export function calculateNodeSize(name, type) {
96
98
  return { width, height };
97
99
  }
98
100
  export function handleSizeCalc(params) {
99
- const { width, height } = calculateNodeSize(params.name, params.type);
101
+ const { width: widthPixels, height: heightPixels } = calculateNodeSize(params.name, params.type);
102
+ // Convert output to units
103
+ const width = pixelsToUnits(widthPixels);
104
+ const height = pixelsToUnits(heightPixels);
100
105
  return {
101
106
  content: [
102
107
  {