@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,61 @@
|
|
|
1
|
+
import { z } from 'zod';
|
|
2
|
+
import { formatError } from '../utils/errors.js';
|
|
3
|
+
export const createEdgeSchema = z.object({
|
|
4
|
+
boardId: z.string().describe('The board ID where the edge will be created'),
|
|
5
|
+
source: z.string().describe('The source node ID'),
|
|
6
|
+
target: z.string().describe('The target node ID'),
|
|
7
|
+
direction: z
|
|
8
|
+
.enum(['none', 'source', 'target'])
|
|
9
|
+
.optional()
|
|
10
|
+
.describe('Arrow direction: "none" (no arrow, default), "source" (arrow points to source), "target" (arrow points to target)'),
|
|
11
|
+
});
|
|
12
|
+
/**
|
|
13
|
+
* Generate a simple ObjectId-like string.
|
|
14
|
+
*/
|
|
15
|
+
export function generateId() {
|
|
16
|
+
const timestamp = Math.floor(Date.now() / 1000)
|
|
17
|
+
.toString(16)
|
|
18
|
+
.padStart(8, '0');
|
|
19
|
+
const random = Array.from({ length: 16 }, () => Math.floor(Math.random() * 16).toString(16)).join('');
|
|
20
|
+
return timestamp + random;
|
|
21
|
+
}
|
|
22
|
+
export async function handleCreateEdge(client, params) {
|
|
23
|
+
try {
|
|
24
|
+
const _id = generateId();
|
|
25
|
+
const edge = await client.addEdge({
|
|
26
|
+
_id,
|
|
27
|
+
boardId: params.boardId,
|
|
28
|
+
source: params.source,
|
|
29
|
+
target: params.target,
|
|
30
|
+
direction: params.direction,
|
|
31
|
+
});
|
|
32
|
+
const result = {
|
|
33
|
+
success: true,
|
|
34
|
+
edge: {
|
|
35
|
+
id: edge._id,
|
|
36
|
+
source: edge.source,
|
|
37
|
+
target: edge.target,
|
|
38
|
+
board: params.boardId,
|
|
39
|
+
},
|
|
40
|
+
};
|
|
41
|
+
return {
|
|
42
|
+
content: [
|
|
43
|
+
{
|
|
44
|
+
type: 'text',
|
|
45
|
+
text: JSON.stringify(result, null, 2),
|
|
46
|
+
},
|
|
47
|
+
],
|
|
48
|
+
};
|
|
49
|
+
}
|
|
50
|
+
catch (error) {
|
|
51
|
+
return {
|
|
52
|
+
content: [
|
|
53
|
+
{
|
|
54
|
+
type: 'text',
|
|
55
|
+
text: `Failed to create edge: ${formatError(error)}`,
|
|
56
|
+
},
|
|
57
|
+
],
|
|
58
|
+
isError: true,
|
|
59
|
+
};
|
|
60
|
+
}
|
|
61
|
+
}
|
|
@@ -0,0 +1,174 @@
|
|
|
1
|
+
import { z } from 'zod';
|
|
2
|
+
import { formatError } from '../utils/errors.js';
|
|
3
|
+
import { generateId } from './create.js';
|
|
4
|
+
import { calculateNodeSize } from './size-calc.js';
|
|
5
|
+
const nodeInputSchema = z.object({
|
|
6
|
+
tempId: z.string().describe('Temporary ID for referencing in edges'),
|
|
7
|
+
name: z.string().describe('The node title/name'),
|
|
8
|
+
content: z.string().optional().describe('Node content'),
|
|
9
|
+
type: z
|
|
10
|
+
.enum(['text', 'markdown', 'code', 'url'])
|
|
11
|
+
.optional()
|
|
12
|
+
.default('text')
|
|
13
|
+
.describe('Node type'),
|
|
14
|
+
color: z.string().optional().describe('Node color'),
|
|
15
|
+
x: z.number().optional().describe('X position'),
|
|
16
|
+
y: z.number().optional().describe('Y position'),
|
|
17
|
+
width: z.number().optional().describe('Width'),
|
|
18
|
+
height: z.number().optional().describe('Height'),
|
|
19
|
+
});
|
|
20
|
+
const edgeInputSchema = z.object({
|
|
21
|
+
source: z.string().describe('Source node tempId'),
|
|
22
|
+
target: z.string().describe('Target node tempId'),
|
|
23
|
+
direction: z
|
|
24
|
+
.enum(['none', 'source', 'target'])
|
|
25
|
+
.optional()
|
|
26
|
+
.describe('Arrow direction'),
|
|
27
|
+
label: z.string().optional().describe('Edge label'),
|
|
28
|
+
});
|
|
29
|
+
export const createGraphSchema = z.object({
|
|
30
|
+
boardId: z.string().describe('The board ID to create the graph on'),
|
|
31
|
+
nodes: z.array(nodeInputSchema).min(1).describe('Array of nodes to create'),
|
|
32
|
+
edges: z
|
|
33
|
+
.array(edgeInputSchema)
|
|
34
|
+
.optional()
|
|
35
|
+
.describe('Array of edges connecting nodes by tempId'),
|
|
36
|
+
});
|
|
37
|
+
const DEFAULT_NODE_COLORS = {
|
|
38
|
+
text: undefined,
|
|
39
|
+
markdown: 'cyan',
|
|
40
|
+
code: 'indigo',
|
|
41
|
+
url: 'blue',
|
|
42
|
+
};
|
|
43
|
+
async function createNode(client, boardId, nodeInput, idMap) {
|
|
44
|
+
const realId = generateId();
|
|
45
|
+
const type = nodeInput.type ?? 'text';
|
|
46
|
+
const color = nodeInput.color ?? DEFAULT_NODE_COLORS[type];
|
|
47
|
+
const autoSize = calculateNodeSize(nodeInput.name, type);
|
|
48
|
+
const x = nodeInput.x ?? 100;
|
|
49
|
+
const y = nodeInput.y ?? 100;
|
|
50
|
+
const width = nodeInput.width ?? autoSize.width;
|
|
51
|
+
const height = nodeInput.height ?? autoSize.height;
|
|
52
|
+
try {
|
|
53
|
+
// For code nodes, store content in the `code` field
|
|
54
|
+
const isCodeNode = type === 'code';
|
|
55
|
+
const nodeContent = nodeInput.content ?? '';
|
|
56
|
+
const node = await client.addNode({
|
|
57
|
+
_id: realId,
|
|
58
|
+
boardId,
|
|
59
|
+
node: {
|
|
60
|
+
name: nodeInput.name,
|
|
61
|
+
...(isCodeNode ? { code: nodeContent } : { content: nodeContent }),
|
|
62
|
+
type,
|
|
63
|
+
color,
|
|
64
|
+
x,
|
|
65
|
+
y,
|
|
66
|
+
width,
|
|
67
|
+
height,
|
|
68
|
+
},
|
|
69
|
+
});
|
|
70
|
+
idMap[nodeInput.tempId] = node._id;
|
|
71
|
+
return {
|
|
72
|
+
success: true,
|
|
73
|
+
result: {
|
|
74
|
+
tempId: nodeInput.tempId,
|
|
75
|
+
id: node._id,
|
|
76
|
+
name: node.name,
|
|
77
|
+
x,
|
|
78
|
+
y,
|
|
79
|
+
width,
|
|
80
|
+
height,
|
|
81
|
+
},
|
|
82
|
+
};
|
|
83
|
+
}
|
|
84
|
+
catch (error) {
|
|
85
|
+
return {
|
|
86
|
+
success: false,
|
|
87
|
+
tempId: nodeInput.tempId,
|
|
88
|
+
error: formatError(error),
|
|
89
|
+
};
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
async function createEdge(client, boardId, edgeInput, idMap) {
|
|
93
|
+
const sourceId = idMap[edgeInput.source];
|
|
94
|
+
const targetId = idMap[edgeInput.target];
|
|
95
|
+
if (!sourceId || !targetId) {
|
|
96
|
+
return {
|
|
97
|
+
success: false,
|
|
98
|
+
error: `Invalid edge: source=${edgeInput.source} target=${edgeInput.target}`,
|
|
99
|
+
};
|
|
100
|
+
}
|
|
101
|
+
try {
|
|
102
|
+
const realId = generateId();
|
|
103
|
+
const edge = await client.addEdge({
|
|
104
|
+
_id: realId,
|
|
105
|
+
boardId,
|
|
106
|
+
source: sourceId,
|
|
107
|
+
target: targetId,
|
|
108
|
+
direction: edgeInput.direction,
|
|
109
|
+
});
|
|
110
|
+
return {
|
|
111
|
+
success: true,
|
|
112
|
+
result: {
|
|
113
|
+
id: edge._id,
|
|
114
|
+
source: sourceId,
|
|
115
|
+
target: targetId,
|
|
116
|
+
},
|
|
117
|
+
};
|
|
118
|
+
}
|
|
119
|
+
catch (error) {
|
|
120
|
+
return {
|
|
121
|
+
success: false,
|
|
122
|
+
error: formatError(error),
|
|
123
|
+
};
|
|
124
|
+
}
|
|
125
|
+
}
|
|
126
|
+
function processNodeOutcomes(outcomes) {
|
|
127
|
+
const results = [];
|
|
128
|
+
const errors = [];
|
|
129
|
+
for (const outcome of outcomes) {
|
|
130
|
+
if (outcome.success && outcome.result) {
|
|
131
|
+
results.push(outcome.result);
|
|
132
|
+
}
|
|
133
|
+
else if (!outcome.success && outcome.tempId) {
|
|
134
|
+
errors.push({ tempId: outcome.tempId, error: outcome.error });
|
|
135
|
+
}
|
|
136
|
+
}
|
|
137
|
+
return { results, errors };
|
|
138
|
+
}
|
|
139
|
+
function processEdgeOutcomes(outcomes) {
|
|
140
|
+
return outcomes
|
|
141
|
+
.filter((o) => o.success && !!o.result)
|
|
142
|
+
.map(o => o.result);
|
|
143
|
+
}
|
|
144
|
+
export async function handleCreateGraph(client, params) {
|
|
145
|
+
try {
|
|
146
|
+
const idMap = {};
|
|
147
|
+
const nodeOutcomes = await Promise.all(params.nodes.map(n => createNode(client, params.boardId, n, idMap)));
|
|
148
|
+
const { results: nodeResults, errors } = processNodeOutcomes(nodeOutcomes);
|
|
149
|
+
let edgeResults = [];
|
|
150
|
+
if (errors.length === 0 && params.edges && params.edges.length > 0) {
|
|
151
|
+
const edgeOutcomes = await Promise.all(params.edges.map(e => createEdge(client, params.boardId, e, idMap)));
|
|
152
|
+
edgeResults = processEdgeOutcomes(edgeOutcomes);
|
|
153
|
+
}
|
|
154
|
+
const response = {
|
|
155
|
+
success: errors.length === 0,
|
|
156
|
+
idMap,
|
|
157
|
+
nodes: nodeResults,
|
|
158
|
+
edges: edgeResults,
|
|
159
|
+
...(errors.length > 0 && { errors }),
|
|
160
|
+
};
|
|
161
|
+
return {
|
|
162
|
+
content: [{ type: 'text', text: JSON.stringify(response, null, 2) }],
|
|
163
|
+
...(errors.length === params.nodes.length && { isError: true }),
|
|
164
|
+
};
|
|
165
|
+
}
|
|
166
|
+
catch (error) {
|
|
167
|
+
return {
|
|
168
|
+
content: [
|
|
169
|
+
{ type: 'text', text: `Failed to create graph: ${formatError(error)}` },
|
|
170
|
+
],
|
|
171
|
+
isError: true,
|
|
172
|
+
};
|
|
173
|
+
}
|
|
174
|
+
}
|
|
@@ -0,0 +1,152 @@
|
|
|
1
|
+
import { z } from 'zod';
|
|
2
|
+
import { formatError } from '../utils/errors.js';
|
|
3
|
+
import { getNodeUrl } from '../utils/urls.js';
|
|
4
|
+
import { snapToGrid } from './grid-calc.js';
|
|
5
|
+
import { calculateNodeSize } from './size-calc.js';
|
|
6
|
+
/**
|
|
7
|
+
* Default colors for node types, matching the context menu behavior.
|
|
8
|
+
*
|
|
9
|
+
* Keep in sync with: src/client/features/boards/inner-board-context-menu.tsx
|
|
10
|
+
*
|
|
11
|
+
* When adding new node types or changing default colors in the context menu,
|
|
12
|
+
* update this mapping to match.
|
|
13
|
+
*/
|
|
14
|
+
const DEFAULT_NODE_COLORS = {
|
|
15
|
+
// Currently supported in MCP
|
|
16
|
+
text: undefined,
|
|
17
|
+
markdown: 'cyan',
|
|
18
|
+
code: 'indigo',
|
|
19
|
+
url: 'blue',
|
|
20
|
+
// Future node types (from context menu)
|
|
21
|
+
image: undefined,
|
|
22
|
+
'ai-chat': 'blue',
|
|
23
|
+
'ai-quiz': 'green',
|
|
24
|
+
board: 'fuchsia',
|
|
25
|
+
mermaid: 'violet',
|
|
26
|
+
voice: undefined,
|
|
27
|
+
excalidraw: 'teal',
|
|
28
|
+
teleprompter: 'rose',
|
|
29
|
+
};
|
|
30
|
+
export const createSchema = z.object({
|
|
31
|
+
boardId: z.string().describe('The board ID to create the node on'),
|
|
32
|
+
name: z.string().describe('The node title/name'),
|
|
33
|
+
content: z
|
|
34
|
+
.string()
|
|
35
|
+
.optional()
|
|
36
|
+
.describe('Content for text/markdown nodes. Use "url" field for url-type nodes.'),
|
|
37
|
+
url: z
|
|
38
|
+
.string()
|
|
39
|
+
.optional()
|
|
40
|
+
.describe('The URL for url-type nodes (e.g., https://example.com). Required for type="url".'),
|
|
41
|
+
type: z
|
|
42
|
+
.enum(['text', 'markdown', 'code', 'url'])
|
|
43
|
+
.optional()
|
|
44
|
+
.default('text')
|
|
45
|
+
.describe('Node type: text (default), markdown, code, or url'),
|
|
46
|
+
color: z
|
|
47
|
+
.string()
|
|
48
|
+
.optional()
|
|
49
|
+
.describe('Optional color override. If not specified, uses default for type (markdown=cyan, code=indigo, url=blue)'),
|
|
50
|
+
x: z
|
|
51
|
+
.number()
|
|
52
|
+
.optional()
|
|
53
|
+
.describe('X position (must be multiple of 16, default: 96)'),
|
|
54
|
+
y: z
|
|
55
|
+
.number()
|
|
56
|
+
.optional()
|
|
57
|
+
.describe('Y position (must be multiple of 16, default: 96)'),
|
|
58
|
+
width: z
|
|
59
|
+
.number()
|
|
60
|
+
.optional()
|
|
61
|
+
.describe('Width (must be multiple of 16, default: 256)'),
|
|
62
|
+
height: z
|
|
63
|
+
.number()
|
|
64
|
+
.optional()
|
|
65
|
+
.describe('Height (must be multiple of 16, default: 128)'),
|
|
66
|
+
});
|
|
67
|
+
/**
|
|
68
|
+
* Generate a simple ObjectId-like string.
|
|
69
|
+
* Uses timestamp + random hex for uniqueness.
|
|
70
|
+
*/
|
|
71
|
+
export function generateId() {
|
|
72
|
+
const timestamp = Math.floor(Date.now() / 1000)
|
|
73
|
+
.toString(16)
|
|
74
|
+
.padStart(8, '0');
|
|
75
|
+
const random = Array.from({ length: 16 }, () => Math.floor(Math.random() * 16).toString(16)).join('');
|
|
76
|
+
return timestamp + random;
|
|
77
|
+
}
|
|
78
|
+
/**
|
|
79
|
+
* Build node fields object with content mapped to the correct field based on type.
|
|
80
|
+
*/
|
|
81
|
+
function buildNodeFields(params, color, position) {
|
|
82
|
+
const nodeFields = {
|
|
83
|
+
name: params.name,
|
|
84
|
+
type: params.type,
|
|
85
|
+
color,
|
|
86
|
+
...position,
|
|
87
|
+
};
|
|
88
|
+
if (params.type === 'code') {
|
|
89
|
+
nodeFields.code = params.content || '';
|
|
90
|
+
}
|
|
91
|
+
else if (params.type === 'url') {
|
|
92
|
+
if (!params.url) {
|
|
93
|
+
throw new Error('URL field is required for url-type nodes');
|
|
94
|
+
}
|
|
95
|
+
nodeFields.url = params.url;
|
|
96
|
+
if (params.content) {
|
|
97
|
+
nodeFields.content = params.content;
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
else {
|
|
101
|
+
nodeFields.content = params.content || '';
|
|
102
|
+
}
|
|
103
|
+
return nodeFields;
|
|
104
|
+
}
|
|
105
|
+
export async function handleCreate(client, params, mentagenUrl) {
|
|
106
|
+
try {
|
|
107
|
+
const _id = generateId();
|
|
108
|
+
const color = params.color || DEFAULT_NODE_COLORS[params.type];
|
|
109
|
+
const autoSize = calculateNodeSize(params.name, params.type);
|
|
110
|
+
const position = {
|
|
111
|
+
x: snapToGrid(params.x ?? 96),
|
|
112
|
+
y: snapToGrid(params.y ?? 96),
|
|
113
|
+
width: snapToGrid(params.width ?? autoSize.width),
|
|
114
|
+
height: snapToGrid(params.height ?? autoSize.height),
|
|
115
|
+
};
|
|
116
|
+
const nodeFields = buildNodeFields(params, color, position);
|
|
117
|
+
const node = await client.addNode({
|
|
118
|
+
_id,
|
|
119
|
+
boardId: params.boardId,
|
|
120
|
+
node: nodeFields,
|
|
121
|
+
});
|
|
122
|
+
const result = {
|
|
123
|
+
success: true,
|
|
124
|
+
node: {
|
|
125
|
+
id: node._id,
|
|
126
|
+
name: node.name,
|
|
127
|
+
board: params.boardId,
|
|
128
|
+
...position,
|
|
129
|
+
link: getNodeUrl(mentagenUrl, params.boardId, node._id),
|
|
130
|
+
},
|
|
131
|
+
};
|
|
132
|
+
return {
|
|
133
|
+
content: [
|
|
134
|
+
{
|
|
135
|
+
type: 'text',
|
|
136
|
+
text: JSON.stringify(result, null, 2),
|
|
137
|
+
},
|
|
138
|
+
],
|
|
139
|
+
};
|
|
140
|
+
}
|
|
141
|
+
catch (error) {
|
|
142
|
+
return {
|
|
143
|
+
content: [
|
|
144
|
+
{
|
|
145
|
+
type: 'text',
|
|
146
|
+
text: `Failed to create node: ${formatError(error)}`,
|
|
147
|
+
},
|
|
148
|
+
],
|
|
149
|
+
isError: true,
|
|
150
|
+
};
|
|
151
|
+
}
|
|
152
|
+
}
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
import { z } from 'zod';
|
|
2
|
+
import { formatError } from '../utils/errors.js';
|
|
3
|
+
export const deleteEdgeSchema = z.object({
|
|
4
|
+
boardId: z.string().describe('The board ID containing the edge'),
|
|
5
|
+
edgeId: z.string().describe('The edge ID to delete'),
|
|
6
|
+
});
|
|
7
|
+
export async function handleDeleteEdge(client, params) {
|
|
8
|
+
try {
|
|
9
|
+
await client.deleteEdge({
|
|
10
|
+
boardId: params.boardId,
|
|
11
|
+
edgeId: params.edgeId,
|
|
12
|
+
});
|
|
13
|
+
return {
|
|
14
|
+
content: [
|
|
15
|
+
{
|
|
16
|
+
type: 'text',
|
|
17
|
+
text: JSON.stringify({
|
|
18
|
+
success: true,
|
|
19
|
+
message: 'Edge deleted successfully. This is a soft delete and can be undone in Mentagen.',
|
|
20
|
+
}, null, 2),
|
|
21
|
+
},
|
|
22
|
+
],
|
|
23
|
+
};
|
|
24
|
+
}
|
|
25
|
+
catch (error) {
|
|
26
|
+
return {
|
|
27
|
+
content: [
|
|
28
|
+
{
|
|
29
|
+
type: 'text',
|
|
30
|
+
text: `Failed to delete edge: ${formatError(error)}`,
|
|
31
|
+
},
|
|
32
|
+
],
|
|
33
|
+
isError: true,
|
|
34
|
+
};
|
|
35
|
+
}
|
|
36
|
+
}
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
import { z } from 'zod';
|
|
2
|
+
import { formatError } from '../utils/errors.js';
|
|
3
|
+
export const deleteSchema = z.object({
|
|
4
|
+
boardId: z.string().describe('The board ID containing the node'),
|
|
5
|
+
nodeId: z.string().describe('The node ID to delete'),
|
|
6
|
+
});
|
|
7
|
+
export async function handleDelete(client, params) {
|
|
8
|
+
try {
|
|
9
|
+
await client.deleteNode({
|
|
10
|
+
boardId: params.boardId,
|
|
11
|
+
nodeId: params.nodeId,
|
|
12
|
+
});
|
|
13
|
+
return {
|
|
14
|
+
content: [
|
|
15
|
+
{
|
|
16
|
+
type: 'text',
|
|
17
|
+
text: JSON.stringify({
|
|
18
|
+
success: true,
|
|
19
|
+
message: 'Node deleted successfully. This is a soft delete and can be undone in Mentagen.',
|
|
20
|
+
}, null, 2),
|
|
21
|
+
},
|
|
22
|
+
],
|
|
23
|
+
};
|
|
24
|
+
}
|
|
25
|
+
catch (error) {
|
|
26
|
+
return {
|
|
27
|
+
content: [
|
|
28
|
+
{
|
|
29
|
+
type: 'text',
|
|
30
|
+
text: `Failed to delete node: ${formatError(error)}`,
|
|
31
|
+
},
|
|
32
|
+
],
|
|
33
|
+
isError: true,
|
|
34
|
+
};
|
|
35
|
+
}
|
|
36
|
+
}
|
|
@@ -0,0 +1,207 @@
|
|
|
1
|
+
import { z } from 'zod';
|
|
2
|
+
import { NodeType } from '../client/types.js';
|
|
3
|
+
import { formatError } from '../utils/errors.js';
|
|
4
|
+
import { getNodeUrl } from '../utils/urls.js';
|
|
5
|
+
export const extractBoardContentSchema = z.object({
|
|
6
|
+
boardId: z.string().describe('The board ID to extract content from'),
|
|
7
|
+
includeEmpty: z
|
|
8
|
+
.boolean()
|
|
9
|
+
.default(false)
|
|
10
|
+
.describe('Include nodes with no content (default: false)'),
|
|
11
|
+
});
|
|
12
|
+
/**
|
|
13
|
+
* Content extractors by node type.
|
|
14
|
+
* Returns the appropriate text content from a node based on its type.
|
|
15
|
+
*/
|
|
16
|
+
const contentExtractors = {
|
|
17
|
+
[NodeType.Code]: node => node.code || '',
|
|
18
|
+
[NodeType.AIChat]: node => node.chatConcatenated || '',
|
|
19
|
+
[NodeType.Image]: node => node.textDetectionsConcatenated || '',
|
|
20
|
+
[NodeType.PDF]: node => node.pdfText || node.content || '',
|
|
21
|
+
[NodeType.URL]: node => node.article || node.content || '',
|
|
22
|
+
};
|
|
23
|
+
/**
|
|
24
|
+
* Get the text content from a node, handling different field locations by type.
|
|
25
|
+
*/
|
|
26
|
+
function getNodeContent(node) {
|
|
27
|
+
const extractor = contentExtractors[node.type];
|
|
28
|
+
return extractor ? extractor(node) : node.content || '';
|
|
29
|
+
}
|
|
30
|
+
/**
|
|
31
|
+
* Strip HTML tags from content for cleaner output.
|
|
32
|
+
*/
|
|
33
|
+
function stripHtml(html) {
|
|
34
|
+
return html
|
|
35
|
+
.replace(/<[^>]*>/g, '')
|
|
36
|
+
.replace(/ /g, ' ')
|
|
37
|
+
.replace(/&/g, '&')
|
|
38
|
+
.replace(/</g, '<')
|
|
39
|
+
.replace(/>/g, '>')
|
|
40
|
+
.replace(/"/g, '"')
|
|
41
|
+
.replace(/'/g, "'")
|
|
42
|
+
.trim();
|
|
43
|
+
}
|
|
44
|
+
/**
|
|
45
|
+
* Infer programming language from file extension.
|
|
46
|
+
*/
|
|
47
|
+
function inferLanguage(filename) {
|
|
48
|
+
const ext = filename.split('.').pop()?.toLowerCase() || '';
|
|
49
|
+
const langMap = {
|
|
50
|
+
ts: 'typescript',
|
|
51
|
+
tsx: 'typescript',
|
|
52
|
+
js: 'javascript',
|
|
53
|
+
jsx: 'javascript',
|
|
54
|
+
py: 'python',
|
|
55
|
+
rb: 'ruby',
|
|
56
|
+
go: 'go',
|
|
57
|
+
rs: 'rust',
|
|
58
|
+
java: 'java',
|
|
59
|
+
kt: 'kotlin',
|
|
60
|
+
swift: 'swift',
|
|
61
|
+
c: 'c',
|
|
62
|
+
cpp: 'cpp',
|
|
63
|
+
h: 'c',
|
|
64
|
+
hpp: 'cpp',
|
|
65
|
+
cs: 'csharp',
|
|
66
|
+
php: 'php',
|
|
67
|
+
sql: 'sql',
|
|
68
|
+
sh: 'bash',
|
|
69
|
+
bash: 'bash',
|
|
70
|
+
zsh: 'bash',
|
|
71
|
+
yml: 'yaml',
|
|
72
|
+
yaml: 'yaml',
|
|
73
|
+
json: 'json',
|
|
74
|
+
xml: 'xml',
|
|
75
|
+
html: 'html',
|
|
76
|
+
css: 'css',
|
|
77
|
+
scss: 'scss',
|
|
78
|
+
md: 'markdown',
|
|
79
|
+
};
|
|
80
|
+
return langMap[ext] || '';
|
|
81
|
+
}
|
|
82
|
+
/**
|
|
83
|
+
* Format node content based on its type.
|
|
84
|
+
*/
|
|
85
|
+
function formatContent(node) {
|
|
86
|
+
const raw = getNodeContent(node);
|
|
87
|
+
if (!raw)
|
|
88
|
+
return '';
|
|
89
|
+
// Code nodes: wrap in code block
|
|
90
|
+
if (node.type === NodeType.Code) {
|
|
91
|
+
const lang = inferLanguage(node.name);
|
|
92
|
+
return '```' + lang + '\n' + raw + '\n```';
|
|
93
|
+
}
|
|
94
|
+
// Markdown nodes: keep as-is (already markdown)
|
|
95
|
+
if (node.type === NodeType.Markdown) {
|
|
96
|
+
return raw;
|
|
97
|
+
}
|
|
98
|
+
// AI Chat nodes: convert server format to markdown
|
|
99
|
+
if (node.type === NodeType.AIChat) {
|
|
100
|
+
// Server returns "User: text\n\nAssistant: text", convert to markdown bold
|
|
101
|
+
return raw
|
|
102
|
+
.replace(/^User:/gm, '**User:**')
|
|
103
|
+
.replace(/^Assistant:/gm, '**Assistant:**');
|
|
104
|
+
}
|
|
105
|
+
// Image nodes with OCR: prefix with context
|
|
106
|
+
if (node.type === NodeType.Image && node.textDetectionsConcatenated) {
|
|
107
|
+
return `*Text detected in image:*\n\n${raw}`;
|
|
108
|
+
}
|
|
109
|
+
// PDF nodes: prefix with context
|
|
110
|
+
if (node.type === NodeType.PDF) {
|
|
111
|
+
return `*Extracted from PDF:*\n\n${raw}`;
|
|
112
|
+
}
|
|
113
|
+
// Text nodes with HTML: strip tags
|
|
114
|
+
if (raw.includes('<')) {
|
|
115
|
+
return stripHtml(raw);
|
|
116
|
+
}
|
|
117
|
+
return raw;
|
|
118
|
+
}
|
|
119
|
+
/**
|
|
120
|
+
* Format a single node as a markdown section.
|
|
121
|
+
*/
|
|
122
|
+
function formatNodeSection(node, baseUrl, boardId) {
|
|
123
|
+
const content = formatContent(node);
|
|
124
|
+
const link = getNodeUrl(baseUrl, boardId, node._id);
|
|
125
|
+
const lines = [
|
|
126
|
+
`## ${node.name}`,
|
|
127
|
+
`**ID:** \`${node._id}\` | **Type:** ${node.type} | [Open](${link})`,
|
|
128
|
+
'',
|
|
129
|
+
content,
|
|
130
|
+
];
|
|
131
|
+
return lines.join('\n');
|
|
132
|
+
}
|
|
133
|
+
export async function handleExtractBoardContent(client, params, baseUrl) {
|
|
134
|
+
try {
|
|
135
|
+
// Fetch board metadata and nodes in parallel
|
|
136
|
+
const [board, nodes] = await Promise.all([
|
|
137
|
+
client.getBoard({ boardId: params.boardId }),
|
|
138
|
+
client.listNodesWithContent({
|
|
139
|
+
boardId: params.boardId,
|
|
140
|
+
limit: 1000,
|
|
141
|
+
}),
|
|
142
|
+
]);
|
|
143
|
+
if (nodes.length === 0) {
|
|
144
|
+
return {
|
|
145
|
+
content: [
|
|
146
|
+
{
|
|
147
|
+
type: 'text',
|
|
148
|
+
text: 'No nodes found in this board.',
|
|
149
|
+
},
|
|
150
|
+
],
|
|
151
|
+
};
|
|
152
|
+
}
|
|
153
|
+
// Filter nodes based on content
|
|
154
|
+
const nodesWithContent = params.includeEmpty
|
|
155
|
+
? nodes
|
|
156
|
+
: nodes.filter(n => {
|
|
157
|
+
const content = getNodeContent(n);
|
|
158
|
+
return content && content.trim().length > 0;
|
|
159
|
+
});
|
|
160
|
+
if (nodesWithContent.length === 0) {
|
|
161
|
+
return {
|
|
162
|
+
content: [
|
|
163
|
+
{
|
|
164
|
+
type: 'text',
|
|
165
|
+
text: `Board has ${nodes.length} nodes but none contain extractable content.`,
|
|
166
|
+
},
|
|
167
|
+
],
|
|
168
|
+
};
|
|
169
|
+
}
|
|
170
|
+
// Build the markdown output
|
|
171
|
+
const sections = nodesWithContent.map(n => formatNodeSection(n, baseUrl, params.boardId));
|
|
172
|
+
// Build board link
|
|
173
|
+
const boardLink = `${baseUrl}/b/${params.boardId}`;
|
|
174
|
+
// Add summary header with board metadata
|
|
175
|
+
const headerLines = [
|
|
176
|
+
`# ${board.name}`,
|
|
177
|
+
'',
|
|
178
|
+
`**Board ID:** \`${params.boardId}\` | [Open Board](${boardLink})`,
|
|
179
|
+
];
|
|
180
|
+
// Add description if present
|
|
181
|
+
if (board.description) {
|
|
182
|
+
headerLines.push('', board.description);
|
|
183
|
+
}
|
|
184
|
+
headerLines.push('', '---', '', `**Total nodes:** ${nodesWithContent.length}${nodes.length !== nodesWithContent.length ? ` (${nodes.length - nodesWithContent.length} empty nodes excluded)` : ''}`, '', '---', '');
|
|
185
|
+
const header = headerLines.join('\n');
|
|
186
|
+
const output = header + sections.join('\n\n---\n\n');
|
|
187
|
+
return {
|
|
188
|
+
content: [
|
|
189
|
+
{
|
|
190
|
+
type: 'text',
|
|
191
|
+
text: output,
|
|
192
|
+
},
|
|
193
|
+
],
|
|
194
|
+
};
|
|
195
|
+
}
|
|
196
|
+
catch (error) {
|
|
197
|
+
return {
|
|
198
|
+
content: [
|
|
199
|
+
{
|
|
200
|
+
type: 'text',
|
|
201
|
+
text: `Failed to extract board content: ${formatError(error)}`,
|
|
202
|
+
},
|
|
203
|
+
],
|
|
204
|
+
isError: true,
|
|
205
|
+
};
|
|
206
|
+
}
|
|
207
|
+
}
|