@mentagen/mcp 0.1.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.
- package/README.md +100 -0
- package/dist/client/helene.js +178 -0
- package/dist/client/types.js +27 -0
- package/dist/index.js +26 -0
- package/dist/tools/batch-update.js +118 -0
- package/dist/tools/boards.js +50 -0
- package/dist/tools/check-collision.js +55 -0
- package/dist/tools/colors.js +35 -0
- package/dist/tools/create-board.js +50 -0
- package/dist/tools/create-edge.js +61 -0
- package/dist/tools/create-graph.js +174 -0
- package/dist/tools/create.js +152 -0
- package/dist/tools/delete-edge.js +36 -0
- package/dist/tools/delete.js +36 -0
- package/dist/tools/extract-board-content.js +207 -0
- package/dist/tools/find-position.js +117 -0
- package/dist/tools/grid-calc.js +205 -0
- package/dist/tools/index.js +940 -0
- package/dist/tools/link.js +22 -0
- package/dist/tools/list-edges.js +58 -0
- package/dist/tools/list-nodes.js +96 -0
- package/dist/tools/list-positions.js +64 -0
- package/dist/tools/node-types.js +65 -0
- package/dist/tools/patch-content.js +143 -0
- package/dist/tools/read.js +41 -0
- package/dist/tools/search-board.js +99 -0
- package/dist/tools/search-boards.js +50 -0
- package/dist/tools/search.js +89 -0
- package/dist/tools/size-calc.js +108 -0
- package/dist/tools/update-board.js +64 -0
- package/dist/tools/update-edge.js +63 -0
- package/dist/tools/update.js +157 -0
- package/dist/utils/collision.js +177 -0
- package/dist/utils/config.js +16 -0
- package/dist/utils/ejson.js +30 -0
- package/dist/utils/errors.js +16 -0
- package/dist/utils/formatting.js +15 -0
- package/dist/utils/urls.js +18 -0
- package/package.json +49 -0
|
@@ -0,0 +1,117 @@
|
|
|
1
|
+
import { z } from 'zod';
|
|
2
|
+
import { findFreePosition, } from '../utils/collision.js';
|
|
3
|
+
import { formatError } from '../utils/errors.js';
|
|
4
|
+
export const findPositionSchema = z.object({
|
|
5
|
+
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'),
|
|
8
|
+
anchorNodeId: z
|
|
9
|
+
.string()
|
|
10
|
+
.optional()
|
|
11
|
+
.describe('Optional node ID to place relative to'),
|
|
12
|
+
direction: z
|
|
13
|
+
.enum(['right', 'below', 'left', 'above'])
|
|
14
|
+
.optional()
|
|
15
|
+
.describe('Search direction from anchor or start (default: right)'),
|
|
16
|
+
gap: z
|
|
17
|
+
.number()
|
|
18
|
+
.optional()
|
|
19
|
+
.describe('Gap between nodes in pixels (default: 16)'),
|
|
20
|
+
startX: z
|
|
21
|
+
.number()
|
|
22
|
+
.optional()
|
|
23
|
+
.describe('Explicit start X (ignored if anchorNodeId provided)'),
|
|
24
|
+
startY: z
|
|
25
|
+
.number()
|
|
26
|
+
.optional()
|
|
27
|
+
.describe('Explicit start Y (ignored if anchorNodeId provided)'),
|
|
28
|
+
});
|
|
29
|
+
export async function handleFindPosition(client, params) {
|
|
30
|
+
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
|
+
};
|
|
62
|
+
}
|
|
63
|
+
// Find free position
|
|
64
|
+
const result = findFreePosition({
|
|
65
|
+
width: params.width,
|
|
66
|
+
height: params.height,
|
|
67
|
+
nodes,
|
|
68
|
+
anchor,
|
|
69
|
+
direction: params.direction,
|
|
70
|
+
gap: params.gap,
|
|
71
|
+
startX: params.startX,
|
|
72
|
+
startY: params.startY,
|
|
73
|
+
});
|
|
74
|
+
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
|
+
};
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
catch (error) {
|
|
107
|
+
return {
|
|
108
|
+
content: [
|
|
109
|
+
{
|
|
110
|
+
type: 'text',
|
|
111
|
+
text: `Failed to find position: ${formatError(error)}`,
|
|
112
|
+
},
|
|
113
|
+
],
|
|
114
|
+
isError: true,
|
|
115
|
+
};
|
|
116
|
+
}
|
|
117
|
+
}
|
|
@@ -0,0 +1,205 @@
|
|
|
1
|
+
import { z } from 'zod';
|
|
2
|
+
import { findCollidingNodes, findSuggestedPosition, } from '../utils/collision.js';
|
|
3
|
+
const GRID_SIZE = 16;
|
|
4
|
+
export const gridCalcSchema = z
|
|
5
|
+
.object({
|
|
6
|
+
operation: z
|
|
7
|
+
.enum(['snap', 'next_x', 'next_y', 'grid_units'])
|
|
8
|
+
.describe('The calculation operation to perform'),
|
|
9
|
+
value: z
|
|
10
|
+
.number()
|
|
11
|
+
.optional()
|
|
12
|
+
.describe('Value to snap to the grid (for snap operation)'),
|
|
13
|
+
position: z
|
|
14
|
+
.number()
|
|
15
|
+
.optional()
|
|
16
|
+
.describe('Current x or y position (for next_x/next_y operations)'),
|
|
17
|
+
size: z
|
|
18
|
+
.number()
|
|
19
|
+
.optional()
|
|
20
|
+
.describe('Width or height of the current node (for next_x/next_y)'),
|
|
21
|
+
gap: z
|
|
22
|
+
.number()
|
|
23
|
+
.optional()
|
|
24
|
+
.describe('Gap between nodes in pixels (default: 16)'),
|
|
25
|
+
units: z
|
|
26
|
+
.number()
|
|
27
|
+
.optional()
|
|
28
|
+
.describe('Number of grid units to convert to pixels (for grid_units)'),
|
|
29
|
+
// Collision-aware params (optional)
|
|
30
|
+
boardId: z
|
|
31
|
+
.string()
|
|
32
|
+
.optional()
|
|
33
|
+
.describe('Board ID - if provided, checks for collisions'),
|
|
34
|
+
nodeWidth: z
|
|
35
|
+
.number()
|
|
36
|
+
.optional()
|
|
37
|
+
.describe('Width of node being placed (required with boardId)'),
|
|
38
|
+
nodeHeight: z
|
|
39
|
+
.number()
|
|
40
|
+
.optional()
|
|
41
|
+
.describe('Height of node being placed (required with boardId)'),
|
|
42
|
+
nodeY: z
|
|
43
|
+
.number()
|
|
44
|
+
.optional()
|
|
45
|
+
.describe('Y position of node (required for next_x collision check)'),
|
|
46
|
+
nodeX: z
|
|
47
|
+
.number()
|
|
48
|
+
.optional()
|
|
49
|
+
.describe('X position of node (required for next_y collision check)'),
|
|
50
|
+
excludeNodeId: z
|
|
51
|
+
.string()
|
|
52
|
+
.optional()
|
|
53
|
+
.describe('Node ID to exclude from collision check'),
|
|
54
|
+
})
|
|
55
|
+
.refine(data => {
|
|
56
|
+
if (data.operation === 'snap') {
|
|
57
|
+
return data.value !== undefined;
|
|
58
|
+
}
|
|
59
|
+
if (data.operation === 'next_x' || data.operation === 'next_y') {
|
|
60
|
+
return data.position !== undefined && data.size !== undefined;
|
|
61
|
+
}
|
|
62
|
+
if (data.operation === 'grid_units') {
|
|
63
|
+
return data.units !== undefined;
|
|
64
|
+
}
|
|
65
|
+
return true;
|
|
66
|
+
}, {
|
|
67
|
+
message: 'Missing required parameters for operation. snap requires value, next_x/next_y require position and size, grid_units requires units.',
|
|
68
|
+
})
|
|
69
|
+
.refine(data => {
|
|
70
|
+
// If boardId is provided, require nodeWidth and nodeHeight
|
|
71
|
+
if (data.boardId) {
|
|
72
|
+
return data.nodeWidth !== undefined && data.nodeHeight !== undefined;
|
|
73
|
+
}
|
|
74
|
+
return true;
|
|
75
|
+
}, {
|
|
76
|
+
message: 'When boardId is provided, nodeWidth and nodeHeight are required for collision checking.',
|
|
77
|
+
});
|
|
78
|
+
/**
|
|
79
|
+
* Snap a value to the nearest multiple of the grid size (16px).
|
|
80
|
+
* Converts -0 to +0 to avoid JavaScript's signed zero quirks.
|
|
81
|
+
*/
|
|
82
|
+
export function snapToGrid(value) {
|
|
83
|
+
const result = Math.round(value / GRID_SIZE) * GRID_SIZE;
|
|
84
|
+
return Object.is(result, -0) ? 0 : result;
|
|
85
|
+
}
|
|
86
|
+
/**
|
|
87
|
+
* Calculate the next position after a node, with a gap.
|
|
88
|
+
* Result is snapped to the grid.
|
|
89
|
+
*/
|
|
90
|
+
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;
|
|
99
|
+
}
|
|
100
|
+
/**
|
|
101
|
+
* Calculate result based on operation type.
|
|
102
|
+
*/
|
|
103
|
+
function calculateResult(params) {
|
|
104
|
+
if (params.operation === 'snap') {
|
|
105
|
+
return snapToGrid(params.value);
|
|
106
|
+
}
|
|
107
|
+
if (params.operation === 'grid_units') {
|
|
108
|
+
return gridUnitsToPixels(params.units);
|
|
109
|
+
}
|
|
110
|
+
// next_x or next_y
|
|
111
|
+
return calculateNextPosition(params.position, params.size, params.gap ?? GRID_SIZE);
|
|
112
|
+
}
|
|
113
|
+
/**
|
|
114
|
+
* Build input object for result output.
|
|
115
|
+
*/
|
|
116
|
+
function buildInputObject(params) {
|
|
117
|
+
const input = {};
|
|
118
|
+
if (params.value !== undefined)
|
|
119
|
+
input.value = params.value;
|
|
120
|
+
if (params.position !== undefined)
|
|
121
|
+
input.position = params.position;
|
|
122
|
+
if (params.size !== undefined)
|
|
123
|
+
input.size = params.size;
|
|
124
|
+
if (params.gap !== undefined)
|
|
125
|
+
input.gap = params.gap;
|
|
126
|
+
if (params.units !== undefined)
|
|
127
|
+
input.units = params.units;
|
|
128
|
+
return input;
|
|
129
|
+
}
|
|
130
|
+
/**
|
|
131
|
+
* Basic grid calculation without collision checking.
|
|
132
|
+
*/
|
|
133
|
+
export function handleGridCalcSync(params) {
|
|
134
|
+
return {
|
|
135
|
+
operation: params.operation,
|
|
136
|
+
input: buildInputObject(params),
|
|
137
|
+
result: calculateResult(params),
|
|
138
|
+
};
|
|
139
|
+
}
|
|
140
|
+
/**
|
|
141
|
+
* Format result as MCP response.
|
|
142
|
+
*/
|
|
143
|
+
function formatResponse(data) {
|
|
144
|
+
return {
|
|
145
|
+
content: [{ type: 'text', text: JSON.stringify(data, null, 2) }],
|
|
146
|
+
};
|
|
147
|
+
}
|
|
148
|
+
/**
|
|
149
|
+
* Check if operation supports collision checking.
|
|
150
|
+
*/
|
|
151
|
+
function supportsCollisionCheck(operation) {
|
|
152
|
+
return operation === 'next_x' || operation === 'next_y';
|
|
153
|
+
}
|
|
154
|
+
/**
|
|
155
|
+
* Build collision info for a position check.
|
|
156
|
+
*/
|
|
157
|
+
async function checkPositionCollision(params, baseResult, client) {
|
|
158
|
+
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);
|
|
163
|
+
const collision = {
|
|
164
|
+
checked: true,
|
|
165
|
+
collides: collisionResult.collides,
|
|
166
|
+
collidingNodes: collisionResult.collidingNodes,
|
|
167
|
+
};
|
|
168
|
+
if (collisionResult.collides) {
|
|
169
|
+
const direction = params.operation === 'next_x' ? 'x' : 'y';
|
|
170
|
+
collision.suggestedPosition = findSuggestedPosition(x, y, params.nodeWidth, params.nodeHeight, nodes, direction, params.excludeNodeId);
|
|
171
|
+
}
|
|
172
|
+
return collision;
|
|
173
|
+
}
|
|
174
|
+
/**
|
|
175
|
+
* Grid calculation with optional collision checking.
|
|
176
|
+
* If client is provided and params include boardId, checks for collisions.
|
|
177
|
+
*/
|
|
178
|
+
export async function handleGridCalc(params, client) {
|
|
179
|
+
const baseResult = handleGridCalcSync(params);
|
|
180
|
+
// If no boardId or client, return basic result
|
|
181
|
+
if (!params.boardId || !client) {
|
|
182
|
+
return formatResponse(baseResult);
|
|
183
|
+
}
|
|
184
|
+
// For snap and grid_units, collision check doesn't make sense
|
|
185
|
+
if (!supportsCollisionCheck(params.operation)) {
|
|
186
|
+
return formatResponse({
|
|
187
|
+
...baseResult,
|
|
188
|
+
collision: {
|
|
189
|
+
checked: false,
|
|
190
|
+
note: 'Collision check only available for next_x and next_y operations',
|
|
191
|
+
},
|
|
192
|
+
});
|
|
193
|
+
}
|
|
194
|
+
try {
|
|
195
|
+
const collision = await checkPositionCollision(params, baseResult, client);
|
|
196
|
+
return formatResponse({ ...baseResult, collision });
|
|
197
|
+
}
|
|
198
|
+
catch (error) {
|
|
199
|
+
const errorMsg = error instanceof Error ? error.message : 'Failed to check collision';
|
|
200
|
+
return formatResponse({
|
|
201
|
+
...baseResult,
|
|
202
|
+
collision: { checked: false, error: errorMsg },
|
|
203
|
+
});
|
|
204
|
+
}
|
|
205
|
+
}
|