@mentagen/mcp 0.4.1 → 0.6.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.
@@ -64,9 +64,10 @@ export class MentagenClient {
64
64
  /**
65
65
  * List boards the user has access to.
66
66
  */
67
- async listBoards() {
68
- const result = await this.call('boards.list', {});
69
- return result.data || [];
67
+ async listBoards(params) {
68
+ return this.call('boards.listSimple', {
69
+ orgId: params?.orgId,
70
+ });
70
71
  }
71
72
  /**
72
73
  * Search boards by name.
@@ -84,6 +85,18 @@ export class MentagenClient {
84
85
  async searchNodes(params) {
85
86
  return this.call('boards.searchNodesText', params);
86
87
  }
88
+ /**
89
+ * List organizations the user has access to.
90
+ */
91
+ async listOrganizations() {
92
+ return this.call('org.list', {});
93
+ }
94
+ /**
95
+ * List teams within an organization.
96
+ */
97
+ async listTeams(params) {
98
+ return this.call('team.list', params);
99
+ }
87
100
  /**
88
101
  * Add a new node to a board.
89
102
  */
package/dist/index.js CHANGED
File without changes
@@ -1,14 +1,16 @@
1
1
  import { z } from 'zod';
2
2
  import { formatError } from '../utils/errors.js';
3
+ import { unitsToPixels } from '../utils/units.js';
4
+ import { snapToGrid } from './grid-calc.js';
3
5
  const nodeUpdateSchema = z.object({
4
6
  nodeId: z.string().describe('The node ID to update'),
5
7
  name: z.string().optional().describe('New name for the node'),
6
8
  content: z.string().optional().describe('New content for the node'),
7
9
  color: z.string().optional().describe('Node color'),
8
- x: z.number().optional().describe('X position'),
9
- y: z.number().optional().describe('Y position'),
10
- width: z.number().optional().describe('Node width'),
11
- height: z.number().optional().describe('Node height'),
10
+ x: z.number().optional().describe('X position in grid units'),
11
+ y: z.number().optional().describe('Y position in grid units'),
12
+ width: z.number().optional().describe('Node width in grid units'),
13
+ height: z.number().optional().describe('Node height in grid units'),
12
14
  });
13
15
  const edgeUpdateSchema = z.object({
14
16
  edgeId: z.string().describe('The edge ID to update'),
@@ -44,8 +46,18 @@ export async function handleBatchUpdate(client, params) {
44
46
  // Process node updates in parallel
45
47
  const nodePromises = nodeUpdates.map(async (update) => {
46
48
  const { nodeId, ...data } = update;
47
- // Filter out undefined values
48
- const cleanData = Object.fromEntries(Object.entries(data).filter(([, v]) => v !== undefined));
49
+ // Filter out undefined values and convert position fields from units to pixels
50
+ const cleanData = {};
51
+ for (const [key, value] of Object.entries(data)) {
52
+ if (value !== undefined) {
53
+ if (key === 'x' || key === 'y' || key === 'width' || key === 'height') {
54
+ cleanData[key] = snapToGrid(unitsToPixels(value));
55
+ }
56
+ else {
57
+ cleanData[key] = value;
58
+ }
59
+ }
60
+ }
49
61
  if (Object.keys(cleanData).length === 0) {
50
62
  return { id: nodeId, success: false, error: 'No fields to update' };
51
63
  }
@@ -3,7 +3,12 @@ import { z } from 'zod';
3
3
  import { formatShortDate } from '../utils/ejson.js';
4
4
  import { formatError } from '../utils/errors.js';
5
5
  import { getBoardUrl } from '../utils/urls.js';
6
- export const boardsSchema = z.object({});
6
+ export const boardsSchema = z.object({
7
+ orgId: z
8
+ .string()
9
+ .optional()
10
+ .describe('Optional organization ID to filter boards'),
11
+ });
7
12
  /**
8
13
  * Format boards as TSV for compact output.
9
14
  * Uses papaparse for proper escaping of special characters.
@@ -13,18 +18,29 @@ function formatBoardsTsv(boards) {
13
18
  delimiter: '\t',
14
19
  header: true,
15
20
  newline: '\n',
16
- columns: ['id', 'name', 'description', 'nodeCount', 'updatedAt', 'link'],
21
+ columns: [
22
+ 'id',
23
+ 'name',
24
+ 'description',
25
+ 'nodeCount',
26
+ 'updatedAt',
27
+ 'org',
28
+ 'teams',
29
+ 'link',
30
+ ],
17
31
  });
18
32
  }
19
- export async function handleBoards(client, _params, baseUrl) {
33
+ export async function handleBoards(client, params, baseUrl) {
20
34
  try {
21
- const boards = await client.listBoards();
35
+ const boards = await client.listBoards({ orgId: params.orgId });
22
36
  const formatted = boards.map(board => ({
23
37
  id: board._id,
24
38
  name: board.name,
25
39
  description: board.description || '',
26
40
  nodeCount: board.nodeCount ?? 0,
27
41
  updatedAt: formatShortDate(board.updatedAt),
42
+ org: board.org || '',
43
+ teams: board.teams?.join(',') || '',
28
44
  link: getBoardUrl(baseUrl, board._id),
29
45
  }));
30
46
  return {
@@ -1,12 +1,13 @@
1
1
  import { z } from 'zod';
2
2
  import { findCollidingNodes, snapToGrid } from '../utils/collision.js';
3
3
  import { formatError } from '../utils/errors.js';
4
+ import { pixelsToUnits, unitsToPixels } from '../utils/units.js';
4
5
  export const checkCollisionSchema = z.object({
5
6
  boardId: z.string().describe('The board ID to check against'),
6
- x: z.number().describe('Left position of the rectangle'),
7
- y: z.number().describe('Top position of the rectangle'),
8
- width: z.number().describe('Width of the rectangle'),
9
- height: z.number().describe('Height of the rectangle'),
7
+ x: z.number().describe('Left position of the rectangle in grid units'),
8
+ y: z.number().describe('Top position of the rectangle in grid units'),
9
+ width: z.number().describe('Width of the rectangle in grid units'),
10
+ height: z.number().describe('Height of the rectangle in grid units'),
10
11
  excludeNodeId: z
11
12
  .string()
12
13
  .optional()
@@ -19,15 +20,32 @@ export async function handleCheckCollision(client, params) {
19
20
  boardId: params.boardId,
20
21
  limit: 500,
21
22
  });
22
- // Snap position to grid
23
- const rect = {
24
- x: snapToGrid(params.x),
25
- y: snapToGrid(params.y),
26
- width: params.width,
27
- height: params.height,
23
+ // Convert units to pixels and snap position to grid
24
+ // Note: width/height don't need snapToGrid since unitsToPixels already produces grid-aligned values
25
+ const rectPixels = {
26
+ x: snapToGrid(unitsToPixels(params.x)),
27
+ y: snapToGrid(unitsToPixels(params.y)),
28
+ width: unitsToPixels(params.width),
29
+ height: unitsToPixels(params.height),
28
30
  };
29
31
  // Check for collisions
30
- const result = findCollidingNodes(rect, nodes, params.excludeNodeId);
32
+ const result = findCollidingNodes(rectPixels, nodes, params.excludeNodeId);
33
+ // Convert results back to units
34
+ const rect = {
35
+ x: pixelsToUnits(rectPixels.x),
36
+ y: pixelsToUnits(rectPixels.y),
37
+ width: pixelsToUnits(rectPixels.width),
38
+ height: pixelsToUnits(rectPixels.height),
39
+ };
40
+ const collidingNodes = result.collidingNodes.map(node => ({
41
+ ...node,
42
+ rect: {
43
+ x: pixelsToUnits(node.rect.x),
44
+ y: pixelsToUnits(node.rect.y),
45
+ width: pixelsToUnits(node.rect.width),
46
+ height: pixelsToUnits(node.rect.height),
47
+ },
48
+ }));
31
49
  return {
32
50
  content: [
33
51
  {
@@ -35,7 +53,7 @@ export async function handleCheckCollision(client, params) {
35
53
  text: JSON.stringify({
36
54
  collides: result.collides,
37
55
  rect,
38
- collidingNodes: result.collidingNodes,
56
+ collidingNodes,
39
57
  }, null, 2),
40
58
  },
41
59
  ],
@@ -4,6 +4,11 @@ import { formatError } from '../utils/errors.js';
4
4
  import { getBoardUrl } from '../utils/urls.js';
5
5
  export const createBoardSchema = z.object({
6
6
  name: z.string().min(1).describe('The name for the new board'),
7
+ orgId: z
8
+ .string()
9
+ .length(24)
10
+ .optional()
11
+ .describe('Optional organization ID to create the board in'),
7
12
  });
8
13
  function generateObjectId() {
9
14
  const timestamp = Math.floor(Date.now() / 1000)
@@ -18,6 +23,7 @@ export async function handleCreateBoard(client, params, baseUrl) {
18
23
  const board = await client.createBoard({
19
24
  _id: boardId,
20
25
  name: params.name,
26
+ org: params.orgId,
21
27
  });
22
28
  const result = {
23
29
  success: true,
@@ -1,6 +1,8 @@
1
1
  import { z } from 'zod';
2
2
  import { formatError } from '../utils/errors.js';
3
+ import { pixelsToUnits, unitsToPixels } from '../utils/units.js';
3
4
  import { applyExtension, generateId } from './create.js';
5
+ import { snapToGrid } from './grid-calc.js';
4
6
  import { calculateNodeSize } from './size-calc.js';
5
7
  const nodeInputSchema = z.object({
6
8
  tempId: z.string().describe('Temporary ID for referencing in edges'),
@@ -16,10 +18,10 @@ const nodeInputSchema = z.object({
16
18
  .default('text')
17
19
  .describe('Node type'),
18
20
  color: z.string().optional().describe('Node color'),
19
- x: z.number().optional().describe('X position'),
20
- y: z.number().optional().describe('Y position'),
21
- width: z.number().optional().describe('Width'),
22
- height: z.number().optional().describe('Height'),
21
+ x: z.number().optional().describe('X position in grid units'),
22
+ y: z.number().optional().describe('Y position in grid units'),
23
+ width: z.number().optional().describe('Width in grid units'),
24
+ height: z.number().optional().describe('Height in grid units'),
23
25
  });
24
26
  const edgeInputSchema = z.object({
25
27
  source: z.string().describe('Source node tempId'),
@@ -50,10 +52,14 @@ async function createNode(client, boardId, nodeInput, idMap) {
50
52
  const finalName = applyExtension(nodeInput.name, type, nodeInput.extension);
51
53
  const color = nodeInput.color ?? DEFAULT_NODE_COLORS[type];
52
54
  const autoSize = calculateNodeSize(finalName, type);
53
- const x = nodeInput.x ?? 100;
54
- const y = nodeInput.y ?? 100;
55
- const width = nodeInput.width ?? autoSize.width;
56
- const height = nodeInput.height ?? autoSize.height;
55
+ const xPixels = snapToGrid(nodeInput.x !== undefined ? unitsToPixels(nodeInput.x) : unitsToPixels(6));
56
+ const yPixels = snapToGrid(nodeInput.y !== undefined ? unitsToPixels(nodeInput.y) : unitsToPixels(6));
57
+ const widthPixels = snapToGrid(nodeInput.width !== undefined
58
+ ? unitsToPixels(nodeInput.width)
59
+ : autoSize.width);
60
+ const heightPixels = snapToGrid(nodeInput.height !== undefined
61
+ ? unitsToPixels(nodeInput.height)
62
+ : autoSize.height);
57
63
  try {
58
64
  // For code nodes, store content in the `code` field
59
65
  const isCodeNode = type === 'code';
@@ -66,10 +72,10 @@ async function createNode(client, boardId, nodeInput, idMap) {
66
72
  ...(isCodeNode ? { code: nodeContent } : { content: nodeContent }),
67
73
  type,
68
74
  color,
69
- x,
70
- y,
71
- width,
72
- height,
75
+ x: xPixels,
76
+ y: yPixels,
77
+ width: widthPixels,
78
+ height: heightPixels,
73
79
  },
74
80
  });
75
81
  idMap[nodeInput.tempId] = node._id;
@@ -79,10 +85,10 @@ async function createNode(client, boardId, nodeInput, idMap) {
79
85
  tempId: nodeInput.tempId,
80
86
  id: node._id,
81
87
  name: node.name,
82
- x,
83
- y,
84
- width,
85
- height,
88
+ x: pixelsToUnits(xPixels),
89
+ y: pixelsToUnits(yPixels),
90
+ width: pixelsToUnits(widthPixels),
91
+ height: pixelsToUnits(heightPixels),
86
92
  },
87
93
  };
88
94
  }
@@ -1,5 +1,6 @@
1
1
  import { z } from 'zod';
2
2
  import { formatError } from '../utils/errors.js';
3
+ import { pixelsToUnits, unitsToPixels } from '../utils/units.js';
3
4
  import { getNodeUrl } from '../utils/urls.js';
4
5
  import { snapToGrid } from './grid-calc.js';
5
6
  import { calculateNodeSize } from './size-calc.js';
@@ -66,22 +67,16 @@ export const createSchema = z.object({
66
67
  .string()
67
68
  .optional()
68
69
  .describe('Optional color override. If not specified, uses default for type (markdown=cyan, code=indigo, url=blue)'),
69
- x: z
70
- .number()
71
- .optional()
72
- .describe('X position (must be multiple of 16, default: 96)'),
73
- y: z
74
- .number()
75
- .optional()
76
- .describe('Y position (must be multiple of 16, default: 96)'),
70
+ x: z.number().optional().describe('X position in grid units (default: 6)'),
71
+ y: z.number().optional().describe('Y position in grid units (default: 6)'),
77
72
  width: z
78
73
  .number()
79
74
  .optional()
80
- .describe('Width (must be multiple of 16, default: 256)'),
75
+ .describe('Width in grid units (default: auto-calculated)'),
81
76
  height: z
82
77
  .number()
83
78
  .optional()
84
- .describe('Height (must be multiple of 16, default: 128)'),
79
+ .describe('Height in grid units (default: auto-calculated)'),
85
80
  });
86
81
  /**
87
82
  * Generate a simple ObjectId-like string.
@@ -129,10 +124,14 @@ export async function handleCreate(client, params, mentagenUrl) {
129
124
  const color = params.color || DEFAULT_NODE_COLORS[params.type];
130
125
  const autoSize = calculateNodeSize(finalName, params.type);
131
126
  const position = {
132
- x: snapToGrid(params.x ?? 96),
133
- y: snapToGrid(params.y ?? 96),
134
- width: snapToGrid(params.width ?? autoSize.width),
135
- height: snapToGrid(params.height ?? autoSize.height),
127
+ x: snapToGrid(unitsToPixels(params.x ?? 6)),
128
+ y: snapToGrid(unitsToPixels(params.y ?? 6)),
129
+ width: snapToGrid(params.width !== undefined
130
+ ? unitsToPixels(params.width)
131
+ : autoSize.width),
132
+ height: snapToGrid(params.height !== undefined
133
+ ? unitsToPixels(params.height)
134
+ : autoSize.height),
136
135
  };
137
136
  const nodeFields = buildNodeFields({ ...params, name: finalName }, color, position);
138
137
  const node = await client.addNode({
@@ -146,7 +145,10 @@ export async function handleCreate(client, params, mentagenUrl) {
146
145
  id: node._id,
147
146
  name: node.name,
148
147
  board: params.boardId,
149
- ...position,
148
+ x: pixelsToUnits(position.x),
149
+ y: pixelsToUnits(position.y),
150
+ width: pixelsToUnits(position.width),
151
+ height: pixelsToUnits(position.height),
150
152
  link: getNodeUrl(mentagenUrl, params.boardId, node._id),
151
153
  },
152
154
  };
@@ -1,6 +1,7 @@
1
1
  import { z } from 'zod';
2
2
  import { rectsIntersect } from '../utils/collision.js';
3
3
  import { formatError } from '../utils/errors.js';
4
+ import { pixelsToUnits } from '../utils/units.js';
4
5
  export const findOverlapsSchema = z.object({
5
6
  boardId: z.string().describe('The board ID to check for overlapping nodes'),
6
7
  });
@@ -43,10 +44,29 @@ export async function handleFindOverlaps(client, params) {
43
44
  height: node2.height,
44
45
  };
45
46
  if (rectsIntersect(rect1, rect2)) {
47
+ const overlapAreaPixels = calculateOverlapArea(rect1, rect2);
46
48
  overlaps.push({
47
- node1: { id: node1._id, name: node1.name, rect: rect1 },
48
- node2: { id: node2._id, name: node2.name, rect: rect2 },
49
- overlapArea: calculateOverlapArea(rect1, rect2),
49
+ node1: {
50
+ id: node1._id,
51
+ name: node1.name,
52
+ rect: {
53
+ x: pixelsToUnits(rect1.x),
54
+ y: pixelsToUnits(rect1.y),
55
+ width: pixelsToUnits(rect1.width),
56
+ height: pixelsToUnits(rect1.height),
57
+ },
58
+ },
59
+ node2: {
60
+ id: node2._id,
61
+ name: node2.name,
62
+ rect: {
63
+ x: pixelsToUnits(rect2.x),
64
+ y: pixelsToUnits(rect2.y),
65
+ width: pixelsToUnits(rect2.width),
66
+ height: pixelsToUnits(rect2.height),
67
+ },
68
+ },
69
+ overlapArea: overlapAreaPixels,
50
70
  });
51
71
  }
52
72
  }
@@ -68,7 +88,7 @@ export async function handleFindOverlaps(client, params) {
68
88
  return (`${i + 1}. ${o.node1.name} (${o.node1.id})\n` +
69
89
  ` overlaps with\n` +
70
90
  ` ${o.node2.name} (${o.node2.id})\n` +
71
- ` Overlap area: ${o.overlapArea}px²`);
91
+ ` Overlap area: ${o.overlapArea}px² (${Math.round(o.overlapArea / 256)} units²)`);
72
92
  })
73
93
  .join('\n\n');
74
94
  return {
@@ -1,10 +1,11 @@
1
1
  import { z } from 'zod';
2
2
  import { findFreePosition, } from '../utils/collision.js';
3
3
  import { formatError } from '../utils/errors.js';
4
+ import { pixelsToUnits, unitsToPixels } from '../utils/units.js';
4
5
  export const findPositionSchema = z.object({
5
6
  boardId: z.string().describe('The board ID to search'),
6
- width: z.number().describe('Width of the node to place'),
7
- height: z.number().describe('Height of the node to place'),
7
+ width: z.number().describe('Width of the node to place in grid units'),
8
+ height: z.number().describe('Height of the node to place in grid units'),
8
9
  anchorNodeId: z
9
10
  .string()
10
11
  .optional()
@@ -16,92 +17,116 @@ export const findPositionSchema = z.object({
16
17
  gap: z
17
18
  .number()
18
19
  .optional()
19
- .describe('Gap between nodes in pixels (default: 16)'),
20
+ .describe('Gap between nodes in grid units. Use 6+ for connected nodes, 1 for unrelated (default: 1)'),
20
21
  startX: z
21
22
  .number()
22
23
  .optional()
23
- .describe('Explicit start X (ignored if anchorNodeId provided)'),
24
+ .describe('Explicit start X in grid units (ignored if anchorNodeId provided)'),
24
25
  startY: z
25
26
  .number()
26
27
  .optional()
27
- .describe('Explicit start Y (ignored if anchorNodeId provided)'),
28
+ .describe('Explicit start Y in grid units (ignored if anchorNodeId provided)'),
28
29
  });
30
+ function findAnchorNode(nodes, anchorNodeId) {
31
+ const anchorNode = nodes.find(n => n._id === anchorNodeId);
32
+ if (!anchorNode)
33
+ return null;
34
+ return {
35
+ anchor: {
36
+ x: anchorNode.x,
37
+ y: anchorNode.y,
38
+ width: anchorNode.width,
39
+ height: anchorNode.height,
40
+ },
41
+ anchorInfo: {
42
+ id: anchorNode._id,
43
+ name: anchorNode.name,
44
+ },
45
+ };
46
+ }
47
+ function formatSuccessResponse(position, anchorInfo, direction, searchSteps) {
48
+ return {
49
+ content: [
50
+ {
51
+ type: 'text',
52
+ text: JSON.stringify({
53
+ found: true,
54
+ position: {
55
+ x: pixelsToUnits(position.x),
56
+ y: pixelsToUnits(position.y),
57
+ },
58
+ anchor: anchorInfo || null,
59
+ direction: direction || 'right',
60
+ searchSteps,
61
+ }, null, 2),
62
+ },
63
+ ],
64
+ };
65
+ }
66
+ function formatErrorResponse(error, searchSteps) {
67
+ return {
68
+ content: [
69
+ {
70
+ type: 'text',
71
+ text: JSON.stringify({
72
+ found: false,
73
+ error: error || 'No valid position found',
74
+ searchSteps,
75
+ }, null, 2),
76
+ },
77
+ ],
78
+ isError: true,
79
+ };
80
+ }
81
+ async function prepareSearchParams(client, params) {
82
+ const nodes = await client.listNodes({
83
+ boardId: params.boardId,
84
+ limit: 500,
85
+ });
86
+ if (!params.anchorNodeId) {
87
+ return { nodes };
88
+ }
89
+ const anchorData = findAnchorNode(nodes, params.anchorNodeId);
90
+ if (!anchorData) {
91
+ return {
92
+ error: {
93
+ content: [
94
+ {
95
+ type: 'text',
96
+ text: `Anchor node not found: ${params.anchorNodeId}`,
97
+ },
98
+ ],
99
+ isError: true,
100
+ },
101
+ };
102
+ }
103
+ return {
104
+ nodes,
105
+ anchor: anchorData.anchor,
106
+ anchorInfo: anchorData.anchorInfo,
107
+ };
108
+ }
29
109
  export async function handleFindPosition(client, params) {
30
110
  try {
31
- // Fetch nodes from board
32
- const nodes = await client.listNodes({
33
- boardId: params.boardId,
34
- limit: 500,
35
- });
36
- // Find anchor node if specified
37
- let anchor;
38
- let anchorInfo;
39
- if (params.anchorNodeId) {
40
- const anchorNode = nodes.find(n => n._id === params.anchorNodeId);
41
- if (!anchorNode) {
42
- return {
43
- content: [
44
- {
45
- type: 'text',
46
- text: `Anchor node not found: ${params.anchorNodeId}`,
47
- },
48
- ],
49
- isError: true,
50
- };
51
- }
52
- anchor = {
53
- x: anchorNode.x,
54
- y: anchorNode.y,
55
- width: anchorNode.width,
56
- height: anchorNode.height,
57
- };
58
- anchorInfo = {
59
- id: anchorNode._id,
60
- name: anchorNode.name,
61
- };
111
+ const prepared = await prepareSearchParams(client, params);
112
+ if ('error' in prepared) {
113
+ return prepared.error;
62
114
  }
63
- // Find free position
115
+ const { nodes, anchor, anchorInfo } = prepared;
64
116
  const result = findFreePosition({
65
- width: params.width,
66
- height: params.height,
67
- nodes,
117
+ width: unitsToPixels(params.width),
118
+ height: unitsToPixels(params.height),
119
+ nodes: nodes,
68
120
  anchor,
69
121
  direction: params.direction,
70
- gap: params.gap,
71
- startX: params.startX,
72
- startY: params.startY,
122
+ gap: params.gap !== undefined ? unitsToPixels(params.gap) : undefined,
123
+ startX: params.startX !== undefined ? unitsToPixels(params.startX) : undefined,
124
+ startY: params.startY !== undefined ? unitsToPixels(params.startY) : undefined,
73
125
  });
74
126
  if (result.found && result.position) {
75
- return {
76
- content: [
77
- {
78
- type: 'text',
79
- text: JSON.stringify({
80
- found: true,
81
- position: result.position,
82
- anchor: anchorInfo || null,
83
- direction: params.direction || 'right',
84
- searchSteps: result.searchSteps,
85
- }, null, 2),
86
- },
87
- ],
88
- };
89
- }
90
- else {
91
- return {
92
- content: [
93
- {
94
- type: 'text',
95
- text: JSON.stringify({
96
- found: false,
97
- error: result.error || 'No valid position found',
98
- searchSteps: result.searchSteps,
99
- }, null, 2),
100
- },
101
- ],
102
- isError: true,
103
- };
127
+ return formatSuccessResponse(result.position, anchorInfo, params.direction, result.searchSteps);
104
128
  }
129
+ return formatErrorResponse(result.error, result.searchSteps);
105
130
  }
106
131
  catch (error) {
107
132
  return {