@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.
Files changed (39) hide show
  1. package/README.md +100 -0
  2. package/dist/client/helene.js +178 -0
  3. package/dist/client/types.js +27 -0
  4. package/dist/index.js +26 -0
  5. package/dist/tools/batch-update.js +118 -0
  6. package/dist/tools/boards.js +50 -0
  7. package/dist/tools/check-collision.js +55 -0
  8. package/dist/tools/colors.js +35 -0
  9. package/dist/tools/create-board.js +50 -0
  10. package/dist/tools/create-edge.js +61 -0
  11. package/dist/tools/create-graph.js +174 -0
  12. package/dist/tools/create.js +152 -0
  13. package/dist/tools/delete-edge.js +36 -0
  14. package/dist/tools/delete.js +36 -0
  15. package/dist/tools/extract-board-content.js +207 -0
  16. package/dist/tools/find-position.js +117 -0
  17. package/dist/tools/grid-calc.js +205 -0
  18. package/dist/tools/index.js +940 -0
  19. package/dist/tools/link.js +22 -0
  20. package/dist/tools/list-edges.js +58 -0
  21. package/dist/tools/list-nodes.js +96 -0
  22. package/dist/tools/list-positions.js +64 -0
  23. package/dist/tools/node-types.js +65 -0
  24. package/dist/tools/patch-content.js +143 -0
  25. package/dist/tools/read.js +41 -0
  26. package/dist/tools/search-board.js +99 -0
  27. package/dist/tools/search-boards.js +50 -0
  28. package/dist/tools/search.js +89 -0
  29. package/dist/tools/size-calc.js +108 -0
  30. package/dist/tools/update-board.js +64 -0
  31. package/dist/tools/update-edge.js +63 -0
  32. package/dist/tools/update.js +157 -0
  33. package/dist/utils/collision.js +177 -0
  34. package/dist/utils/config.js +16 -0
  35. package/dist/utils/ejson.js +30 -0
  36. package/dist/utils/errors.js +16 -0
  37. package/dist/utils/formatting.js +15 -0
  38. package/dist/utils/urls.js +18 -0
  39. package/package.json +49 -0
@@ -0,0 +1,22 @@
1
+ import { z } from 'zod';
2
+ import { getBoardUrl, getNodeUrl } from '../utils/urls.js';
3
+ export const linkSchema = z.object({
4
+ boardId: z.string().describe('The board ID'),
5
+ nodeId: z
6
+ .string()
7
+ .optional()
8
+ .describe('Optional node ID to link directly to a node'),
9
+ });
10
+ export function handleLink(params, config) {
11
+ const url = params.nodeId
12
+ ? getNodeUrl(config.mentagenUrl, params.boardId, params.nodeId)
13
+ : getBoardUrl(config.mentagenUrl, params.boardId);
14
+ return {
15
+ content: [
16
+ {
17
+ type: 'text',
18
+ text: url,
19
+ },
20
+ ],
21
+ };
22
+ }
@@ -0,0 +1,58 @@
1
+ import { z } from 'zod';
2
+ import { EdgeDirection } from '../client/types.js';
3
+ import { formatError } from '../utils/errors.js';
4
+ export const listEdgesSchema = z.object({
5
+ boardId: z.string().describe('The board ID to list edges from'),
6
+ limit: z
7
+ .number()
8
+ .max(1000)
9
+ .optional()
10
+ .describe('Maximum number of edges to return (default: 1000, max: 1000)'),
11
+ });
12
+ /**
13
+ * Format edges as TSV for concise output.
14
+ */
15
+ function formatEdgesTsv(edges) {
16
+ if (edges.length === 0) {
17
+ return 'No edges found on this board.';
18
+ }
19
+ const columns = ['id', 'source', 'target', 'direction', 'label'];
20
+ const header = columns.join('\t');
21
+ const rows = edges.map(edge => [edge.id, edge.source, edge.target, edge.direction, edge.label || ''].join('\t'));
22
+ return [header, ...rows].join('\n');
23
+ }
24
+ export async function handleListEdges(client, params) {
25
+ try {
26
+ const edges = await client.listEdges({
27
+ boardId: params.boardId,
28
+ limit: params.limit,
29
+ });
30
+ const formattedEdges = edges.map(edge => ({
31
+ id: edge._id,
32
+ source: edge.source,
33
+ target: edge.target,
34
+ direction: edge.direction || EdgeDirection.None,
35
+ label: edge.label || null,
36
+ }));
37
+ const tsv = formatEdgesTsv(formattedEdges);
38
+ return {
39
+ content: [
40
+ {
41
+ type: 'text',
42
+ text: `Found ${edges.length} edge(s):\n\n${tsv}`,
43
+ },
44
+ ],
45
+ };
46
+ }
47
+ catch (error) {
48
+ return {
49
+ content: [
50
+ {
51
+ type: 'text',
52
+ text: `Failed to list edges: ${formatError(error)}`,
53
+ },
54
+ ],
55
+ isError: true,
56
+ };
57
+ }
58
+ }
@@ -0,0 +1,96 @@
1
+ import Papa from 'papaparse';
2
+ import { z } from 'zod';
3
+ import { formatDate } from '../utils/ejson.js';
4
+ import { formatError } from '../utils/errors.js';
5
+ import { getNodeUrl } from '../utils/urls.js';
6
+ export const listNodesSchema = z.object({
7
+ boardId: z.string().describe('The board ID to list nodes from'),
8
+ limit: z
9
+ .number()
10
+ .max(1000)
11
+ .default(1000)
12
+ .describe('Maximum number of nodes to return (default: 1000, max: 1000)'),
13
+ });
14
+ const MAX_CONTENT_LENGTH = 100;
15
+ function truncateContent(content, maxLength) {
16
+ if (!content)
17
+ return '';
18
+ if (content.length <= maxLength)
19
+ return content;
20
+ const truncated = content.slice(0, maxLength);
21
+ const lastSpace = truncated.lastIndexOf(' ');
22
+ return ((lastSpace > maxLength * 0.7 ? truncated.slice(0, lastSpace) : truncated) +
23
+ '...');
24
+ }
25
+ function formatNodesTsv(nodes) {
26
+ const rows = nodes.map(n => ({
27
+ id: n._id,
28
+ name: n.name,
29
+ type: n.type,
30
+ x: n.x,
31
+ y: n.y,
32
+ width: n.width,
33
+ height: n.height,
34
+ content: truncateContent(n.content ?? null, MAX_CONTENT_LENGTH),
35
+ updatedAt: formatDate(n.updatedAt),
36
+ link: n.link,
37
+ }));
38
+ return Papa.unparse(rows, {
39
+ delimiter: '\t',
40
+ header: true,
41
+ newline: '\n',
42
+ columns: [
43
+ 'id',
44
+ 'name',
45
+ 'type',
46
+ 'x',
47
+ 'y',
48
+ 'width',
49
+ 'height',
50
+ 'content',
51
+ 'updatedAt',
52
+ 'link',
53
+ ],
54
+ });
55
+ }
56
+ export async function handleListNodes(client, params, baseUrl) {
57
+ try {
58
+ const nodes = await client.listNodes({
59
+ boardId: params.boardId,
60
+ limit: params.limit,
61
+ });
62
+ if (nodes.length === 0) {
63
+ return {
64
+ content: [
65
+ {
66
+ type: 'text',
67
+ text: 'No nodes found in this board.',
68
+ },
69
+ ],
70
+ };
71
+ }
72
+ const nodesWithLinks = nodes.map(n => ({
73
+ ...n,
74
+ link: getNodeUrl(baseUrl, params.boardId, n._id),
75
+ }));
76
+ return {
77
+ content: [
78
+ {
79
+ type: 'text',
80
+ text: formatNodesTsv(nodesWithLinks),
81
+ },
82
+ ],
83
+ };
84
+ }
85
+ catch (error) {
86
+ return {
87
+ content: [
88
+ {
89
+ type: 'text',
90
+ text: `Failed to list nodes: ${formatError(error)}`,
91
+ },
92
+ ],
93
+ isError: true,
94
+ };
95
+ }
96
+ }
@@ -0,0 +1,64 @@
1
+ import Papa from 'papaparse';
2
+ import { z } from 'zod';
3
+ import { formatError } from '../utils/errors.js';
4
+ export const listPositionsSchema = z.object({
5
+ boardId: z.string().describe('The board ID to list node positions from'),
6
+ limit: z
7
+ .number()
8
+ .max(1000)
9
+ .default(1000)
10
+ .describe('Maximum number of nodes to return (default: 1000, max: 1000)'),
11
+ });
12
+ function formatPositionsTsv(nodes) {
13
+ const rows = nodes.map(n => ({
14
+ id: n._id,
15
+ name: n.name,
16
+ x: n.x,
17
+ y: n.y,
18
+ w: n.width,
19
+ h: n.height,
20
+ }));
21
+ return Papa.unparse(rows, {
22
+ delimiter: '\t',
23
+ header: true,
24
+ newline: '\n',
25
+ columns: ['id', 'name', 'x', 'y', 'w', 'h'],
26
+ });
27
+ }
28
+ export async function handleListPositions(client, params) {
29
+ try {
30
+ const nodes = await client.listNodes({
31
+ boardId: params.boardId,
32
+ limit: params.limit,
33
+ });
34
+ if (nodes.length === 0) {
35
+ return {
36
+ content: [
37
+ {
38
+ type: 'text',
39
+ text: 'No nodes found in this board.',
40
+ },
41
+ ],
42
+ };
43
+ }
44
+ return {
45
+ content: [
46
+ {
47
+ type: 'text',
48
+ text: formatPositionsTsv(nodes),
49
+ },
50
+ ],
51
+ };
52
+ }
53
+ catch (error) {
54
+ return {
55
+ content: [
56
+ {
57
+ type: 'text',
58
+ text: `Failed to list positions: ${formatError(error)}`,
59
+ },
60
+ ],
61
+ isError: true,
62
+ };
63
+ }
64
+ }
@@ -0,0 +1,65 @@
1
+ import { z } from 'zod';
2
+ export const nodeTypesSchema = z.object({});
3
+ /**
4
+ * Supported node types with their default colors.
5
+ *
6
+ * Keep in sync with: src/client/features/boards/inner-board-context-menu.tsx
7
+ *
8
+ * When adding new node types or changing default colors in the context menu,
9
+ * update this list to match.
10
+ */
11
+ const NODE_TYPES = [
12
+ {
13
+ type: 'text',
14
+ defaultColor: null,
15
+ description: 'Simple text node with a title and optional rich text content.',
16
+ usage: 'Use for short notes, labels, headings, or any content that needs a title. Content supports basic HTML formatting.',
17
+ fields: {
18
+ name: 'The node title (required)',
19
+ content: 'Optional HTML content (e.g., <p>paragraph</p>, <ul><li>list</li></ul>)',
20
+ },
21
+ },
22
+ {
23
+ type: 'markdown',
24
+ defaultColor: 'cyan',
25
+ description: 'Markdown node for longer-form content with full Markdown syntax support.',
26
+ usage: 'Use for documentation, articles, formatted notes, or any content that benefits from Markdown formatting like headers, lists, code blocks, and links.',
27
+ fields: {
28
+ name: 'The node title (required)',
29
+ content: 'Markdown content (supports full GFM syntax)',
30
+ },
31
+ },
32
+ {
33
+ type: 'code',
34
+ defaultColor: 'indigo',
35
+ description: 'Code node for storing and displaying source code with syntax highlighting.',
36
+ usage: 'Use for code snippets, scripts, configuration files, or any content that needs syntax highlighting. Supports multiple programming languages.',
37
+ fields: {
38
+ name: 'The node title (required)',
39
+ content: 'Source code content',
40
+ },
41
+ },
42
+ {
43
+ type: 'url',
44
+ defaultColor: 'blue',
45
+ description: 'Bookmark node that stores and displays a link to an external URL.',
46
+ usage: 'Use for saving references to external resources, articles, documentation, or any web link. The system will automatically fetch metadata like title and preview.',
47
+ fields: {
48
+ name: 'The link title (required)',
49
+ url: 'The full URL including protocol (e.g., https://example.com)',
50
+ },
51
+ },
52
+ ];
53
+ export function handleNodeTypes() {
54
+ return {
55
+ content: [
56
+ {
57
+ type: 'text',
58
+ text: JSON.stringify({
59
+ nodeTypes: NODE_TYPES,
60
+ note: 'When creating nodes via mentagen_create_node, specify the type parameter. Default is "text" if not specified.',
61
+ }, null, 2),
62
+ },
63
+ ],
64
+ };
65
+ }
@@ -0,0 +1,143 @@
1
+ import { z } from 'zod';
2
+ import { NodeType } from '../client/types.js';
3
+ import { formatError } from '../utils/errors.js';
4
+ const replaceOp = z.object({
5
+ type: z.literal('replace'),
6
+ oldString: z.string().min(1),
7
+ newString: z.string(),
8
+ replaceAll: z.boolean().optional(),
9
+ });
10
+ const prependOp = z.object({
11
+ type: z.literal('prepend'),
12
+ content: z.string(),
13
+ });
14
+ const appendOp = z.object({
15
+ type: z.literal('append'),
16
+ content: z.string(),
17
+ });
18
+ export const patchContentSchema = z.object({
19
+ boardId: z.string(),
20
+ nodeId: z.string(),
21
+ operations: z
22
+ .array(z.discriminatedUnion('type', [replaceOp, prependOp, appendOp]))
23
+ .min(1),
24
+ });
25
+ /**
26
+ * Determines the content field and value for a node.
27
+ * Code nodes use the `code` field, all others use `content`.
28
+ */
29
+ function getNodeTextField(node) {
30
+ if (node.type === NodeType.Code) {
31
+ return { field: 'code', text: node.code || '' };
32
+ }
33
+ return { field: 'content', text: node.content || '' };
34
+ }
35
+ /**
36
+ * Applies a string replacement operation.
37
+ * Returns the modified content or an error message.
38
+ */
39
+ export function applyReplace(content, op) {
40
+ if (op.replaceAll) {
41
+ if (!content.includes(op.oldString)) {
42
+ return { result: content, error: 'String not found in content' };
43
+ }
44
+ return { result: content.replaceAll(op.oldString, op.newString) };
45
+ }
46
+ const index = content.indexOf(op.oldString);
47
+ if (index === -1) {
48
+ return { result: content, error: 'String not found in content' };
49
+ }
50
+ const secondIndex = content.indexOf(op.oldString, index + op.oldString.length);
51
+ if (secondIndex !== -1) {
52
+ return {
53
+ result: content,
54
+ error: `String appears multiple times (at positions ${index} and ${secondIndex}). ` +
55
+ `Provide more surrounding context to make oldString unique, or use replaceAll: true.`,
56
+ };
57
+ }
58
+ return {
59
+ result: content.slice(0, index) +
60
+ op.newString +
61
+ content.slice(index + op.oldString.length),
62
+ };
63
+ }
64
+ export async function handlePatchContent(client, params) {
65
+ try {
66
+ const node = await client.getNode({
67
+ boardId: params.boardId,
68
+ nodeId: params.nodeId,
69
+ });
70
+ const { field, text: initialText } = getNodeTextField(node);
71
+ let text = initialText;
72
+ const appliedOps = [];
73
+ for (const op of params.operations) {
74
+ switch (op.type) {
75
+ case 'replace': {
76
+ const { result, error } = applyReplace(text, op);
77
+ if (error) {
78
+ return {
79
+ content: [
80
+ {
81
+ type: 'text',
82
+ text: JSON.stringify({
83
+ success: false,
84
+ error: `Replace failed: ${error}`,
85
+ field,
86
+ appliedOps,
87
+ contentLength: text.length,
88
+ }, null, 2),
89
+ },
90
+ ],
91
+ isError: true,
92
+ };
93
+ }
94
+ text = result;
95
+ appliedOps.push(`replace: "${truncate(op.oldString, 30)}" → "${truncate(op.newString, 30)}"`);
96
+ break;
97
+ }
98
+ case 'prepend':
99
+ text = op.content + text;
100
+ appliedOps.push(`prepend: ${op.content.length} chars`);
101
+ break;
102
+ case 'append':
103
+ text = text + op.content;
104
+ appliedOps.push(`append: ${op.content.length} chars`);
105
+ break;
106
+ }
107
+ }
108
+ await client.updateNode({
109
+ boardId: params.boardId,
110
+ nodeId: params.nodeId,
111
+ data: { [field]: text },
112
+ });
113
+ return {
114
+ content: [
115
+ {
116
+ type: 'text',
117
+ text: JSON.stringify({
118
+ success: true,
119
+ field,
120
+ operations: appliedOps,
121
+ newLength: text.length,
122
+ }, null, 2),
123
+ },
124
+ ],
125
+ };
126
+ }
127
+ catch (error) {
128
+ return {
129
+ content: [
130
+ {
131
+ type: 'text',
132
+ text: `Patch failed: ${formatError(error)}`,
133
+ },
134
+ ],
135
+ isError: true,
136
+ };
137
+ }
138
+ }
139
+ function truncate(str, maxLen) {
140
+ if (str.length <= maxLen)
141
+ return str;
142
+ return str.slice(0, maxLen - 3) + '...';
143
+ }
@@ -0,0 +1,41 @@
1
+ import { z } from 'zod';
2
+ import { formatError } from '../utils/errors.js';
3
+ import { getNodeUrl } from '../utils/urls.js';
4
+ export const readSchema = z.object({
5
+ boardId: z.string().describe('The board ID containing the node'),
6
+ nodeId: z.string().describe('The node ID to read'),
7
+ });
8
+ export async function handleRead(client, params, baseUrl) {
9
+ try {
10
+ const node = await client.getNode({
11
+ boardId: params.boardId,
12
+ nodeId: params.nodeId,
13
+ });
14
+ // Exclude large fields that aren't useful for AI consumption
15
+ const { _vectors, __v, textDetections, ...nodeData } = node;
16
+ // Add link to the response
17
+ const response = {
18
+ ...nodeData,
19
+ link: getNodeUrl(baseUrl, params.boardId, params.nodeId),
20
+ };
21
+ return {
22
+ content: [
23
+ {
24
+ type: 'text',
25
+ text: JSON.stringify(response, null, 2),
26
+ },
27
+ ],
28
+ };
29
+ }
30
+ catch (error) {
31
+ return {
32
+ content: [
33
+ {
34
+ type: 'text',
35
+ text: `Failed to read node: ${formatError(error)}`,
36
+ },
37
+ ],
38
+ isError: true,
39
+ };
40
+ }
41
+ }
@@ -0,0 +1,99 @@
1
+ import Papa from 'papaparse';
2
+ import { z } from 'zod';
3
+ import { formatError } from '../utils/errors.js';
4
+ import { formatSearchResults } from '../utils/formatting.js';
5
+ import { getNodeUrl } from '../utils/urls.js';
6
+ export const searchBoardSchema = z.object({
7
+ boardId: z.string().describe('The board ID to search within (required)'),
8
+ query: z
9
+ .string()
10
+ .describe('The search query - can be a question or keywords'),
11
+ limit: z
12
+ .number()
13
+ .default(10)
14
+ .describe('Maximum number of results (default: 10)'),
15
+ semanticRatio: z
16
+ .number()
17
+ .default(0.5)
18
+ .describe('Balance between semantic (1.0) and keyword (0.0) search'),
19
+ });
20
+ const MAX_CONTENT_LENGTH = 200;
21
+ /**
22
+ * Truncate content to save tokens, preserving word boundaries.
23
+ */
24
+ function truncateContent(content, maxLength) {
25
+ if (!content)
26
+ return '';
27
+ if (content.length <= maxLength)
28
+ return content;
29
+ const truncated = content.slice(0, maxLength);
30
+ const lastSpace = truncated.lastIndexOf(' ');
31
+ return ((lastSpace > maxLength * 0.7 ? truncated.slice(0, lastSpace) : truncated) +
32
+ '...');
33
+ }
34
+ /**
35
+ * Format search results as TSV for compact output.
36
+ */
37
+ function formatSearchTsv(results) {
38
+ const rows = results.map(r => ({
39
+ id: r.id,
40
+ name: r.name,
41
+ content: truncateContent(r.content, MAX_CONTENT_LENGTH),
42
+ type: r.type,
43
+ score: Math.round(r.score * 100) / 100,
44
+ link: r.link,
45
+ }));
46
+ return Papa.unparse(rows, {
47
+ delimiter: '\t',
48
+ header: true,
49
+ newline: '\n',
50
+ columns: ['id', 'name', 'content', 'type', 'score', 'link'],
51
+ });
52
+ }
53
+ export async function handleSearchBoard(client, params, baseUrl) {
54
+ try {
55
+ const results = await client.searchNodes({
56
+ query: params.query,
57
+ board: params.boardId,
58
+ semanticRatio: params.semanticRatio,
59
+ });
60
+ if (results.hits.length === 0) {
61
+ return {
62
+ content: [
63
+ {
64
+ type: 'text',
65
+ text: `No nodes found matching "${params.query}" in this board.`,
66
+ },
67
+ ],
68
+ };
69
+ }
70
+ const formatted = formatSearchResults(results.hits.slice(0, params.limit));
71
+ const resultsWithLinks = formatted.results.map(r => ({
72
+ id: r.id,
73
+ name: r.name,
74
+ content: r.content,
75
+ type: r.type,
76
+ score: r.score,
77
+ link: getNodeUrl(baseUrl, params.boardId, r.id),
78
+ }));
79
+ return {
80
+ content: [
81
+ {
82
+ type: 'text',
83
+ text: formatSearchTsv(resultsWithLinks),
84
+ },
85
+ ],
86
+ };
87
+ }
88
+ catch (error) {
89
+ return {
90
+ content: [
91
+ {
92
+ type: 'text',
93
+ text: `Search failed: ${formatError(error)}`,
94
+ },
95
+ ],
96
+ isError: true,
97
+ };
98
+ }
99
+ }
@@ -0,0 +1,50 @@
1
+ import Papa from 'papaparse';
2
+ import { z } from 'zod';
3
+ import { formatShortDate } from '../utils/ejson.js';
4
+ import { formatError } from '../utils/errors.js';
5
+ import { getBoardUrl } from '../utils/urls.js';
6
+ export const searchBoardsSchema = z.object({
7
+ query: z.string().describe('Search query to filter boards by name'),
8
+ });
9
+ /**
10
+ * Format boards as TSV for compact output.
11
+ */
12
+ function formatBoardsTsv(boards) {
13
+ return Papa.unparse(boards, {
14
+ delimiter: '\t',
15
+ header: true,
16
+ newline: '\n',
17
+ columns: ['id', 'name', 'nodeCount', 'updatedAt', 'link'],
18
+ });
19
+ }
20
+ export async function handleSearchBoards(client, params, baseUrl) {
21
+ try {
22
+ const boards = await client.searchBoards(params.query);
23
+ const formatted = boards.map(board => ({
24
+ id: board._id,
25
+ name: board.name,
26
+ nodeCount: board.nodeCount ?? 0,
27
+ updatedAt: formatShortDate(board.updatedAt),
28
+ link: getBoardUrl(baseUrl, board._id),
29
+ }));
30
+ return {
31
+ content: [
32
+ {
33
+ type: 'text',
34
+ text: formatBoardsTsv(formatted),
35
+ },
36
+ ],
37
+ };
38
+ }
39
+ catch (error) {
40
+ return {
41
+ content: [
42
+ {
43
+ type: 'text',
44
+ text: `Failed to search boards: ${formatError(error)}`,
45
+ },
46
+ ],
47
+ isError: true,
48
+ };
49
+ }
50
+ }