@mentagen/mcp 0.7.0 → 0.8.1
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/index.js +3 -0
- package/dist/prompts/index.js +259 -0
- package/dist/tools/batch-update.js +91 -30
- package/dist/tools/create-graph.js +43 -22
- package/dist/tools/create.js +36 -5
- package/dist/tools/extract-board-content.js +20 -2
- package/dist/tools/index.js +110 -2
- package/dist/tools/list-nodes.js +9 -0
- package/dist/tools/size-calc.js +1 -1
- package/dist/tools/todo.js +171 -0
- package/dist/tools/update.js +133 -18
- package/package.json +1 -1
package/dist/index.js
CHANGED
|
@@ -2,6 +2,7 @@
|
|
|
2
2
|
import { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js';
|
|
3
3
|
import { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js';
|
|
4
4
|
import { createMentagenClient } from './client/helene.js';
|
|
5
|
+
import { registerPrompts } from './prompts/index.js';
|
|
5
6
|
import { registerResources } from './resources/index.js';
|
|
6
7
|
import { registerTools } from './tools/index.js';
|
|
7
8
|
import { loadConfig } from './utils/config.js';
|
|
@@ -15,9 +16,11 @@ async function main() {
|
|
|
15
16
|
capabilities: {
|
|
16
17
|
tools: {},
|
|
17
18
|
resources: {},
|
|
19
|
+
prompts: {},
|
|
18
20
|
},
|
|
19
21
|
});
|
|
20
22
|
registerResources(mcpServer);
|
|
23
|
+
registerPrompts(mcpServer);
|
|
21
24
|
registerTools(mcpServer.server, client, config);
|
|
22
25
|
const transport = new StdioServerTransport();
|
|
23
26
|
await mcpServer.connect(transport);
|
|
@@ -0,0 +1,259 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Comprehensive guide for AI agents on using Mentagen nodes effectively.
|
|
3
|
+
*/
|
|
4
|
+
const NODE_USAGE_GUIDE = `# Mentagen Node Usage Guide
|
|
5
|
+
|
|
6
|
+
## Node Types Overview
|
|
7
|
+
|
|
8
|
+
Mentagen supports several node types, each designed for specific use cases:
|
|
9
|
+
|
|
10
|
+
### Markdown Nodes (Recommended for Most Content)
|
|
11
|
+
- **Type**: \`markdown\`
|
|
12
|
+
- **Default Color**: cyan
|
|
13
|
+
- **Best For**: Documentation, notes, articles, structured content
|
|
14
|
+
- **Features**:
|
|
15
|
+
- Full GitHub Flavored Markdown (GFM) support
|
|
16
|
+
- **Todos/Checklists**: Only markdown nodes support the todos feature
|
|
17
|
+
- Rich formatting with headers, lists, code blocks, tables
|
|
18
|
+
- **When to Use**: Default choice for most text content
|
|
19
|
+
|
|
20
|
+
### Code Nodes
|
|
21
|
+
- **Type**: \`code\`
|
|
22
|
+
- **Default Color**: indigo
|
|
23
|
+
- **Best For**: Source code, scripts, configuration files
|
|
24
|
+
- **Features**:
|
|
25
|
+
- Syntax highlighting for many languages
|
|
26
|
+
- Monospace font optimized for code
|
|
27
|
+
- **No todo support** - use markdown nodes for task lists
|
|
28
|
+
- **When to Use**: When content is primarily source code
|
|
29
|
+
- **Naming Convention**: Include file extension (e.g., "utils.ts", "config.json")
|
|
30
|
+
|
|
31
|
+
### Text Nodes
|
|
32
|
+
- **Type**: \`text\`
|
|
33
|
+
- **Default Color**: none (neutral)
|
|
34
|
+
- **Best For**: Simple labels, titles, short notes
|
|
35
|
+
- **When to Use**: Brief content that doesn't need formatting
|
|
36
|
+
|
|
37
|
+
### URL Nodes
|
|
38
|
+
- **Type**: \`url\`
|
|
39
|
+
- **Default Color**: blue
|
|
40
|
+
- **Best For**: Bookmarks, external references, documentation links
|
|
41
|
+
- **When to Use**: Storing links to external resources
|
|
42
|
+
|
|
43
|
+
## Todo/Checklist Support
|
|
44
|
+
|
|
45
|
+
### CRITICAL: Use Native Todos, NOT Markdown Checkboxes
|
|
46
|
+
|
|
47
|
+
**ALWAYS use the native todo API** (\`mentagen_add_todo\`, \`mentagen_toggle_todo\`, etc.) instead of writing markdown checkboxes (\`- [ ]\`) in the content field.
|
|
48
|
+
|
|
49
|
+
**Why native todos are better:**
|
|
50
|
+
- Proper tracking with \`mentagen_list_todos\` showing completion stats
|
|
51
|
+
- Toggle completion with \`mentagen_toggle_todo\` without rewriting content
|
|
52
|
+
- Todos render as interactive checkboxes in the Mentagen UI
|
|
53
|
+
- Each todo has a unique ID for precise manipulation
|
|
54
|
+
|
|
55
|
+
**Wrong approach (don't do this):**
|
|
56
|
+
\`\`\`
|
|
57
|
+
mentagen_update_node({
|
|
58
|
+
content: "- [ ] Task 1\\n- [ ] Task 2" // NO! This is just text
|
|
59
|
+
})
|
|
60
|
+
\`\`\`
|
|
61
|
+
|
|
62
|
+
**Correct approach:**
|
|
63
|
+
\`\`\`
|
|
64
|
+
mentagen_add_todo({ text: "Task 1" })
|
|
65
|
+
mentagen_add_todo({ text: "Task 2" })
|
|
66
|
+
\`\`\`
|
|
67
|
+
|
|
68
|
+
### Node Granularity
|
|
69
|
+
|
|
70
|
+
Create **separate nodes for distinct task lists** rather than one giant node:
|
|
71
|
+
|
|
72
|
+
- **Good**: "Sprint 1 Tasks" node, "Sprint 2 Tasks" node, "Backlog" node
|
|
73
|
+
- **Bad**: One "All Tasks" node with 50 todos
|
|
74
|
+
|
|
75
|
+
Each focused node should have:
|
|
76
|
+
- A clear, descriptive name (the node title)
|
|
77
|
+
- Related todos that belong together
|
|
78
|
+
- Optional markdown content for context/notes
|
|
79
|
+
|
|
80
|
+
### Supported Node Types
|
|
81
|
+
|
|
82
|
+
**Only markdown and text nodes support todos.** Code nodes do NOT support todos.
|
|
83
|
+
|
|
84
|
+
### Todo Operations
|
|
85
|
+
- \`mentagen_add_todo\`: Add individual tasks (auto-generates UUID and order)
|
|
86
|
+
- \`mentagen_toggle_todo\`: Mark tasks complete/incomplete
|
|
87
|
+
- \`mentagen_list_todos\`: View all todos with completion statistics
|
|
88
|
+
- \`mentagen_update_node\` with \`todos\` array: Replace all todos at once
|
|
89
|
+
|
|
90
|
+
## Node Positioning
|
|
91
|
+
|
|
92
|
+
### Grid System
|
|
93
|
+
- 1 grid unit = 16 pixels
|
|
94
|
+
- Positions snap to grid automatically
|
|
95
|
+
- Use \`mentagen_find_position\` to find non-colliding positions
|
|
96
|
+
|
|
97
|
+
### Spacing Guidelines
|
|
98
|
+
- **Connected nodes** (with edges): Use gap of 6+ units (96px+)
|
|
99
|
+
- **Unrelated nodes**: Gap of 1 unit (16px) is sufficient
|
|
100
|
+
- Nodes auto-size based on their title, not content
|
|
101
|
+
|
|
102
|
+
## Common Patterns
|
|
103
|
+
|
|
104
|
+
### Creating a Task Board
|
|
105
|
+
1. Create a markdown node for the task list
|
|
106
|
+
2. Add todos using \`mentagen_add_todo\`
|
|
107
|
+
3. Use \`mentagen_toggle_todo\` to track completion
|
|
108
|
+
|
|
109
|
+
### Documentation Structure
|
|
110
|
+
1. Use markdown nodes for content sections
|
|
111
|
+
2. Connect related nodes with edges
|
|
112
|
+
3. Use consistent colors for categories
|
|
113
|
+
|
|
114
|
+
### Code Organization
|
|
115
|
+
1. Use code nodes for source files
|
|
116
|
+
2. Name nodes with file extensions
|
|
117
|
+
3. Group related code with edges
|
|
118
|
+
`;
|
|
119
|
+
/**
|
|
120
|
+
* Guide specifically for creating and managing todo lists.
|
|
121
|
+
*/
|
|
122
|
+
const TODO_GUIDE = `# Creating Checklists with Todos
|
|
123
|
+
|
|
124
|
+
## IMPORTANT: Native Todos vs Markdown Checkboxes
|
|
125
|
+
|
|
126
|
+
**ALWAYS use native todos** via the todo API tools. Do NOT write markdown checkboxes in content.
|
|
127
|
+
|
|
128
|
+
| Approach | Method | Result |
|
|
129
|
+
|----------|--------|--------|
|
|
130
|
+
| **CORRECT** | \`mentagen_add_todo({ text: "Task" })\` | Interactive checkbox, trackable |
|
|
131
|
+
| **WRONG** | \`content: "- [ ] Task"\` | Just text, not trackable |
|
|
132
|
+
|
|
133
|
+
Native todos provide:
|
|
134
|
+
- Completion tracking via \`mentagen_list_todos\`
|
|
135
|
+
- Toggle without rewriting content
|
|
136
|
+
- Interactive checkboxes in UI
|
|
137
|
+
- Unique IDs for each todo
|
|
138
|
+
|
|
139
|
+
## Node Granularity Best Practices
|
|
140
|
+
|
|
141
|
+
Create **focused nodes for related tasks**, not one giant task dump:
|
|
142
|
+
|
|
143
|
+
**Good structure:**
|
|
144
|
+
- "Feature A - Tasks" (5 todos)
|
|
145
|
+
- "Feature B - Tasks" (3 todos)
|
|
146
|
+
- "Bug Fixes" (4 todos)
|
|
147
|
+
|
|
148
|
+
**Bad structure:**
|
|
149
|
+
- "All Project Tasks" (50 todos in one node)
|
|
150
|
+
|
|
151
|
+
Each task node should:
|
|
152
|
+
- Have a clear, descriptive title
|
|
153
|
+
- Contain only related todos
|
|
154
|
+
- Optionally include context in the markdown content field
|
|
155
|
+
|
|
156
|
+
## Quick Start
|
|
157
|
+
|
|
158
|
+
### Option A: Create node with initial todos (recommended for bulk)
|
|
159
|
+
|
|
160
|
+
\`\`\`
|
|
161
|
+
mentagen_create_node({
|
|
162
|
+
boardId: "...",
|
|
163
|
+
name: "Sprint 1 Tasks",
|
|
164
|
+
type: "markdown",
|
|
165
|
+
content: "Tasks for the first sprint milestone.",
|
|
166
|
+
todos: [
|
|
167
|
+
{ text: "Implement auth" },
|
|
168
|
+
{ text: "Write tests" },
|
|
169
|
+
{ text: "Update docs" }
|
|
170
|
+
]
|
|
171
|
+
})
|
|
172
|
+
\`\`\`
|
|
173
|
+
|
|
174
|
+
Note: Just provide "text" for each todo. The id and order are auto-generated.
|
|
175
|
+
|
|
176
|
+
### Option B: Create node then add todos one by one
|
|
177
|
+
|
|
178
|
+
1. **Create a markdown node** for your task list:
|
|
179
|
+
\`\`\`
|
|
180
|
+
mentagen_create_node({
|
|
181
|
+
boardId: "...",
|
|
182
|
+
name: "Sprint 1 Tasks",
|
|
183
|
+
type: "markdown",
|
|
184
|
+
content: "Tasks for the first sprint milestone."
|
|
185
|
+
})
|
|
186
|
+
\`\`\`
|
|
187
|
+
|
|
188
|
+
2. **Add todos using the API** (not markdown syntax):
|
|
189
|
+
\`\`\`
|
|
190
|
+
mentagen_add_todo({ boardId: "...", nodeId: "...", text: "Implement auth" })
|
|
191
|
+
mentagen_add_todo({ boardId: "...", nodeId: "...", text: "Write tests" })
|
|
192
|
+
mentagen_add_todo({ boardId: "...", nodeId: "...", text: "Update docs" })
|
|
193
|
+
\`\`\`
|
|
194
|
+
|
|
195
|
+
3. **Check progress**:
|
|
196
|
+
\`\`\`
|
|
197
|
+
mentagen_list_todos({ boardId: "...", nodeId: "..." })
|
|
198
|
+
\`\`\`
|
|
199
|
+
Returns: \`{ summary: { total: 3, completed: 0, remaining: 3 } }\`
|
|
200
|
+
|
|
201
|
+
4. **Mark complete**:
|
|
202
|
+
\`\`\`
|
|
203
|
+
mentagen_toggle_todo({ boardId: "...", nodeId: "...", todoId: "..." })
|
|
204
|
+
\`\`\`
|
|
205
|
+
|
|
206
|
+
## Supported Node Types
|
|
207
|
+
|
|
208
|
+
- **Markdown nodes**: Full todo support ✓
|
|
209
|
+
- **Text nodes**: Full todo support ✓
|
|
210
|
+
- **Code nodes**: NO todo support ✗
|
|
211
|
+
|
|
212
|
+
## Bulk Operations
|
|
213
|
+
|
|
214
|
+
To set all todos at once (useful for importing):
|
|
215
|
+
\`\`\`
|
|
216
|
+
mentagen_update_node({
|
|
217
|
+
boardId: "...",
|
|
218
|
+
nodeId: "...",
|
|
219
|
+
todos: [
|
|
220
|
+
{ id: "uuid-1", text: "Task 1", completed: false, order: 0 },
|
|
221
|
+
{ id: "uuid-2", text: "Task 2", completed: true, order: 1 }
|
|
222
|
+
]
|
|
223
|
+
})
|
|
224
|
+
\`\`\`
|
|
225
|
+
`;
|
|
226
|
+
export function registerPrompts(mcpServer) {
|
|
227
|
+
// Enable prompts capability
|
|
228
|
+
mcpServer.server.registerCapabilities({
|
|
229
|
+
prompts: {
|
|
230
|
+
listChanged: true,
|
|
231
|
+
},
|
|
232
|
+
});
|
|
233
|
+
mcpServer.registerPrompt('node-usage-guide', {
|
|
234
|
+
description: 'Comprehensive guide for using Mentagen nodes effectively, including node types, todos, and best practices.',
|
|
235
|
+
}, async () => ({
|
|
236
|
+
messages: [
|
|
237
|
+
{
|
|
238
|
+
role: 'user',
|
|
239
|
+
content: {
|
|
240
|
+
type: 'text',
|
|
241
|
+
text: NODE_USAGE_GUIDE,
|
|
242
|
+
},
|
|
243
|
+
},
|
|
244
|
+
],
|
|
245
|
+
}));
|
|
246
|
+
mcpServer.registerPrompt('todo-checklist-guide', {
|
|
247
|
+
description: 'Step-by-step guide for creating and managing todo lists/checklists on Mentagen nodes.',
|
|
248
|
+
}, async () => ({
|
|
249
|
+
messages: [
|
|
250
|
+
{
|
|
251
|
+
role: 'user',
|
|
252
|
+
content: {
|
|
253
|
+
type: 'text',
|
|
254
|
+
text: TODO_GUIDE,
|
|
255
|
+
},
|
|
256
|
+
},
|
|
257
|
+
],
|
|
258
|
+
}));
|
|
259
|
+
}
|
|
@@ -2,15 +2,32 @@ import { z } from 'zod';
|
|
|
2
2
|
import { formatError } from '../utils/errors.js';
|
|
3
3
|
import { unitsToPixels } from '../utils/units.js';
|
|
4
4
|
import { snapToGrid } from './grid-calc.js';
|
|
5
|
+
import { validateTypeChange } from './update.js';
|
|
6
|
+
const todoItemSchema = z.object({
|
|
7
|
+
id: z.string().describe('Unique identifier (UUID v4)'),
|
|
8
|
+
text: z.string().describe('Todo text content'),
|
|
9
|
+
completed: z.boolean().describe('Completion status'),
|
|
10
|
+
order: z.number().describe('Sort order (0-based)'),
|
|
11
|
+
});
|
|
5
12
|
const nodeUpdateSchema = z.object({
|
|
6
13
|
nodeId: z.string().describe('The node ID to update'),
|
|
14
|
+
type: z
|
|
15
|
+
.enum(['markdown', 'code', 'url'])
|
|
16
|
+
.optional()
|
|
17
|
+
.describe('Change node type. Safe conversions: markdown ↔ code. Content is auto-migrated.'),
|
|
7
18
|
name: z.string().optional().describe('New name for the node'),
|
|
8
19
|
content: z.string().optional().describe('New content for the node'),
|
|
20
|
+
code: z.string().optional().describe('Code content for code-type nodes'),
|
|
21
|
+
url: z.string().optional().describe('URL for url-type nodes'),
|
|
9
22
|
color: z.string().optional().describe('Node color'),
|
|
10
23
|
x: z.number().optional().describe('X position in grid units'),
|
|
11
24
|
y: z.number().optional().describe('Y position in grid units'),
|
|
12
25
|
width: z.number().optional().describe('Node width in grid units'),
|
|
13
26
|
height: z.number().optional().describe('Node height in grid units'),
|
|
27
|
+
todos: z
|
|
28
|
+
.array(todoItemSchema)
|
|
29
|
+
.optional()
|
|
30
|
+
.describe('Replace all todos on this markdown node. Only markdown/text nodes support todos.'),
|
|
14
31
|
});
|
|
15
32
|
const edgeUpdateSchema = z.object({
|
|
16
33
|
edgeId: z.string().describe('The edge ID to update'),
|
|
@@ -25,6 +42,79 @@ export const batchUpdateSchema = z.object({
|
|
|
25
42
|
nodes: z.array(nodeUpdateSchema).optional().describe('Array of node updates'),
|
|
26
43
|
edges: z.array(edgeUpdateSchema).optional().describe('Array of edge updates'),
|
|
27
44
|
});
|
|
45
|
+
const POSITION_FIELDS = ['x', 'y', 'width', 'height'];
|
|
46
|
+
/**
|
|
47
|
+
* Process type change and return migration fields or error.
|
|
48
|
+
*/
|
|
49
|
+
async function processNodeTypeChange(client, boardId, nodeId, targetType, data) {
|
|
50
|
+
try {
|
|
51
|
+
const currentNode = await client.getNode({ boardId, nodeId });
|
|
52
|
+
if (targetType === currentNode.type) {
|
|
53
|
+
return { success: true, fields: {} };
|
|
54
|
+
}
|
|
55
|
+
const result = validateTypeChange(currentNode.type, targetType, currentNode, {
|
|
56
|
+
...data,
|
|
57
|
+
type: targetType,
|
|
58
|
+
boardId,
|
|
59
|
+
nodeId,
|
|
60
|
+
autoSize: true,
|
|
61
|
+
});
|
|
62
|
+
if (!result.valid) {
|
|
63
|
+
return { success: false, error: result.error };
|
|
64
|
+
}
|
|
65
|
+
return { success: true, fields: result.fields };
|
|
66
|
+
}
|
|
67
|
+
catch (error) {
|
|
68
|
+
return { success: false, error: formatError(error) };
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
/**
|
|
72
|
+
* Convert data fields to update format with pixel conversion.
|
|
73
|
+
*/
|
|
74
|
+
function buildNodeUpdateData(data, migrationFields) {
|
|
75
|
+
const cleanData = {
|
|
76
|
+
...migrationFields,
|
|
77
|
+
};
|
|
78
|
+
for (const [key, value] of Object.entries(data)) {
|
|
79
|
+
if (value === undefined)
|
|
80
|
+
continue;
|
|
81
|
+
if (POSITION_FIELDS.includes(key)) {
|
|
82
|
+
cleanData[key] = snapToGrid(unitsToPixels(value));
|
|
83
|
+
}
|
|
84
|
+
else if (key === 'todos') {
|
|
85
|
+
cleanData[key] = value;
|
|
86
|
+
}
|
|
87
|
+
else {
|
|
88
|
+
cleanData[key] = value;
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
return cleanData;
|
|
92
|
+
}
|
|
93
|
+
/**
|
|
94
|
+
* Process a single node update.
|
|
95
|
+
*/
|
|
96
|
+
async function processNodeUpdate(client, boardId, update) {
|
|
97
|
+
const { nodeId, type: targetType, ...data } = update;
|
|
98
|
+
let migrationFields = {};
|
|
99
|
+
if (targetType !== undefined) {
|
|
100
|
+
const typeResult = await processNodeTypeChange(client, boardId, nodeId, targetType, data);
|
|
101
|
+
if (!typeResult.success) {
|
|
102
|
+
return { id: nodeId, success: false, error: typeResult.error };
|
|
103
|
+
}
|
|
104
|
+
migrationFields = typeResult.fields;
|
|
105
|
+
}
|
|
106
|
+
const cleanData = buildNodeUpdateData(data, migrationFields);
|
|
107
|
+
if (Object.keys(cleanData).length === 0) {
|
|
108
|
+
return { id: nodeId, success: false, error: 'No fields to update' };
|
|
109
|
+
}
|
|
110
|
+
try {
|
|
111
|
+
await client.updateNode({ boardId, nodeId, data: cleanData });
|
|
112
|
+
return { id: nodeId, success: true };
|
|
113
|
+
}
|
|
114
|
+
catch (error) {
|
|
115
|
+
return { id: nodeId, success: false, error: formatError(error) };
|
|
116
|
+
}
|
|
117
|
+
}
|
|
28
118
|
export async function handleBatchUpdate(client, params) {
|
|
29
119
|
const nodeUpdates = params.nodes ?? [];
|
|
30
120
|
const edgeUpdates = params.edges ?? [];
|
|
@@ -43,36 +133,7 @@ export async function handleBatchUpdate(client, params) {
|
|
|
43
133
|
nodes: [],
|
|
44
134
|
edges: [],
|
|
45
135
|
};
|
|
46
|
-
|
|
47
|
-
const nodePromises = nodeUpdates.map(async (update) => {
|
|
48
|
-
const { nodeId, ...data } = update;
|
|
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
|
-
}
|
|
61
|
-
if (Object.keys(cleanData).length === 0) {
|
|
62
|
-
return { id: nodeId, success: false, error: 'No fields to update' };
|
|
63
|
-
}
|
|
64
|
-
try {
|
|
65
|
-
await client.updateNode({
|
|
66
|
-
boardId: params.boardId,
|
|
67
|
-
nodeId,
|
|
68
|
-
data: cleanData,
|
|
69
|
-
});
|
|
70
|
-
return { id: nodeId, success: true };
|
|
71
|
-
}
|
|
72
|
-
catch (error) {
|
|
73
|
-
return { id: nodeId, success: false, error: formatError(error) };
|
|
74
|
-
}
|
|
75
|
-
});
|
|
136
|
+
const nodePromises = nodeUpdates.map(update => processNodeUpdate(client, params.boardId, update));
|
|
76
137
|
// Process edge updates in parallel
|
|
77
138
|
const edgePromises = edgeUpdates.map(async (update) => {
|
|
78
139
|
const { edgeId, direction, label } = update;
|
|
@@ -1,9 +1,17 @@
|
|
|
1
1
|
import { z } from 'zod';
|
|
2
2
|
import { formatError } from '../utils/errors.js';
|
|
3
3
|
import { pixelsToUnits, unitsToPixels } from '../utils/units.js';
|
|
4
|
-
import { applyExtension, generateId } from './create.js';
|
|
4
|
+
import { applyExtension, generateId, normalizeTodos } from './create.js';
|
|
5
5
|
import { snapToGrid } from './grid-calc.js';
|
|
6
6
|
import { calculateNodeSize } from './size-calc.js';
|
|
7
|
+
const todoInputSchema = z.object({
|
|
8
|
+
text: z.string().describe('Todo text content'),
|
|
9
|
+
completed: z
|
|
10
|
+
.boolean()
|
|
11
|
+
.optional()
|
|
12
|
+
.default(false)
|
|
13
|
+
.describe('Completion status (default: false)'),
|
|
14
|
+
});
|
|
7
15
|
const nodeInputSchema = z.object({
|
|
8
16
|
tempId: z.string().describe('Temporary ID for referencing in edges'),
|
|
9
17
|
name: z.string().describe('The node title/name'),
|
|
@@ -13,15 +21,19 @@ const nodeInputSchema = z.object({
|
|
|
13
21
|
.describe('File extension to append to name (e.g., ".ts", ".py"). Only applies to code nodes. Defaults to .js.'),
|
|
14
22
|
content: z.string().optional().describe('Node content'),
|
|
15
23
|
type: z
|
|
16
|
-
.enum(['
|
|
24
|
+
.enum(['markdown', 'code', 'url'])
|
|
17
25
|
.optional()
|
|
18
|
-
.default('
|
|
26
|
+
.default('markdown')
|
|
19
27
|
.describe('Node type'),
|
|
20
28
|
color: z.string().optional().describe('Node color'),
|
|
21
29
|
x: z.number().optional().describe('X position in grid units'),
|
|
22
30
|
y: z.number().optional().describe('Y position in grid units'),
|
|
23
31
|
width: z.number().optional().describe('Width in grid units'),
|
|
24
32
|
height: z.number().optional().describe('Height in grid units'),
|
|
33
|
+
todos: z
|
|
34
|
+
.array(todoInputSchema)
|
|
35
|
+
.optional()
|
|
36
|
+
.describe('Initial todos for the node. Only supported on markdown/text nodes. Only text is required; id and order are auto-generated.'),
|
|
25
37
|
});
|
|
26
38
|
const edgeInputSchema = z.object({
|
|
27
39
|
source: z.string().describe('Source node tempId'),
|
|
@@ -46,24 +58,32 @@ const DEFAULT_NODE_COLORS = {
|
|
|
46
58
|
code: 'indigo',
|
|
47
59
|
url: 'blue',
|
|
48
60
|
};
|
|
61
|
+
/** Calculate snapped pixel dimensions for a node. */
|
|
62
|
+
function calcNodeDimensions(nodeInput, autoSize) {
|
|
63
|
+
const DEFAULT_POS = 6;
|
|
64
|
+
return {
|
|
65
|
+
x: snapToGrid(unitsToPixels(nodeInput.x ?? DEFAULT_POS)),
|
|
66
|
+
y: snapToGrid(unitsToPixels(nodeInput.y ?? DEFAULT_POS)),
|
|
67
|
+
width: snapToGrid(nodeInput.width !== undefined
|
|
68
|
+
? unitsToPixels(nodeInput.width)
|
|
69
|
+
: autoSize.width),
|
|
70
|
+
height: snapToGrid(nodeInput.height !== undefined
|
|
71
|
+
? unitsToPixels(nodeInput.height)
|
|
72
|
+
: autoSize.height),
|
|
73
|
+
};
|
|
74
|
+
}
|
|
49
75
|
async function createNode(client, boardId, nodeInput, idMap) {
|
|
50
76
|
const realId = generateId();
|
|
51
77
|
const type = nodeInput.type ?? 'text';
|
|
52
78
|
const finalName = applyExtension(nodeInput.name, type, nodeInput.extension);
|
|
53
79
|
const color = nodeInput.color ?? DEFAULT_NODE_COLORS[type];
|
|
54
80
|
const autoSize = calculateNodeSize(finalName, type);
|
|
55
|
-
const
|
|
56
|
-
const
|
|
57
|
-
const
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
const heightPixels = snapToGrid(nodeInput.height !== undefined
|
|
61
|
-
? unitsToPixels(nodeInput.height)
|
|
62
|
-
: autoSize.height);
|
|
81
|
+
const dims = calcNodeDimensions(nodeInput, autoSize);
|
|
82
|
+
const isCodeNode = type === 'code';
|
|
83
|
+
const nodeContent = nodeInput.content ?? '';
|
|
84
|
+
// Only pass todos for non-code nodes
|
|
85
|
+
const todos = !isCodeNode && nodeInput.todos ? normalizeTodos(nodeInput.todos) : undefined;
|
|
63
86
|
try {
|
|
64
|
-
// For code nodes, store content in the `code` field
|
|
65
|
-
const isCodeNode = type === 'code';
|
|
66
|
-
const nodeContent = nodeInput.content ?? '';
|
|
67
87
|
const node = await client.addNode({
|
|
68
88
|
_id: realId,
|
|
69
89
|
boardId,
|
|
@@ -72,10 +92,11 @@ async function createNode(client, boardId, nodeInput, idMap) {
|
|
|
72
92
|
...(isCodeNode ? { code: nodeContent } : { content: nodeContent }),
|
|
73
93
|
type,
|
|
74
94
|
color,
|
|
75
|
-
x:
|
|
76
|
-
y:
|
|
77
|
-
width:
|
|
78
|
-
height:
|
|
95
|
+
x: dims.x,
|
|
96
|
+
y: dims.y,
|
|
97
|
+
width: dims.width,
|
|
98
|
+
height: dims.height,
|
|
99
|
+
...(todos && { todos }),
|
|
79
100
|
},
|
|
80
101
|
});
|
|
81
102
|
idMap[nodeInput.tempId] = node._id;
|
|
@@ -85,10 +106,10 @@ async function createNode(client, boardId, nodeInput, idMap) {
|
|
|
85
106
|
tempId: nodeInput.tempId,
|
|
86
107
|
id: node._id,
|
|
87
108
|
name: node.name,
|
|
88
|
-
x: pixelsToUnits(
|
|
89
|
-
y: pixelsToUnits(
|
|
90
|
-
width: pixelsToUnits(
|
|
91
|
-
height: pixelsToUnits(
|
|
109
|
+
x: pixelsToUnits(dims.x),
|
|
110
|
+
y: pixelsToUnits(dims.y),
|
|
111
|
+
width: pixelsToUnits(dims.width),
|
|
112
|
+
height: pixelsToUnits(dims.height),
|
|
92
113
|
},
|
|
93
114
|
};
|
|
94
115
|
}
|
package/dist/tools/create.js
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { randomUUID } from 'crypto';
|
|
1
2
|
import { z } from 'zod';
|
|
2
3
|
import { formatError } from '../utils/errors.js';
|
|
3
4
|
import { pixelsToUnits, unitsToPixels } from '../utils/units.js';
|
|
@@ -14,7 +15,6 @@ import { calculateNodeSize } from './size-calc.js';
|
|
|
14
15
|
*/
|
|
15
16
|
const DEFAULT_NODE_COLORS = {
|
|
16
17
|
// Currently supported in MCP
|
|
17
|
-
text: undefined,
|
|
18
18
|
markdown: 'cyan',
|
|
19
19
|
code: 'indigo',
|
|
20
20
|
url: 'blue',
|
|
@@ -49,7 +49,7 @@ export const createSchema = z.object({
|
|
|
49
49
|
.string()
|
|
50
50
|
.optional()
|
|
51
51
|
.describe('File extension to append to name (e.g., ".ts", ".py"). Only applies to code nodes. Defaults to .js.'),
|
|
52
|
-
content: z.string().optional().describe('Content for
|
|
52
|
+
content: z.string().optional().describe('Content for markdown nodes.'),
|
|
53
53
|
code: z
|
|
54
54
|
.string()
|
|
55
55
|
.optional()
|
|
@@ -59,10 +59,10 @@ export const createSchema = z.object({
|
|
|
59
59
|
.optional()
|
|
60
60
|
.describe('The URL for url-type nodes (e.g., https://example.com). Required for type="url".'),
|
|
61
61
|
type: z
|
|
62
|
-
.enum(['
|
|
62
|
+
.enum(['markdown', 'code', 'url'])
|
|
63
63
|
.optional()
|
|
64
|
-
.default('
|
|
65
|
-
.describe('Node type:
|
|
64
|
+
.default('markdown')
|
|
65
|
+
.describe('Node type: markdown (default), code, or url'),
|
|
66
66
|
color: z
|
|
67
67
|
.string()
|
|
68
68
|
.optional()
|
|
@@ -77,6 +77,17 @@ export const createSchema = z.object({
|
|
|
77
77
|
.number()
|
|
78
78
|
.optional()
|
|
79
79
|
.describe('Height in grid units (default: auto-calculated)'),
|
|
80
|
+
todos: z
|
|
81
|
+
.array(z.object({
|
|
82
|
+
text: z.string().describe('Todo text content'),
|
|
83
|
+
completed: z
|
|
84
|
+
.boolean()
|
|
85
|
+
.optional()
|
|
86
|
+
.default(false)
|
|
87
|
+
.describe('Completion status (default: false)'),
|
|
88
|
+
}))
|
|
89
|
+
.optional()
|
|
90
|
+
.describe('Initial todos for the node. Only supported on markdown/text nodes, not code nodes. Only text is required; id and order are auto-generated.'),
|
|
80
91
|
});
|
|
81
92
|
/**
|
|
82
93
|
* Generate a simple ObjectId-like string.
|
|
@@ -89,6 +100,19 @@ export function generateId() {
|
|
|
89
100
|
const random = Array.from({ length: 16 }, () => Math.floor(Math.random() * 16).toString(16)).join('');
|
|
90
101
|
return timestamp + random;
|
|
91
102
|
}
|
|
103
|
+
/**
|
|
104
|
+
* Normalize todos by generating ids and orders.
|
|
105
|
+
* Auto-generates UUID for id, defaults completed to false,
|
|
106
|
+
* and assigns order based on array index.
|
|
107
|
+
*/
|
|
108
|
+
export function normalizeTodos(todos) {
|
|
109
|
+
return todos.map((todo, index) => ({
|
|
110
|
+
id: randomUUID(),
|
|
111
|
+
text: todo.text,
|
|
112
|
+
completed: todo.completed ?? false,
|
|
113
|
+
order: index,
|
|
114
|
+
}));
|
|
115
|
+
}
|
|
92
116
|
/**
|
|
93
117
|
* Build node fields object with content mapped to the correct field based on type.
|
|
94
118
|
* Expects params.name to already have extension applied.
|
|
@@ -102,6 +126,7 @@ function buildNodeFields(params, color, position) {
|
|
|
102
126
|
};
|
|
103
127
|
if (params.type === 'code') {
|
|
104
128
|
nodeFields.code = params.code ?? params.content ?? '';
|
|
129
|
+
// Code nodes don't support todos - ignore if provided
|
|
105
130
|
}
|
|
106
131
|
else if (params.type === 'url') {
|
|
107
132
|
if (!params.url) {
|
|
@@ -111,9 +136,15 @@ function buildNodeFields(params, color, position) {
|
|
|
111
136
|
if (params.content) {
|
|
112
137
|
nodeFields.content = params.content;
|
|
113
138
|
}
|
|
139
|
+
if (params.todos) {
|
|
140
|
+
nodeFields.todos = normalizeTodos(params.todos);
|
|
141
|
+
}
|
|
114
142
|
}
|
|
115
143
|
else {
|
|
116
144
|
nodeFields.content = params.content || '';
|
|
145
|
+
if (params.todos) {
|
|
146
|
+
nodeFields.todos = normalizeTodos(params.todos);
|
|
147
|
+
}
|
|
117
148
|
}
|
|
118
149
|
return nodeFields;
|
|
119
150
|
}
|
|
@@ -41,6 +41,16 @@ function stripHtml(html) {
|
|
|
41
41
|
.replace(/'/g, "'")
|
|
42
42
|
.trim();
|
|
43
43
|
}
|
|
44
|
+
/**
|
|
45
|
+
* Format todos as markdown checkboxes.
|
|
46
|
+
*/
|
|
47
|
+
function formatTodos(todos) {
|
|
48
|
+
if (!todos || todos.length === 0)
|
|
49
|
+
return '';
|
|
50
|
+
const sorted = [...todos].sort((a, b) => a.order - b.order);
|
|
51
|
+
const lines = sorted.map(t => `- [${t.completed ? 'x' : ' '}] ${t.text}`);
|
|
52
|
+
return lines.join('\n');
|
|
53
|
+
}
|
|
44
54
|
/**
|
|
45
55
|
* Infer programming language from file extension.
|
|
46
56
|
*/
|
|
@@ -121,6 +131,7 @@ function formatContent(node) {
|
|
|
121
131
|
*/
|
|
122
132
|
function formatNodeSection(node, baseUrl, boardId) {
|
|
123
133
|
const content = formatContent(node);
|
|
134
|
+
const todos = formatTodos(node.todos);
|
|
124
135
|
const link = getNodeUrl(baseUrl, boardId, node._id);
|
|
125
136
|
const lines = [
|
|
126
137
|
`## ${node.name}`,
|
|
@@ -128,6 +139,12 @@ function formatNodeSection(node, baseUrl, boardId) {
|
|
|
128
139
|
'',
|
|
129
140
|
content,
|
|
130
141
|
];
|
|
142
|
+
// Add todos section if present
|
|
143
|
+
if (todos) {
|
|
144
|
+
if (content)
|
|
145
|
+
lines.push('', '### Todos', '');
|
|
146
|
+
lines.push(todos);
|
|
147
|
+
}
|
|
131
148
|
return lines.join('\n');
|
|
132
149
|
}
|
|
133
150
|
export async function handleExtractBoardContent(client, params, baseUrl) {
|
|
@@ -150,12 +167,13 @@ export async function handleExtractBoardContent(client, params, baseUrl) {
|
|
|
150
167
|
],
|
|
151
168
|
};
|
|
152
169
|
}
|
|
153
|
-
// Filter nodes based on content
|
|
170
|
+
// Filter nodes based on content or todos
|
|
154
171
|
const nodesWithContent = params.includeEmpty
|
|
155
172
|
? nodes
|
|
156
173
|
: nodes.filter(n => {
|
|
157
174
|
const content = getNodeContent(n);
|
|
158
|
-
|
|
175
|
+
const hasTodos = n.todos && n.todos.length > 0;
|
|
176
|
+
return (content && content.trim().length > 0) || hasTodos;
|
|
159
177
|
});
|
|
160
178
|
if (nodesWithContent.length === 0) {
|
|
161
179
|
return {
|
package/dist/tools/index.js
CHANGED
|
@@ -26,6 +26,7 @@ import { handleRead, readSchema } from './read.js';
|
|
|
26
26
|
import { handleSearch, searchSchema } from './search.js';
|
|
27
27
|
import { handleSearchBoards, searchBoardsSchema } from './search-boards.js';
|
|
28
28
|
import { handleSizeCalc, sizeCalcSchema } from './size-calc.js';
|
|
29
|
+
import { addTodoSchema, handleAddTodo, handleListTodos, handleToggleTodo, listTodosSchema, toggleTodoSchema, } from './todo.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';
|
|
@@ -251,7 +252,7 @@ export function registerTools(server, client, config) {
|
|
|
251
252
|
},
|
|
252
253
|
{
|
|
253
254
|
name: 'mentagen_create_node',
|
|
254
|
-
description: 'Create a new node on a Mentagen board. IMPORTANT: Do NOT provide width/height - let auto-sizing calculate dimensions from the name. Only provide explicit dimensions if the user specifically requests a custom size. Content does not affect size (it scrolls inside the node).',
|
|
255
|
+
description: 'Create a new node on a Mentagen board. IMPORTANT: Do NOT provide width/height - let auto-sizing calculate dimensions from the name. Only provide explicit dimensions if the user specifically requests a custom size. Content does not affect size (it scrolls inside the node). For task lists, pass todos array to create with initial checklist items.',
|
|
255
256
|
inputSchema: {
|
|
256
257
|
type: 'object',
|
|
257
258
|
properties: {
|
|
@@ -296,6 +297,21 @@ export function registerTools(server, client, config) {
|
|
|
296
297
|
type: 'number',
|
|
297
298
|
description: 'DO NOT USE unless user requests specific size. Auto-calculated from name.',
|
|
298
299
|
},
|
|
300
|
+
todos: {
|
|
301
|
+
type: 'array',
|
|
302
|
+
description: 'Initial todos for the node. Only supported on markdown/text nodes, not code nodes. Just provide text; id and order are auto-generated.',
|
|
303
|
+
items: {
|
|
304
|
+
type: 'object',
|
|
305
|
+
properties: {
|
|
306
|
+
text: { type: 'string', description: 'Todo text content' },
|
|
307
|
+
completed: {
|
|
308
|
+
type: 'boolean',
|
|
309
|
+
description: 'Completion status (default: false)',
|
|
310
|
+
},
|
|
311
|
+
},
|
|
312
|
+
required: ['text'],
|
|
313
|
+
},
|
|
314
|
+
},
|
|
299
315
|
},
|
|
300
316
|
required: ['boardId', 'name'],
|
|
301
317
|
},
|
|
@@ -508,7 +524,7 @@ export function registerTools(server, client, config) {
|
|
|
508
524
|
},
|
|
509
525
|
{
|
|
510
526
|
name: 'mentagen_create_graph',
|
|
511
|
-
description: 'Create multiple nodes with edges in a single call. IMPORTANT: Do NOT provide width/height for nodes - let auto-sizing calculate dimensions from names. Only provide explicit dimensions if the user specifically requests custom sizes.',
|
|
527
|
+
description: 'Create multiple nodes with edges in a single call. IMPORTANT: Do NOT provide width/height for nodes - let auto-sizing calculate dimensions from names. Only provide explicit dimensions if the user specifically requests custom sizes. Nodes can include initial todos for task lists.',
|
|
512
528
|
inputSchema: {
|
|
513
529
|
type: 'object',
|
|
514
530
|
properties: {
|
|
@@ -560,6 +576,18 @@ export function registerTools(server, client, config) {
|
|
|
560
576
|
type: 'number',
|
|
561
577
|
description: 'Height in grid units (1 unit = 16px, DO NOT USE - auto-calculated from name)',
|
|
562
578
|
},
|
|
579
|
+
todos: {
|
|
580
|
+
type: 'array',
|
|
581
|
+
description: 'Initial todos for markdown/text nodes. Just provide text; id and order are auto-generated.',
|
|
582
|
+
items: {
|
|
583
|
+
type: 'object',
|
|
584
|
+
properties: {
|
|
585
|
+
text: { type: 'string' },
|
|
586
|
+
completed: { type: 'boolean', default: false },
|
|
587
|
+
},
|
|
588
|
+
required: ['text'],
|
|
589
|
+
},
|
|
590
|
+
},
|
|
563
591
|
},
|
|
564
592
|
required: ['tempId', 'name'],
|
|
565
593
|
},
|
|
@@ -887,6 +915,73 @@ export function registerTools(server, client, config) {
|
|
|
887
915
|
required: ['boardId'],
|
|
888
916
|
},
|
|
889
917
|
},
|
|
918
|
+
// Todo tools
|
|
919
|
+
{
|
|
920
|
+
name: 'mentagen_add_todo',
|
|
921
|
+
description: 'Add a new todo item to a markdown node. Generates a unique ID and appends to the end of the list. Note: Only markdown/text nodes support todos; code nodes do not.',
|
|
922
|
+
inputSchema: {
|
|
923
|
+
type: 'object',
|
|
924
|
+
properties: {
|
|
925
|
+
boardId: {
|
|
926
|
+
type: 'string',
|
|
927
|
+
description: 'The board ID containing the node',
|
|
928
|
+
},
|
|
929
|
+
nodeId: {
|
|
930
|
+
type: 'string',
|
|
931
|
+
description: 'The node ID to add the todo to',
|
|
932
|
+
},
|
|
933
|
+
text: {
|
|
934
|
+
type: 'string',
|
|
935
|
+
description: 'The todo text content',
|
|
936
|
+
},
|
|
937
|
+
},
|
|
938
|
+
required: ['boardId', 'nodeId', 'text'],
|
|
939
|
+
},
|
|
940
|
+
},
|
|
941
|
+
{
|
|
942
|
+
name: 'mentagen_toggle_todo',
|
|
943
|
+
description: 'Toggle a todo item completion status on a markdown node. Can set explicit state or toggle current state.',
|
|
944
|
+
inputSchema: {
|
|
945
|
+
type: 'object',
|
|
946
|
+
properties: {
|
|
947
|
+
boardId: {
|
|
948
|
+
type: 'string',
|
|
949
|
+
description: 'The board ID containing the node',
|
|
950
|
+
},
|
|
951
|
+
nodeId: {
|
|
952
|
+
type: 'string',
|
|
953
|
+
description: 'The node ID containing the todo',
|
|
954
|
+
},
|
|
955
|
+
todoId: {
|
|
956
|
+
type: 'string',
|
|
957
|
+
description: 'The todo ID to toggle',
|
|
958
|
+
},
|
|
959
|
+
completed: {
|
|
960
|
+
type: 'boolean',
|
|
961
|
+
description: 'Set explicit completion state. Omit to toggle current state.',
|
|
962
|
+
},
|
|
963
|
+
},
|
|
964
|
+
required: ['boardId', 'nodeId', 'todoId'],
|
|
965
|
+
},
|
|
966
|
+
},
|
|
967
|
+
{
|
|
968
|
+
name: 'mentagen_list_todos',
|
|
969
|
+
description: 'List all todo items on a markdown node with summary statistics (total, completed, remaining).',
|
|
970
|
+
inputSchema: {
|
|
971
|
+
type: 'object',
|
|
972
|
+
properties: {
|
|
973
|
+
boardId: {
|
|
974
|
+
type: 'string',
|
|
975
|
+
description: 'The board ID containing the node',
|
|
976
|
+
},
|
|
977
|
+
nodeId: {
|
|
978
|
+
type: 'string',
|
|
979
|
+
description: 'The node ID to list todos from',
|
|
980
|
+
},
|
|
981
|
+
},
|
|
982
|
+
required: ['boardId', 'nodeId'],
|
|
983
|
+
},
|
|
984
|
+
},
|
|
890
985
|
],
|
|
891
986
|
}));
|
|
892
987
|
const toolHandlers = {
|
|
@@ -1011,6 +1106,19 @@ export function registerTools(server, client, config) {
|
|
|
1011
1106
|
const params = visualizeBoardSchema.parse(args);
|
|
1012
1107
|
return handleVisualizeBoard(client, params);
|
|
1013
1108
|
},
|
|
1109
|
+
// Todo handlers
|
|
1110
|
+
mentagen_add_todo: async (args) => {
|
|
1111
|
+
const params = addTodoSchema.parse(args);
|
|
1112
|
+
return handleAddTodo(client, params);
|
|
1113
|
+
},
|
|
1114
|
+
mentagen_toggle_todo: async (args) => {
|
|
1115
|
+
const params = toggleTodoSchema.parse(args);
|
|
1116
|
+
return handleToggleTodo(client, params);
|
|
1117
|
+
},
|
|
1118
|
+
mentagen_list_todos: async (args) => {
|
|
1119
|
+
const params = listTodosSchema.parse(args);
|
|
1120
|
+
return handleListTodos(client, params);
|
|
1121
|
+
},
|
|
1014
1122
|
};
|
|
1015
1123
|
server.setRequestHandler(CallToolRequestSchema, async (request) => {
|
|
1016
1124
|
const { name, arguments: args } = request.params;
|
package/dist/tools/list-nodes.js
CHANGED
|
@@ -23,6 +23,13 @@ function truncateContent(content, maxLength) {
|
|
|
23
23
|
return ((lastSpace > maxLength * 0.7 ? truncated.slice(0, lastSpace) : truncated) +
|
|
24
24
|
'...');
|
|
25
25
|
}
|
|
26
|
+
/** Format todos as "completed/total" or empty string if no todos. */
|
|
27
|
+
function formatTodoCounts(todos) {
|
|
28
|
+
if (!todos || todos.length === 0)
|
|
29
|
+
return '';
|
|
30
|
+
const completed = todos.filter(t => t.completed).length;
|
|
31
|
+
return `${completed}/${todos.length}`;
|
|
32
|
+
}
|
|
26
33
|
function formatNodesTsv(nodes) {
|
|
27
34
|
const rows = nodes.map(n => ({
|
|
28
35
|
id: n._id,
|
|
@@ -33,6 +40,7 @@ function formatNodesTsv(nodes) {
|
|
|
33
40
|
width: pixelsToUnits(n.width),
|
|
34
41
|
height: pixelsToUnits(n.height),
|
|
35
42
|
content: truncateContent(n.content ?? null, MAX_CONTENT_LENGTH),
|
|
43
|
+
todos: formatTodoCounts(n.todos),
|
|
36
44
|
updatedAt: formatDate(n.updatedAt),
|
|
37
45
|
link: n.link,
|
|
38
46
|
}));
|
|
@@ -49,6 +57,7 @@ function formatNodesTsv(nodes) {
|
|
|
49
57
|
'width',
|
|
50
58
|
'height',
|
|
51
59
|
'content',
|
|
60
|
+
'todos',
|
|
52
61
|
'updatedAt',
|
|
53
62
|
'link',
|
|
54
63
|
],
|
package/dist/tools/size-calc.js
CHANGED
|
@@ -0,0 +1,171 @@
|
|
|
1
|
+
import { randomUUID } from 'crypto';
|
|
2
|
+
import { z } from 'zod';
|
|
3
|
+
import { formatError } from '../utils/errors.js';
|
|
4
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
5
|
+
// Schemas
|
|
6
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
7
|
+
export const addTodoSchema = z.object({
|
|
8
|
+
boardId: z.string().describe('The board ID containing the node'),
|
|
9
|
+
nodeId: z.string().describe('The node ID to add the todo to'),
|
|
10
|
+
text: z.string().describe('The todo text content'),
|
|
11
|
+
});
|
|
12
|
+
export const toggleTodoSchema = z.object({
|
|
13
|
+
boardId: z.string().describe('The board ID containing the node'),
|
|
14
|
+
nodeId: z.string().describe('The node ID containing the todo'),
|
|
15
|
+
todoId: z.string().describe('The todo ID to toggle'),
|
|
16
|
+
completed: z
|
|
17
|
+
.boolean()
|
|
18
|
+
.optional()
|
|
19
|
+
.describe('Set explicit completion state. Omit to toggle current state.'),
|
|
20
|
+
});
|
|
21
|
+
export const listTodosSchema = z.object({
|
|
22
|
+
boardId: z.string().describe('The board ID containing the node'),
|
|
23
|
+
nodeId: z.string().describe('The node ID to list todos from'),
|
|
24
|
+
});
|
|
25
|
+
/**
|
|
26
|
+
* Add a new todo to a markdown node.
|
|
27
|
+
* Generates a UUID for the todo and appends it with the next order value.
|
|
28
|
+
* Note: Only markdown/text nodes support todos; code nodes do not.
|
|
29
|
+
*/
|
|
30
|
+
export async function handleAddTodo(client, params) {
|
|
31
|
+
try {
|
|
32
|
+
const node = await client.getNode({
|
|
33
|
+
boardId: params.boardId,
|
|
34
|
+
nodeId: params.nodeId,
|
|
35
|
+
});
|
|
36
|
+
const existingTodos = node.todos || [];
|
|
37
|
+
const newTodo = {
|
|
38
|
+
id: randomUUID(),
|
|
39
|
+
text: params.text,
|
|
40
|
+
completed: false,
|
|
41
|
+
order: existingTodos.length,
|
|
42
|
+
};
|
|
43
|
+
const updatedTodos = [...existingTodos, newTodo];
|
|
44
|
+
await client.updateNode({
|
|
45
|
+
boardId: params.boardId,
|
|
46
|
+
nodeId: params.nodeId,
|
|
47
|
+
data: { todos: updatedTodos },
|
|
48
|
+
});
|
|
49
|
+
return {
|
|
50
|
+
content: [
|
|
51
|
+
{
|
|
52
|
+
type: 'text',
|
|
53
|
+
text: JSON.stringify({
|
|
54
|
+
success: true,
|
|
55
|
+
todo: newTodo,
|
|
56
|
+
totalTodos: updatedTodos.length,
|
|
57
|
+
}, null, 2),
|
|
58
|
+
},
|
|
59
|
+
],
|
|
60
|
+
};
|
|
61
|
+
}
|
|
62
|
+
catch (error) {
|
|
63
|
+
return {
|
|
64
|
+
content: [
|
|
65
|
+
{
|
|
66
|
+
type: 'text',
|
|
67
|
+
text: `Failed to add todo: ${formatError(error)}`,
|
|
68
|
+
},
|
|
69
|
+
],
|
|
70
|
+
isError: true,
|
|
71
|
+
};
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
/**
|
|
75
|
+
* Toggle a todo's completion status.
|
|
76
|
+
* Can set explicit state or toggle current state.
|
|
77
|
+
*/
|
|
78
|
+
export async function handleToggleTodo(client, params) {
|
|
79
|
+
try {
|
|
80
|
+
const node = await client.getNode({
|
|
81
|
+
boardId: params.boardId,
|
|
82
|
+
nodeId: params.nodeId,
|
|
83
|
+
});
|
|
84
|
+
const existingTodos = node.todos || [];
|
|
85
|
+
const todoIndex = existingTodos.findIndex(t => t.id === params.todoId);
|
|
86
|
+
if (todoIndex === -1) {
|
|
87
|
+
return {
|
|
88
|
+
content: [
|
|
89
|
+
{
|
|
90
|
+
type: 'text',
|
|
91
|
+
text: `Todo not found: ${params.todoId}`,
|
|
92
|
+
},
|
|
93
|
+
],
|
|
94
|
+
isError: true,
|
|
95
|
+
};
|
|
96
|
+
}
|
|
97
|
+
const todo = existingTodos[todoIndex];
|
|
98
|
+
const newCompleted = params.completed ?? !todo.completed;
|
|
99
|
+
const updatedTodos = existingTodos.map(t => t.id === params.todoId ? { ...t, completed: newCompleted } : t);
|
|
100
|
+
await client.updateNode({
|
|
101
|
+
boardId: params.boardId,
|
|
102
|
+
nodeId: params.nodeId,
|
|
103
|
+
data: { todos: updatedTodos },
|
|
104
|
+
});
|
|
105
|
+
const updatedTodo = updatedTodos[todoIndex];
|
|
106
|
+
return {
|
|
107
|
+
content: [
|
|
108
|
+
{
|
|
109
|
+
type: 'text',
|
|
110
|
+
text: JSON.stringify({
|
|
111
|
+
success: true,
|
|
112
|
+
todo: updatedTodo,
|
|
113
|
+
action: newCompleted ? 'completed' : 'uncompleted',
|
|
114
|
+
}, null, 2),
|
|
115
|
+
},
|
|
116
|
+
],
|
|
117
|
+
};
|
|
118
|
+
}
|
|
119
|
+
catch (error) {
|
|
120
|
+
return {
|
|
121
|
+
content: [
|
|
122
|
+
{
|
|
123
|
+
type: 'text',
|
|
124
|
+
text: `Failed to toggle todo: ${formatError(error)}`,
|
|
125
|
+
},
|
|
126
|
+
],
|
|
127
|
+
isError: true,
|
|
128
|
+
};
|
|
129
|
+
}
|
|
130
|
+
}
|
|
131
|
+
/**
|
|
132
|
+
* List all todos on a node with summary statistics.
|
|
133
|
+
*/
|
|
134
|
+
export async function handleListTodos(client, params) {
|
|
135
|
+
try {
|
|
136
|
+
const node = await client.getNode({
|
|
137
|
+
boardId: params.boardId,
|
|
138
|
+
nodeId: params.nodeId,
|
|
139
|
+
});
|
|
140
|
+
const todos = (node.todos || []).sort((a, b) => a.order - b.order);
|
|
141
|
+
const completedCount = todos.filter(t => t.completed).length;
|
|
142
|
+
return {
|
|
143
|
+
content: [
|
|
144
|
+
{
|
|
145
|
+
type: 'text',
|
|
146
|
+
text: JSON.stringify({
|
|
147
|
+
nodeId: params.nodeId,
|
|
148
|
+
nodeName: node.name,
|
|
149
|
+
todos,
|
|
150
|
+
summary: {
|
|
151
|
+
total: todos.length,
|
|
152
|
+
completed: completedCount,
|
|
153
|
+
remaining: todos.length - completedCount,
|
|
154
|
+
},
|
|
155
|
+
}, null, 2),
|
|
156
|
+
},
|
|
157
|
+
],
|
|
158
|
+
};
|
|
159
|
+
}
|
|
160
|
+
catch (error) {
|
|
161
|
+
return {
|
|
162
|
+
content: [
|
|
163
|
+
{
|
|
164
|
+
type: 'text',
|
|
165
|
+
text: `Failed to list todos: ${formatError(error)}`,
|
|
166
|
+
},
|
|
167
|
+
],
|
|
168
|
+
isError: true,
|
|
169
|
+
};
|
|
170
|
+
}
|
|
171
|
+
}
|
package/dist/tools/update.js
CHANGED
|
@@ -4,6 +4,23 @@ import { formatError } from '../utils/errors.js';
|
|
|
4
4
|
import { pixelsToUnits, unitsToPixels } from '../utils/units.js';
|
|
5
5
|
import { snapToGrid } from './grid-calc.js';
|
|
6
6
|
import { calculateNodeSize } from './size-calc.js';
|
|
7
|
+
/**
|
|
8
|
+
* Node types that can be converted between safely.
|
|
9
|
+
* These are text-based types where content can be migrated without data loss.
|
|
10
|
+
* Note: text nodes are accepted for conversion (legacy) but MCP only creates markdown.
|
|
11
|
+
*/
|
|
12
|
+
const CONVERTIBLE_TYPES = [
|
|
13
|
+
NodeType.Text,
|
|
14
|
+
NodeType.Markdown,
|
|
15
|
+
NodeType.Code,
|
|
16
|
+
NodeType.URL,
|
|
17
|
+
];
|
|
18
|
+
/**
|
|
19
|
+
* Check if a type is convertible (text-based).
|
|
20
|
+
*/
|
|
21
|
+
function isConvertibleType(type) {
|
|
22
|
+
return CONVERTIBLE_TYPES.includes(type);
|
|
23
|
+
}
|
|
7
24
|
export const updateSchema = z.object({
|
|
8
25
|
boardId: z.string().describe('The board ID containing the node'),
|
|
9
26
|
nodeId: z.string().describe('The node ID to update'),
|
|
@@ -11,8 +28,12 @@ export const updateSchema = z.object({
|
|
|
11
28
|
.string()
|
|
12
29
|
.optional()
|
|
13
30
|
.describe('Move node to a different board. Provide the target board ID to move the node while preserving its type and properties.'),
|
|
31
|
+
type: z
|
|
32
|
+
.enum(['markdown', 'code', 'url'])
|
|
33
|
+
.optional()
|
|
34
|
+
.describe('Change node type. Safe conversions: markdown ↔ code. Content is auto-migrated between fields.'),
|
|
14
35
|
name: z.string().optional().describe('New name for the node'),
|
|
15
|
-
content: z.string().optional().describe('Content for
|
|
36
|
+
content: z.string().optional().describe('Content for markdown nodes.'),
|
|
16
37
|
code: z
|
|
17
38
|
.string()
|
|
18
39
|
.optional()
|
|
@@ -34,8 +55,70 @@ export const updateSchema = z.object({
|
|
|
34
55
|
.optional()
|
|
35
56
|
.default(true)
|
|
36
57
|
.describe('Auto-resize node when name changes (default: true). Set to false to keep current size.'),
|
|
58
|
+
todos: z
|
|
59
|
+
.array(z.object({
|
|
60
|
+
id: z.string().describe('Unique identifier (UUID v4)'),
|
|
61
|
+
text: z.string().describe('Todo text content'),
|
|
62
|
+
completed: z.boolean().describe('Completion status'),
|
|
63
|
+
order: z.number().describe('Sort order (0-based)'),
|
|
64
|
+
}))
|
|
65
|
+
.optional()
|
|
66
|
+
.describe('Replace all todos on this markdown node. Only markdown/text nodes support todos; code nodes do not. Each todo needs id, text, completed, and order fields.'),
|
|
37
67
|
});
|
|
38
68
|
const POSITION_FIELDS = ['x', 'y', 'width', 'height'];
|
|
69
|
+
/**
|
|
70
|
+
* Compute migration fields when converting TO code type.
|
|
71
|
+
*/
|
|
72
|
+
function migrateToCode(node, params, fields) {
|
|
73
|
+
// Migrate content → code (unless code explicitly provided)
|
|
74
|
+
if (params.code === undefined && params.content === undefined) {
|
|
75
|
+
fields.code = node.content || '';
|
|
76
|
+
}
|
|
77
|
+
// Always clear content field when moving to code type
|
|
78
|
+
fields.content = null;
|
|
79
|
+
}
|
|
80
|
+
/**
|
|
81
|
+
* Compute migration fields when converting FROM code type.
|
|
82
|
+
*/
|
|
83
|
+
function migrateFromCode(node, params, fields) {
|
|
84
|
+
// Always clear code field
|
|
85
|
+
fields.code = null;
|
|
86
|
+
// Migrate code → content (unless content explicitly provided)
|
|
87
|
+
if (params.content === undefined && params.code === undefined) {
|
|
88
|
+
fields.content = node.code || '';
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
/**
|
|
92
|
+
* Validate and compute migration fields for a type change.
|
|
93
|
+
*
|
|
94
|
+
* Returns the fields that need to be set/unset to migrate content
|
|
95
|
+
* between node types, or an error if the conversion is unsafe.
|
|
96
|
+
*/
|
|
97
|
+
export function validateTypeChange(currentType, targetType, node, params) {
|
|
98
|
+
if (currentType === targetType) {
|
|
99
|
+
return { valid: true, fields: {} };
|
|
100
|
+
}
|
|
101
|
+
if (!isConvertibleType(currentType)) {
|
|
102
|
+
return {
|
|
103
|
+
valid: false,
|
|
104
|
+
error: `Cannot convert from '${currentType}' to '${targetType}'. Only markdown, code, and url types support conversion.`,
|
|
105
|
+
};
|
|
106
|
+
}
|
|
107
|
+
const fields = { type: targetType };
|
|
108
|
+
if (targetType === NodeType.URL && !params.url && !node.url) {
|
|
109
|
+
return {
|
|
110
|
+
valid: false,
|
|
111
|
+
error: `Converting to 'url' type requires a url field. Provide url parameter.`,
|
|
112
|
+
};
|
|
113
|
+
}
|
|
114
|
+
if (targetType === NodeType.Code) {
|
|
115
|
+
migrateToCode(node, params, fields);
|
|
116
|
+
}
|
|
117
|
+
else if (currentType === NodeType.Code) {
|
|
118
|
+
migrateFromCode(node, params, fields);
|
|
119
|
+
}
|
|
120
|
+
return { valid: true, fields };
|
|
121
|
+
}
|
|
39
122
|
/**
|
|
40
123
|
* Add content fields to update data based on node type.
|
|
41
124
|
*/
|
|
@@ -61,15 +144,21 @@ function addContentFields(data, params, nodeType) {
|
|
|
61
144
|
/**
|
|
62
145
|
* Builds the update data object, mapping content/url to the correct field based on node type.
|
|
63
146
|
* Code nodes store their content in `code`, url nodes use `url`, others use `content`.
|
|
147
|
+
*
|
|
148
|
+
* @param params - Update parameters from the tool call
|
|
149
|
+
* @param nodeType - The target node type (after any type change)
|
|
150
|
+
* @param migrationFields - Fields from type migration (type change, content migration)
|
|
64
151
|
*/
|
|
65
|
-
function buildUpdateData(params, nodeType) {
|
|
66
|
-
const data = {};
|
|
152
|
+
function buildUpdateData(params, nodeType, migrationFields = {}) {
|
|
153
|
+
const data = { ...migrationFields };
|
|
67
154
|
if (params.name !== undefined)
|
|
68
155
|
data.name = params.name;
|
|
69
156
|
if (params.color !== undefined)
|
|
70
157
|
data.color = params.color;
|
|
71
158
|
if (params.targetBoardId !== undefined)
|
|
72
159
|
data.board = params.targetBoardId;
|
|
160
|
+
if (params.todos !== undefined)
|
|
161
|
+
data.todos = params.todos;
|
|
73
162
|
addContentFields(data, params, nodeType);
|
|
74
163
|
for (const field of POSITION_FIELDS) {
|
|
75
164
|
const value = params[field];
|
|
@@ -116,29 +205,55 @@ function formatSuccessResponse(node) {
|
|
|
116
205
|
content: [{ type: 'text', text: JSON.stringify(result, null, 2) }],
|
|
117
206
|
};
|
|
118
207
|
}
|
|
208
|
+
/**
|
|
209
|
+
* Process type change request and return migration context.
|
|
210
|
+
*/
|
|
211
|
+
function processTypeChange(currentNode, params) {
|
|
212
|
+
if (!params.type || params.type === currentNode.type) {
|
|
213
|
+
return { valid: true, fields: {}, targetType: currentNode.type };
|
|
214
|
+
}
|
|
215
|
+
const result = validateTypeChange(currentNode.type, params.type, currentNode, params);
|
|
216
|
+
if (!result.valid) {
|
|
217
|
+
return result;
|
|
218
|
+
}
|
|
219
|
+
return { valid: true, fields: result.fields, targetType: params.type };
|
|
220
|
+
}
|
|
221
|
+
/**
|
|
222
|
+
* Build params with auto-size dimensions applied.
|
|
223
|
+
*/
|
|
224
|
+
function applyAutoSize(params, autoSize) {
|
|
225
|
+
return {
|
|
226
|
+
...params,
|
|
227
|
+
width: params.width !== undefined
|
|
228
|
+
? params.width
|
|
229
|
+
: autoSize?.width !== undefined
|
|
230
|
+
? pixelsToUnits(autoSize.width)
|
|
231
|
+
: undefined,
|
|
232
|
+
height: params.height !== undefined
|
|
233
|
+
? params.height
|
|
234
|
+
: autoSize?.height !== undefined
|
|
235
|
+
? pixelsToUnits(autoSize.height)
|
|
236
|
+
: undefined,
|
|
237
|
+
};
|
|
238
|
+
}
|
|
119
239
|
export async function handleUpdate(client, params) {
|
|
120
240
|
try {
|
|
121
241
|
const { node: currentNode, autoSize } = await getNodeContext(client, params);
|
|
122
|
-
const
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
? params.height
|
|
132
|
-
: autoSize?.height !== undefined
|
|
133
|
-
? pixelsToUnits(autoSize.height)
|
|
134
|
-
: undefined,
|
|
135
|
-
}, currentNode.type);
|
|
242
|
+
const typeChange = processTypeChange(currentNode, params);
|
|
243
|
+
if (!typeChange.valid) {
|
|
244
|
+
return {
|
|
245
|
+
content: [{ type: 'text', text: typeChange.error }],
|
|
246
|
+
isError: true,
|
|
247
|
+
};
|
|
248
|
+
}
|
|
249
|
+
const paramsWithSize = applyAutoSize(params, autoSize);
|
|
250
|
+
const data = buildUpdateData(paramsWithSize, typeChange.targetType, typeChange.fields);
|
|
136
251
|
if (!data) {
|
|
137
252
|
return {
|
|
138
253
|
content: [
|
|
139
254
|
{
|
|
140
255
|
type: 'text',
|
|
141
|
-
text: `At least one field to update must be provided (name, content, url, color, x, y, width, height, targetBoardId).`,
|
|
256
|
+
text: `At least one field to update must be provided (name, content, url, color, x, y, width, height, targetBoardId, type, todos).`,
|
|
142
257
|
},
|
|
143
258
|
],
|
|
144
259
|
isError: true,
|