diff --git a/mcp-server/N8N-EXAMPLES.md b/mcp-server/N8N-EXAMPLES.md index 4cb78f9..4d29a88 100644 --- a/mcp-server/N8N-EXAMPLES.md +++ b/mcp-server/N8N-EXAMPLES.md @@ -409,6 +409,29 @@ All examples assume an MCP Client node configured with Streamable HTTP and `x-ap } ``` +### Create a sub-notebook (hierarchical) + +```json +{ + "tool": "create_notebook", + "arguments": { + "name": "Project Alpha", + "icon": "🚀", + "color": "#10B981", + "parentId": "{{ $json.parentNotebookId }}" + } +} +``` + +### Get notebook hierarchy (tree view) + +```json +{ + "tool": "get_notebook_hierarchy", + "arguments": {} +} +``` + ### Delete a notebook (notes go to Inbox) ```json @@ -422,6 +445,33 @@ All examples assume an MCP Client node configured with Streamable HTTP and `x-ap --- +## Batch Operations + +### Batch move notes to a notebook + +```json +{ + "tool": "batch_move_notes", + "arguments": { + "ids": ["{{ $json.noteId1 }}", "{{ $json.noteId2 }}", "{{ $json.noteId3 }}"], + "notebookId": "{{ $json.targetNotebookId }}" + } +} +``` + +### Batch delete notes + +```json +{ + "tool": "batch_delete_notes", + "arguments": { + "ids": ["{{ $json.noteId1 }}", "{{ $json.noteId2 }}"] + } +} +``` + +--- + ## Labels ### Create a label inside a notebook diff --git a/mcp-server/README.md b/mcp-server/README.md index 229e323..1e9b38b 100644 --- a/mcp-server/README.md +++ b/mcp-server/README.md @@ -48,7 +48,7 @@ Generate API keys from the Memento web UI: **Settings > MCP**. curl -H "x-api-key: mcp_sk_xxx" http://localhost:3001/ ``` -## Available Tools (22) +## Available Tools (25) ### Notes (11) @@ -65,6 +65,8 @@ curl -H "x-api-key: mcp_sk_xxx" http://localhost:3001/ | `toggle_archive` | Archive/unarchive a note | | `export_notes` | Export notes as JSON | | `import_notes` | Import notes from JSON | +| `batch_move_notes` | Move multiple notes at once | +| `batch_delete_notes` | Delete multiple notes at once | ### Notebooks (6) @@ -76,6 +78,7 @@ curl -H "x-api-key: mcp_sk_xxx" http://localhost:3001/ | `update_notebook` | Update a notebook | | `delete_notebook` | Delete a notebook | | `reorder_notebooks` | Reorder notebooks | +| `get_notebook_hierarchy` | Get tree structure of notebooks | ### Labels (4) diff --git a/mcp-server/n8n-workflow-notebook-management.json b/mcp-server/n8n-workflow-notebook-management.json index a0ce815..6ebde7e 100644 --- a/mcp-server/n8n-workflow-notebook-management.json +++ b/mcp-server/n8n-workflow-notebook-management.json @@ -96,6 +96,10 @@ { "name": "color", "value": "={{ $json.color || '#3B82F6' }}" + }, + { + "name": "parentId", + "value": "={{ $json.parentId }}" } ] } @@ -155,6 +159,10 @@ { "name": "color", "value": "={{ $json.color }}" + }, + { + "name": "parentId", + "value": "={{ $json.parentId }}" } ] } diff --git a/mcp-server/tools.js b/mcp-server/tools.js index 6ec20b0..fb05f45 100644 --- a/mcp-server/tools.js +++ b/mcp-server/tools.js @@ -204,6 +204,29 @@ const toolDefinitions = [ required: ['id'], }, }, + { + name: 'batch_move_notes', + description: 'Move multiple notes to a notebook simultaneously.', + inputSchema: { + type: 'object', + properties: { + ids: { type: 'array', items: { type: 'string' }, description: 'Array of Note IDs' }, + notebookId: { type: 'string', description: 'Target notebook ID, or null for Inbox' }, + }, + required: ['ids'], + }, + }, + { + name: 'batch_delete_notes', + description: 'Permanently delete multiple notes.', + inputSchema: { + type: 'object', + properties: { + ids: { type: 'array', items: { type: 'string' }, description: 'Array of Note IDs' }, + }, + required: ['ids'], + }, + }, { name: 'toggle_pin', description: 'Toggle the pin status of a note.', @@ -264,6 +287,7 @@ const toolDefinitions = [ icon: { type: 'string', description: 'Icon (emoji)', default: '📁' }, color: { type: 'string', description: 'Hex color', default: '#3B82F6' }, order: { type: 'number', description: 'Sort position (auto if omitted)' }, + parentId: { type: 'string', description: 'Parent notebook ID for sub-notebooks' }, }, required: ['name'], }, @@ -293,6 +317,7 @@ const toolDefinitions = [ icon: { type: 'string' }, color: { type: 'string' }, order: { type: 'number' }, + parentId: { type: 'string', description: 'Parent notebook ID (set to null to move to root)' }, }, required: ['id'], }, @@ -321,6 +346,11 @@ const toolDefinitions = [ required: ['notebookIds'], }, }, + { + name: 'get_notebook_hierarchy', + description: 'Get a nested tree structure of all notebooks (parents and children).', + inputSchema: { type: 'object', properties: {} }, + }, // ═══ LABELS ═══ { @@ -546,6 +576,26 @@ export function registerTools(server, prisma) { }); } + case 'batch_move_notes': { + const targetId = args.notebookId || null; + const ids = args.ids || []; + + await prisma.note.updateMany({ + where: { id: { in: ids }, ...(uid ? { userId: uid } : {}), trashedAt: null }, + data: { notebookId: targetId, updatedAt: new Date() }, + }); + + return textResult({ success: true, count: ids.length, notebookId: targetId }); + } + + case 'batch_delete_notes': { + const ids = args.ids || []; + await prisma.note.deleteMany({ + where: { id: { in: ids }, ...(uid ? { userId: uid } : {}), trashedAt: null }, + }); + return textResult({ success: true, count: ids.length }); + } + case 'toggle_pin': { const note = await prisma.note.findUnique({ where: { id: args.id, ...(uid ? { userId: uid } : {}), trashedAt: null }, @@ -711,6 +761,7 @@ export function registerTools(server, prisma) { icon: args.icon || '📁', color: args.color || '#3B82F6', order: nextOrder, + parentId: args.parentId || null, userId: uid || await ensureUserId(), }, include: { labels: true, _count: { select: { notes: true } } }, @@ -754,6 +805,7 @@ export function registerTools(server, prisma) { if ('icon' in args) d.icon = args.icon; if ('color' in args) d.color = args.color; if ('order' in args) d.order = args.order; + if ('parentId' in args) d.parentId = args.parentId; const where = { id: args.id, ...(uid ? { userId: uid } : {}) }; const notebook = await prisma.notebook.update({ @@ -799,6 +851,32 @@ export function registerTools(server, prisma) { return textResult({ success: true }); } + case 'get_notebook_hierarchy': { + const where = uid ? { userId: uid } : {}; + const notebooks = await prisma.notebook.findMany({ + where, + include: { + _count: { select: { notes: true } }, + }, + orderBy: { order: 'asc' }, + }); + + // Build tree + const nbMap = new Map(); + notebooks.forEach(nb => nbMap.set(nb.id, { ...nb, notesCount: nb._count.notes, children: [] })); + + const root = []; + nbMap.forEach(nb => { + if (nb.parentId && nbMap.has(nb.parentId)) { + nbMap.get(nb.parentId).children.push(nb); + } else { + root.push(nb); + } + }); + + return textResult(root); + } + // ═══ LABELS ═══ case 'create_label': { const existing = await prisma.label.findFirst({