@mentagen/mcp 0.1.0 → 0.2.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/dist/tools/find-overlaps.js +94 -0
- package/dist/tools/index.js +41 -0
- package/dist/tools/visualize-board.js +319 -0
- package/package.json +2 -1
|
@@ -0,0 +1,94 @@
|
|
|
1
|
+
import { z } from 'zod';
|
|
2
|
+
import { rectsIntersect } from '../utils/collision.js';
|
|
3
|
+
import { formatError } from '../utils/errors.js';
|
|
4
|
+
export const findOverlapsSchema = z.object({
|
|
5
|
+
boardId: z.string().describe('The board ID to check for overlapping nodes'),
|
|
6
|
+
});
|
|
7
|
+
function calculateOverlapArea(a, b) {
|
|
8
|
+
const xOverlap = Math.max(0, Math.min(a.x + a.width, b.x + b.width) - Math.max(a.x, b.x));
|
|
9
|
+
const yOverlap = Math.max(0, Math.min(a.y + a.height, b.y + b.height) - Math.max(a.y, b.y));
|
|
10
|
+
return xOverlap * yOverlap;
|
|
11
|
+
}
|
|
12
|
+
export async function handleFindOverlaps(client, params) {
|
|
13
|
+
try {
|
|
14
|
+
const nodes = await client.listNodes({
|
|
15
|
+
boardId: params.boardId,
|
|
16
|
+
limit: 1000,
|
|
17
|
+
});
|
|
18
|
+
if (nodes.length === 0) {
|
|
19
|
+
return {
|
|
20
|
+
content: [
|
|
21
|
+
{
|
|
22
|
+
type: 'text',
|
|
23
|
+
text: 'No nodes found in this board.',
|
|
24
|
+
},
|
|
25
|
+
],
|
|
26
|
+
};
|
|
27
|
+
}
|
|
28
|
+
const overlaps = [];
|
|
29
|
+
for (let i = 0; i < nodes.length; i++) {
|
|
30
|
+
const node1 = nodes[i];
|
|
31
|
+
const rect1 = {
|
|
32
|
+
x: node1.x,
|
|
33
|
+
y: node1.y,
|
|
34
|
+
width: node1.width,
|
|
35
|
+
height: node1.height,
|
|
36
|
+
};
|
|
37
|
+
for (let j = i + 1; j < nodes.length; j++) {
|
|
38
|
+
const node2 = nodes[j];
|
|
39
|
+
const rect2 = {
|
|
40
|
+
x: node2.x,
|
|
41
|
+
y: node2.y,
|
|
42
|
+
width: node2.width,
|
|
43
|
+
height: node2.height,
|
|
44
|
+
};
|
|
45
|
+
if (rectsIntersect(rect1, rect2)) {
|
|
46
|
+
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),
|
|
50
|
+
});
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
if (overlaps.length === 0) {
|
|
55
|
+
return {
|
|
56
|
+
content: [
|
|
57
|
+
{
|
|
58
|
+
type: 'text',
|
|
59
|
+
text: `No overlapping nodes found. All ${nodes.length} nodes are properly positioned.`,
|
|
60
|
+
},
|
|
61
|
+
],
|
|
62
|
+
};
|
|
63
|
+
}
|
|
64
|
+
overlaps.sort((a, b) => b.overlapArea - a.overlapArea);
|
|
65
|
+
const summary = `Found ${overlaps.length} overlapping node pair(s) among ${nodes.length} nodes.\n`;
|
|
66
|
+
const details = overlaps
|
|
67
|
+
.map((o, i) => {
|
|
68
|
+
return (`${i + 1}. ${o.node1.name} (${o.node1.id})\n` +
|
|
69
|
+
` overlaps with\n` +
|
|
70
|
+
` ${o.node2.name} (${o.node2.id})\n` +
|
|
71
|
+
` Overlap area: ${o.overlapArea}px²`);
|
|
72
|
+
})
|
|
73
|
+
.join('\n\n');
|
|
74
|
+
return {
|
|
75
|
+
content: [
|
|
76
|
+
{
|
|
77
|
+
type: 'text',
|
|
78
|
+
text: summary + '\n' + details,
|
|
79
|
+
},
|
|
80
|
+
],
|
|
81
|
+
};
|
|
82
|
+
}
|
|
83
|
+
catch (error) {
|
|
84
|
+
return {
|
|
85
|
+
content: [
|
|
86
|
+
{
|
|
87
|
+
type: 'text',
|
|
88
|
+
text: `Failed to find overlaps: ${formatError(error)}`,
|
|
89
|
+
},
|
|
90
|
+
],
|
|
91
|
+
isError: true,
|
|
92
|
+
};
|
|
93
|
+
}
|
|
94
|
+
}
|
package/dist/tools/index.js
CHANGED
|
@@ -13,6 +13,7 @@ import { createGraphSchema, handleCreateGraph } from './create-graph.js';
|
|
|
13
13
|
import { deleteSchema, handleDelete } from './delete.js';
|
|
14
14
|
import { deleteEdgeSchema, handleDeleteEdge } from './delete-edge.js';
|
|
15
15
|
import { extractBoardContentSchema, handleExtractBoardContent, } from './extract-board-content.js';
|
|
16
|
+
import { findOverlapsSchema, handleFindOverlaps } from './find-overlaps.js';
|
|
16
17
|
import { findPositionSchema, handleFindPosition } from './find-position.js';
|
|
17
18
|
import { gridCalcSchema, handleGridCalc } from './grid-calc.js';
|
|
18
19
|
import { handleLink, linkSchema } from './link.js';
|
|
@@ -29,6 +30,7 @@ import { handleSizeCalc, sizeCalcSchema } from './size-calc.js';
|
|
|
29
30
|
import { handleUpdate, updateSchema } from './update.js';
|
|
30
31
|
import { handleUpdateBoard, updateBoardSchema } from './update-board.js';
|
|
31
32
|
import { handleUpdateEdge, updateEdgeSchema } from './update-edge.js';
|
|
33
|
+
import { handleVisualizeBoard, visualizeBoardSchema, } from './visualize-board.js';
|
|
32
34
|
export function registerTools(server, client, config) {
|
|
33
35
|
server.setRequestHandler(ListToolsRequestSchema, async () => ({
|
|
34
36
|
tools: [
|
|
@@ -749,6 +751,20 @@ export function registerTools(server, client, config) {
|
|
|
749
751
|
required: ['boardId', 'x', 'y', 'width', 'height'],
|
|
750
752
|
},
|
|
751
753
|
},
|
|
754
|
+
{
|
|
755
|
+
name: 'mentagen_find_overlaps',
|
|
756
|
+
description: 'Find all overlapping node pairs on a board. Returns a list of nodes that overlap with each other, sorted by overlap area. Use this to detect layout issues and identify nodes that need repositioning.',
|
|
757
|
+
inputSchema: {
|
|
758
|
+
type: 'object',
|
|
759
|
+
properties: {
|
|
760
|
+
boardId: {
|
|
761
|
+
type: 'string',
|
|
762
|
+
description: 'The board ID to check for overlapping nodes',
|
|
763
|
+
},
|
|
764
|
+
},
|
|
765
|
+
required: ['boardId'],
|
|
766
|
+
},
|
|
767
|
+
},
|
|
752
768
|
{
|
|
753
769
|
name: 'mentagen_find_position',
|
|
754
770
|
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.',
|
|
@@ -810,6 +826,23 @@ export function registerTools(server, client, config) {
|
|
|
810
826
|
required: ['name'],
|
|
811
827
|
},
|
|
812
828
|
},
|
|
829
|
+
{
|
|
830
|
+
name: 'mentagen_visualize_board',
|
|
831
|
+
description: 'Generate a visual PNG image of a board showing all nodes and their connections. ' +
|
|
832
|
+
'Each node displays its ID in the center. Edges are drawn with arrows for directed connections. ' +
|
|
833
|
+
'Image dimensions are automatically computed to fit all nodes at a readable size. ' +
|
|
834
|
+
'Use this to understand the spatial layout and relationships between nodes.',
|
|
835
|
+
inputSchema: {
|
|
836
|
+
type: 'object',
|
|
837
|
+
properties: {
|
|
838
|
+
boardId: {
|
|
839
|
+
type: 'string',
|
|
840
|
+
description: 'The board ID to visualize',
|
|
841
|
+
},
|
|
842
|
+
},
|
|
843
|
+
required: ['boardId'],
|
|
844
|
+
},
|
|
845
|
+
},
|
|
813
846
|
],
|
|
814
847
|
}));
|
|
815
848
|
const toolHandlers = {
|
|
@@ -917,6 +950,10 @@ export function registerTools(server, client, config) {
|
|
|
917
950
|
const params = checkCollisionSchema.parse(args);
|
|
918
951
|
return handleCheckCollision(client, params);
|
|
919
952
|
},
|
|
953
|
+
mentagen_find_overlaps: async (args) => {
|
|
954
|
+
const params = findOverlapsSchema.parse(args);
|
|
955
|
+
return handleFindOverlaps(client, params);
|
|
956
|
+
},
|
|
920
957
|
mentagen_find_position: async (args) => {
|
|
921
958
|
const params = findPositionSchema.parse(args);
|
|
922
959
|
return handleFindPosition(client, params);
|
|
@@ -925,6 +962,10 @@ export function registerTools(server, client, config) {
|
|
|
925
962
|
const params = sizeCalcSchema.parse(args);
|
|
926
963
|
return handleSizeCalc(params);
|
|
927
964
|
},
|
|
965
|
+
mentagen_visualize_board: async (args) => {
|
|
966
|
+
const params = visualizeBoardSchema.parse(args);
|
|
967
|
+
return handleVisualizeBoard(client, params);
|
|
968
|
+
},
|
|
928
969
|
};
|
|
929
970
|
server.setRequestHandler(CallToolRequestSchema, async (request) => {
|
|
930
971
|
const { name, arguments: args } = request.params;
|
|
@@ -0,0 +1,319 @@
|
|
|
1
|
+
import { Resvg } from '@resvg/resvg-js';
|
|
2
|
+
import { z } from 'zod';
|
|
3
|
+
import { EdgeDirection } from '../client/types.js';
|
|
4
|
+
import { formatError } from '../utils/errors.js';
|
|
5
|
+
export const visualizeBoardSchema = z.object({
|
|
6
|
+
boardId: z.string().describe('The board ID to visualize'),
|
|
7
|
+
});
|
|
8
|
+
const PADDING = 60;
|
|
9
|
+
const MIN_IMAGE_WIDTH = 1000;
|
|
10
|
+
const MAX_IMAGE_WIDTH = 1800;
|
|
11
|
+
const MIN_NODE_DISPLAY_SIZE = 100;
|
|
12
|
+
function computeBounds(nodes) {
|
|
13
|
+
if (nodes.length === 0) {
|
|
14
|
+
return { minX: 0, minY: 0, maxX: 400, maxY: 300, width: 400, height: 300 };
|
|
15
|
+
}
|
|
16
|
+
const minX = Math.min(...nodes.map(n => n.x));
|
|
17
|
+
const minY = Math.min(...nodes.map(n => n.y));
|
|
18
|
+
const maxX = Math.max(...nodes.map(n => n.x + n.width));
|
|
19
|
+
const maxY = Math.max(...nodes.map(n => n.y + n.height));
|
|
20
|
+
return {
|
|
21
|
+
minX,
|
|
22
|
+
minY,
|
|
23
|
+
maxX,
|
|
24
|
+
maxY,
|
|
25
|
+
width: maxX - minX,
|
|
26
|
+
height: maxY - minY,
|
|
27
|
+
};
|
|
28
|
+
}
|
|
29
|
+
function computeImageDimensions(bounds, nodeCount) {
|
|
30
|
+
if (nodeCount === 0) {
|
|
31
|
+
return { width: MIN_IMAGE_WIDTH, height: 600, scale: 1 };
|
|
32
|
+
}
|
|
33
|
+
const avgNodeSize = Math.min(bounds.width, bounds.height) / Math.sqrt(nodeCount);
|
|
34
|
+
const scale = avgNodeSize < MIN_NODE_DISPLAY_SIZE
|
|
35
|
+
? MIN_NODE_DISPLAY_SIZE / avgNodeSize
|
|
36
|
+
: 1;
|
|
37
|
+
let width = (bounds.width + PADDING * 2) * scale;
|
|
38
|
+
let height = (bounds.height + PADDING * 2) * scale;
|
|
39
|
+
if (width > MAX_IMAGE_WIDTH) {
|
|
40
|
+
const ratio = MAX_IMAGE_WIDTH / width;
|
|
41
|
+
width = MAX_IMAGE_WIDTH;
|
|
42
|
+
height = height * ratio;
|
|
43
|
+
}
|
|
44
|
+
if (width < MIN_IMAGE_WIDTH) {
|
|
45
|
+
const ratio = MIN_IMAGE_WIDTH / width;
|
|
46
|
+
width = MIN_IMAGE_WIDTH;
|
|
47
|
+
height = height * ratio;
|
|
48
|
+
}
|
|
49
|
+
height = Math.max(height, 400);
|
|
50
|
+
height = Math.min(height, 1200);
|
|
51
|
+
return {
|
|
52
|
+
width: Math.round(width),
|
|
53
|
+
height: Math.round(height),
|
|
54
|
+
scale,
|
|
55
|
+
};
|
|
56
|
+
}
|
|
57
|
+
function escapeXml(str) {
|
|
58
|
+
return str
|
|
59
|
+
.replace(/&/g, '&')
|
|
60
|
+
.replace(/</g, '<')
|
|
61
|
+
.replace(/>/g, '>')
|
|
62
|
+
.replace(/"/g, '"')
|
|
63
|
+
.replace(/'/g, ''');
|
|
64
|
+
}
|
|
65
|
+
function calculateFontSize(nodeWidth, textLength) {
|
|
66
|
+
const charWidth = 7;
|
|
67
|
+
const padding = 16;
|
|
68
|
+
const availableWidth = nodeWidth - padding;
|
|
69
|
+
const idealSize = availableWidth / (textLength * (charWidth / 12));
|
|
70
|
+
return Math.max(10, Math.min(14, idealSize));
|
|
71
|
+
}
|
|
72
|
+
function getNodeColor(type) {
|
|
73
|
+
const colors = {
|
|
74
|
+
text: '#f1f5f9',
|
|
75
|
+
markdown: '#ecfeff',
|
|
76
|
+
code: '#eef2ff',
|
|
77
|
+
image: '#fef2f2',
|
|
78
|
+
pdf: '#fef3c7',
|
|
79
|
+
url: '#dbeafe',
|
|
80
|
+
'ai-chat': '#f3e8ff',
|
|
81
|
+
mermaid: '#dcfce7',
|
|
82
|
+
};
|
|
83
|
+
return colors[type] || '#f1f5f9';
|
|
84
|
+
}
|
|
85
|
+
function getNodeStrokeColor(type) {
|
|
86
|
+
const colors = {
|
|
87
|
+
text: '#64748b',
|
|
88
|
+
markdown: '#06b6d4',
|
|
89
|
+
code: '#6366f1',
|
|
90
|
+
image: '#ef4444',
|
|
91
|
+
pdf: '#f59e0b',
|
|
92
|
+
url: '#3b82f6',
|
|
93
|
+
'ai-chat': '#a855f7',
|
|
94
|
+
mermaid: '#22c55e',
|
|
95
|
+
};
|
|
96
|
+
return colors[type] || '#64748b';
|
|
97
|
+
}
|
|
98
|
+
function renderNode(node) {
|
|
99
|
+
const fill = getNodeColor(node.type);
|
|
100
|
+
const stroke = getNodeStrokeColor(node.type);
|
|
101
|
+
const id = node._id;
|
|
102
|
+
const centerX = node.x + node.width / 2;
|
|
103
|
+
const centerY = node.y + node.height / 2;
|
|
104
|
+
const fontSize = calculateFontSize(node.width, id.length);
|
|
105
|
+
return `
|
|
106
|
+
<g>
|
|
107
|
+
<rect
|
|
108
|
+
x="${node.x}"
|
|
109
|
+
y="${node.y}"
|
|
110
|
+
width="${node.width}"
|
|
111
|
+
height="${node.height}"
|
|
112
|
+
fill="${fill}"
|
|
113
|
+
stroke="${stroke}"
|
|
114
|
+
stroke-width="2"
|
|
115
|
+
rx="6"
|
|
116
|
+
/>
|
|
117
|
+
<text
|
|
118
|
+
x="${centerX}"
|
|
119
|
+
y="${centerY}"
|
|
120
|
+
text-anchor="middle"
|
|
121
|
+
dominant-baseline="middle"
|
|
122
|
+
font-family="ui-monospace, SFMono-Regular, monospace"
|
|
123
|
+
font-size="${fontSize}"
|
|
124
|
+
font-weight="500"
|
|
125
|
+
fill="#1e293b"
|
|
126
|
+
>${escapeXml(id)}</text>
|
|
127
|
+
</g>
|
|
128
|
+
`;
|
|
129
|
+
}
|
|
130
|
+
function findNodeCenter(nodes, nodeId) {
|
|
131
|
+
const node = nodes.find(n => n._id === nodeId);
|
|
132
|
+
if (!node)
|
|
133
|
+
return null;
|
|
134
|
+
return {
|
|
135
|
+
x: node.x + node.width / 2,
|
|
136
|
+
y: node.y + node.height / 2,
|
|
137
|
+
};
|
|
138
|
+
}
|
|
139
|
+
function findClosestSides(sourceNode, targetNode) {
|
|
140
|
+
const sourceCenterX = sourceNode.x + sourceNode.width / 2;
|
|
141
|
+
const sourceCenterY = sourceNode.y + sourceNode.height / 2;
|
|
142
|
+
const targetCenterX = targetNode.x + targetNode.width / 2;
|
|
143
|
+
const targetCenterY = targetNode.y + targetNode.height / 2;
|
|
144
|
+
const sourceSides = {
|
|
145
|
+
top: { x: sourceCenterX, y: sourceNode.y },
|
|
146
|
+
bottom: { x: sourceCenterX, y: sourceNode.y + sourceNode.height },
|
|
147
|
+
left: { x: sourceNode.x, y: sourceCenterY },
|
|
148
|
+
right: { x: sourceNode.x + sourceNode.width, y: sourceCenterY },
|
|
149
|
+
};
|
|
150
|
+
const targetSides = {
|
|
151
|
+
top: { x: targetCenterX, y: targetNode.y },
|
|
152
|
+
bottom: { x: targetCenterX, y: targetNode.y + targetNode.height },
|
|
153
|
+
left: { x: targetNode.x, y: targetCenterY },
|
|
154
|
+
right: { x: targetNode.x + targetNode.width, y: targetCenterY },
|
|
155
|
+
};
|
|
156
|
+
let minDist = Infinity;
|
|
157
|
+
let bestSource = sourceSides.right;
|
|
158
|
+
let bestTarget = targetSides.left;
|
|
159
|
+
for (const [, sp] of Object.entries(sourceSides)) {
|
|
160
|
+
for (const [, tp] of Object.entries(targetSides)) {
|
|
161
|
+
const dist = Math.sqrt((sp.x - tp.x) ** 2 + (sp.y - tp.y) ** 2);
|
|
162
|
+
if (dist < minDist) {
|
|
163
|
+
minDist = dist;
|
|
164
|
+
bestSource = sp;
|
|
165
|
+
bestTarget = tp;
|
|
166
|
+
}
|
|
167
|
+
}
|
|
168
|
+
}
|
|
169
|
+
return { source: bestSource, target: bestTarget };
|
|
170
|
+
}
|
|
171
|
+
function renderEdge(edge, nodes) {
|
|
172
|
+
const sourceNode = nodes.find(n => n._id === edge.source);
|
|
173
|
+
const targetNode = nodes.find(n => n._id === edge.target);
|
|
174
|
+
if (!sourceNode || !targetNode)
|
|
175
|
+
return '';
|
|
176
|
+
const { source, target } = findClosestSides(sourceNode, targetNode);
|
|
177
|
+
const hasArrow = edge.direction && edge.direction !== EdgeDirection.None;
|
|
178
|
+
const markerId = `arrow-${edge._id}`;
|
|
179
|
+
let path = '';
|
|
180
|
+
let marker = '';
|
|
181
|
+
if (hasArrow) {
|
|
182
|
+
const isToSource = edge.direction === EdgeDirection.Source;
|
|
183
|
+
const arrowStart = isToSource ? target : source;
|
|
184
|
+
const arrowEnd = isToSource ? source : target;
|
|
185
|
+
marker = `
|
|
186
|
+
<defs>
|
|
187
|
+
<marker
|
|
188
|
+
id="${markerId}"
|
|
189
|
+
markerWidth="10"
|
|
190
|
+
markerHeight="7"
|
|
191
|
+
refX="9"
|
|
192
|
+
refY="3.5"
|
|
193
|
+
orient="auto"
|
|
194
|
+
>
|
|
195
|
+
<polygon points="0 0, 10 3.5, 0 7" fill="#64748b" />
|
|
196
|
+
</marker>
|
|
197
|
+
</defs>
|
|
198
|
+
`;
|
|
199
|
+
path = `
|
|
200
|
+
<line
|
|
201
|
+
x1="${arrowStart.x}"
|
|
202
|
+
y1="${arrowStart.y}"
|
|
203
|
+
x2="${arrowEnd.x}"
|
|
204
|
+
y2="${arrowEnd.y}"
|
|
205
|
+
stroke="#64748b"
|
|
206
|
+
stroke-width="2"
|
|
207
|
+
marker-end="url(#${markerId})"
|
|
208
|
+
/>
|
|
209
|
+
`;
|
|
210
|
+
}
|
|
211
|
+
else {
|
|
212
|
+
path = `
|
|
213
|
+
<line
|
|
214
|
+
x1="${source.x}"
|
|
215
|
+
y1="${source.y}"
|
|
216
|
+
x2="${target.x}"
|
|
217
|
+
y2="${target.y}"
|
|
218
|
+
stroke="#94a3b8"
|
|
219
|
+
stroke-width="1.5"
|
|
220
|
+
/>
|
|
221
|
+
`;
|
|
222
|
+
}
|
|
223
|
+
return marker + path;
|
|
224
|
+
}
|
|
225
|
+
function generateSvg(nodes, edges, bounds, dimensions) {
|
|
226
|
+
const viewBoxX = bounds.minX - PADDING;
|
|
227
|
+
const viewBoxY = bounds.minY - PADDING;
|
|
228
|
+
const viewBoxWidth = bounds.width + PADDING * 2;
|
|
229
|
+
const viewBoxHeight = bounds.height + PADDING * 2;
|
|
230
|
+
const edgesSvg = edges.map(e => renderEdge(e, nodes)).join('\n');
|
|
231
|
+
const nodesSvg = nodes.map(n => renderNode(n)).join('\n');
|
|
232
|
+
return `<?xml version="1.0" encoding="UTF-8"?>
|
|
233
|
+
<svg
|
|
234
|
+
xmlns="http://www.w3.org/2000/svg"
|
|
235
|
+
viewBox="${viewBoxX} ${viewBoxY} ${viewBoxWidth} ${viewBoxHeight}"
|
|
236
|
+
width="${dimensions.width}"
|
|
237
|
+
height="${dimensions.height}"
|
|
238
|
+
>
|
|
239
|
+
<rect
|
|
240
|
+
x="${viewBoxX}"
|
|
241
|
+
y="${viewBoxY}"
|
|
242
|
+
width="${viewBoxWidth}"
|
|
243
|
+
height="${viewBoxHeight}"
|
|
244
|
+
fill="#ffffff"
|
|
245
|
+
/>
|
|
246
|
+
${edgesSvg}
|
|
247
|
+
${nodesSvg}
|
|
248
|
+
</svg>`;
|
|
249
|
+
}
|
|
250
|
+
export async function handleVisualizeBoard(client, params) {
|
|
251
|
+
try {
|
|
252
|
+
const [nodes, edges] = await Promise.all([
|
|
253
|
+
client.listNodes({ boardId: params.boardId, limit: 1000 }),
|
|
254
|
+
client.listEdges({ boardId: params.boardId, limit: 1000 }),
|
|
255
|
+
]);
|
|
256
|
+
if (nodes.length === 0) {
|
|
257
|
+
return {
|
|
258
|
+
content: [
|
|
259
|
+
{
|
|
260
|
+
type: 'text',
|
|
261
|
+
text: 'No nodes found in this board.',
|
|
262
|
+
},
|
|
263
|
+
],
|
|
264
|
+
};
|
|
265
|
+
}
|
|
266
|
+
const bounds = computeBounds(nodes);
|
|
267
|
+
const dimensions = computeImageDimensions(bounds, nodes.length);
|
|
268
|
+
const svg = generateSvg(nodes, edges, bounds, dimensions);
|
|
269
|
+
const resvg = new Resvg(svg, {
|
|
270
|
+
fitTo: {
|
|
271
|
+
mode: 'width',
|
|
272
|
+
value: dimensions.width,
|
|
273
|
+
},
|
|
274
|
+
});
|
|
275
|
+
const pngData = resvg.render();
|
|
276
|
+
const pngBuffer = pngData.asPng();
|
|
277
|
+
const base64 = pngBuffer.toString('base64');
|
|
278
|
+
const summary = `Board visualization: ${nodes.length} nodes, ${edges.length} edges (${dimensions.width}x${dimensions.height}px)`;
|
|
279
|
+
const nodeLegend = nodes
|
|
280
|
+
.map(n => `${n._id}\t${n.type}\t${n.name}`)
|
|
281
|
+
.join('\n');
|
|
282
|
+
const edgeLegend = edges.length > 0
|
|
283
|
+
? edges
|
|
284
|
+
.map(e => {
|
|
285
|
+
const dir = e.direction === EdgeDirection.Target
|
|
286
|
+
? '→'
|
|
287
|
+
: e.direction === EdgeDirection.Source
|
|
288
|
+
? '←'
|
|
289
|
+
: '—';
|
|
290
|
+
return `${e.source} ${dir} ${e.target}${e.label ? ` (${e.label})` : ''}`;
|
|
291
|
+
})
|
|
292
|
+
.join('\n')
|
|
293
|
+
: 'No edges';
|
|
294
|
+
return {
|
|
295
|
+
content: [
|
|
296
|
+
{
|
|
297
|
+
type: 'text',
|
|
298
|
+
text: `${summary}\n\nNodes (id, type, name):\n${nodeLegend}\n\nEdges (source → target):\n${edgeLegend}`,
|
|
299
|
+
},
|
|
300
|
+
{
|
|
301
|
+
type: 'image',
|
|
302
|
+
data: base64,
|
|
303
|
+
mimeType: 'image/png',
|
|
304
|
+
},
|
|
305
|
+
],
|
|
306
|
+
};
|
|
307
|
+
}
|
|
308
|
+
catch (error) {
|
|
309
|
+
return {
|
|
310
|
+
content: [
|
|
311
|
+
{
|
|
312
|
+
type: 'text',
|
|
313
|
+
text: `Failed to visualize board: ${formatError(error)}`,
|
|
314
|
+
},
|
|
315
|
+
],
|
|
316
|
+
isError: true,
|
|
317
|
+
};
|
|
318
|
+
}
|
|
319
|
+
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@mentagen/mcp",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.2.0",
|
|
4
4
|
"description": "MCP server for Mentagen knowledge base integration with Cursor",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"bin": "dist/index.js",
|
|
@@ -29,6 +29,7 @@
|
|
|
29
29
|
"license": "UNLICENSED",
|
|
30
30
|
"dependencies": {
|
|
31
31
|
"@modelcontextprotocol/sdk": "^1.0.0",
|
|
32
|
+
"@resvg/resvg-js": "^2.6.2",
|
|
32
33
|
"ejson2": "^1.1.0",
|
|
33
34
|
"papaparse": "^5.5.3",
|
|
34
35
|
"zod": "^3.24.1"
|