@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 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
- // Process node updates in parallel
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(['text', 'markdown', 'code', 'url'])
24
+ .enum(['markdown', 'code', 'url'])
17
25
  .optional()
18
- .default('text')
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 xPixels = snapToGrid(nodeInput.x !== undefined ? unitsToPixels(nodeInput.x) : unitsToPixels(6));
56
- const yPixels = snapToGrid(nodeInput.y !== undefined ? unitsToPixels(nodeInput.y) : unitsToPixels(6));
57
- const widthPixels = snapToGrid(nodeInput.width !== undefined
58
- ? unitsToPixels(nodeInput.width)
59
- : autoSize.width);
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: xPixels,
76
- y: yPixels,
77
- width: widthPixels,
78
- height: heightPixels,
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(xPixels),
89
- y: pixelsToUnits(yPixels),
90
- width: pixelsToUnits(widthPixels),
91
- height: pixelsToUnits(heightPixels),
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
  }
@@ -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 text/markdown nodes.'),
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(['text', 'markdown', 'code', 'url'])
62
+ .enum(['markdown', 'code', 'url'])
63
63
  .optional()
64
- .default('text')
65
- .describe('Node type: text (default), markdown, code, or url'),
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
- return content && content.trim().length > 0;
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 {
@@ -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;
@@ -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
  ],
@@ -38,7 +38,7 @@ export const sizeCalcSchema = z.object({
38
38
  type: z
39
39
  .string()
40
40
  .optional()
41
- .default('text')
41
+ .default('markdown')
42
42
  .describe('Node type (affects icon space calculation)'),
43
43
  });
44
44
  function roundToGrid(value) {
@@ -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
+ }
@@ -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 text/markdown nodes.'),
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 data = buildUpdateData({
123
- ...params,
124
- // Convert autoSize from pixels to units since buildUpdateData expects units
125
- width: params.width !== undefined
126
- ? params.width
127
- : autoSize?.width !== undefined
128
- ? pixelsToUnits(autoSize.width)
129
- : undefined,
130
- height: params.height !== undefined
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,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@mentagen/mcp",
3
- "version": "0.7.0",
3
+ "version": "0.8.1",
4
4
  "description": "MCP server for Mentagen knowledge base integration with Cursor",
5
5
  "type": "module",
6
6
  "bin": "dist/index.js",