feat(mcp): add all 4 note types, translate N8N docs to English, add N8N workflow examples
All checks were successful
Deploy to Production / Build and Deploy (push) Successful in 55s

- tools.js: expose type enum ['text','markdown','richtext','checklist'] in create_note & update_note
  - default changed from 'text' to 'richtext' (matches Prisma schema)
  - isMarkdown marked as deprecated in favour of type='markdown'
- N8N-CONFIG.md: full French → English translation
- N8N-WORKFLOWS.md: full French → English translation
- N8N-EXAMPLES.md: new comprehensive examples for all 22 MCP tools + workflow patterns
- n8n-workflow-mcp-reminder-bot.json: cron → get_due_reminders → Telegram → mark done
- n8n-workflow-mcp-email-to-note.json: IMAP → create_note → urgent Slack alert
- n8n-workflow-mcp-daily-digest.json: 8AM cron → notes + reminders digest → save + Slack
- n8n-workflow-mcp-webhook-to-note.json: universal webhook → create_note → respond
- notebooks-list.tsx: fix truncated notebook names (pe-24→pe-14), replace hover overlay with Tooltip
This commit is contained in:
Antigravity
2026-05-03 20:49:11 +00:00
parent 0311c97a35
commit 0ebf10344d
9 changed files with 1203 additions and 113 deletions

View File

@@ -1,34 +1,34 @@
# Configuration N8N - Memento MCP Server # N8N Configuration Memento MCP Server
## Configuration MCP Client dans N8N ## MCP Client Setup in N8N
Le serveur MCP utilise le transport **Streamable HTTP** (remplace l'ancien SSE). The MCP server uses **Streamable HTTP** transport (replaces the legacy SSE transport).
### 1. Generer une cle API ### 1. Generate an API Key
Dans Memento : **Parametres > MCP > Generer une cle** In Memento: **Settings > MCP > Generate a key**
La cle a le format `mcp_sk_...` et est associee a votre compte utilisateur. Seules vos notes seront accessibles. The key has the format `mcp_sk_...` and is scoped to your user account. Only your notes will be accessible.
### 2. Configurer le noeud MCP Client dans N8N ### 2. Configure the MCP Client Node in N8N
1. Ajouter un noeud **MCP Client** dans votre workflow 1. Add an **MCP Client** node to your workflow
2. **Server Transport** : `Streamable HTTP` 2. **Server Transport**: `Streamable HTTP`
3. **MCP Endpoint URL** : `http://memento-mcp:3001/mcp` (Docker) ou `http://VOTRE_IP:3001/mcp` 3. **MCP Endpoint URL**: `http://memento-mcp:3001/mcp` (Docker) or `http://YOUR_IP:3001/mcp`
4. **Authentication** : Header Auth 4. **Authentication**: Header Auth
- Header Name : `x-api-key` - Header Name: `x-api-key`
- Header Value : votre cle API (`mcp_sk_...`) - Header Value: your API key (`mcp_sk_...`)
### Alternative : curl ### Alternative: curl
```bash ```bash
# Health check # Health check
curl -H "x-api-key: mcp_sk_votrecle" http://localhost:3001/ curl -H "x-api-key: mcp_sk_yourkey" http://localhost:3001/
# Lister les outils # List available tools
curl -X POST http://localhost:3001/mcp \ curl -X POST http://localhost:3001/mcp \
-H "Content-Type: application/json" \ -H "Content-Type: application/json" \
-H "x-api-key: mcp_sk_votrecle" \ -H "x-api-key: mcp_sk_yourkey" \
-d '{ -d '{
"jsonrpc": "2.0", "jsonrpc": "2.0",
"id": 1, "id": 1,
@@ -36,10 +36,10 @@ curl -X POST http://localhost:3001/mcp \
"params": {} "params": {}
}' }'
# Creer une note # Create a note
curl -X POST http://localhost:3001/mcp \ curl -X POST http://localhost:3001/mcp \
-H "Content-Type: application/json" \ -H "Content-Type: application/json" \
-H "x-api-key: mcp_sk_votrecle" \ -H "x-api-key: mcp_sk_yourkey" \
-d '{ -d '{
"jsonrpc": "2.0", "jsonrpc": "2.0",
"id": 2, "id": 2,
@@ -47,72 +47,81 @@ curl -X POST http://localhost:3001/mcp \
"params": { "params": {
"name": "create_note", "name": "create_note",
"arguments": { "arguments": {
"title": "Ma note", "title": "My note",
"content": "Contenu de la note" "content": "Note content here"
} }
} }
}' }'
``` ```
## Outils disponibles (22) ## Available Tools (22)
### Notes (11) ### Notes (11)
| Outil | Description | | Tool | Description |
|-------|-------------| |------|-------------|
| `create_note` | Creer une note | | `create_note` | Create a note |
| `get_notes` | Lister les notes | | `get_notes` | List notes |
| `get_note` | Recuperer une note par ID | | `get_note` | Get a note by ID |
| `update_note` | Modifier une note | | `update_note` | Update a note |
| `delete_note` | Supprimer une note | | `delete_note` | Delete a note |
| `search_notes` | Rechercher par mot-cle | | `search_notes` | Search by keyword |
| `move_note` | Deplacer vers un notebook | | `move_note` | Move to a notebook |
| `toggle_pin` | Epingler/Depingler | | `toggle_pin` | Pin / unpin |
| `toggle_archive` | Archiver/Desarchiver | | `toggle_archive` | Archive / unarchive |
| `export_notes` | Exporter en JSON | | `export_notes` | Export as JSON |
| `import_notes` | Importer depuis JSON | | `import_notes` | Import from JSON |
#### Note Types
| `type` value | Description |
|--------------|-------------|
| `richtext` | Rich text editor — **default** |
| `markdown` | Markdown rendered (use instead of `isMarkdown`) |
| `text` | Plain text |
| `checklist` | Interactive checklist with `checkItems` |
### Notebooks (6) ### Notebooks (6)
| Outil | Description | | Tool | Description |
|-------|-------------| |------|-------------|
| `create_notebook` | Creer un notebook | | `create_notebook` | Create a notebook |
| `get_notebooks` | Lister les notebooks | | `get_notebooks` | List all notebooks |
| `get_notebook` | Details d'un notebook | | `get_notebook` | Get notebook details |
| `update_notebook` | Modifier un notebook | | `update_notebook` | Update a notebook |
| `delete_notebook` | Supprimer un notebook | | `delete_notebook` | Delete a notebook |
| `reorder_notebooks` | Reordonner | | `reorder_notebooks` | Reorder notebooks |
### Labels (4) ### Labels (4)
| Outil | Description | | Tool | Description |
|-------|-------------| |------|-------------|
| `create_label` | Creer un label | | `create_label` | Create a label |
| `get_labels` | Lister les labels | | `get_labels` | List labels |
| `update_label` | Modifier un label | | `update_label` | Update a label |
| `delete_label` | Supprimer un label | | `delete_label` | Delete a label |
### Rappels (1) ### Reminders (1)
| Outil | Description | | Tool | Description |
|-------|-------------| |------|-------------|
| `get_due_reminders` | Recuperer les rappels dus | | `get_due_reminders` | Get due reminders |
## Endpoints HTTP ## HTTP Endpoints
| Endpoint | Methode | Description | | Endpoint | Method | Description |
|----------|---------|-------------| |----------|--------|-------------|
| `/` | GET | Health check | | `/` | GET | Health check |
| `/mcp` | GET/POST | Endpoint MCP principal | | `/mcp` | GET/POST | Main MCP endpoint |
| `/sse` | GET/POST | Legacy (redirige vers `/mcp`) | | `/sse` | GET/POST | Legacy (redirects to `/mcp`) |
| `/sessions` | GET | Sessions actives | | `/sessions` | GET | Active sessions |
## Securite ## Security
- **Authentification obligatoire** en production (`MCP_REQUIRE_AUTH=true` dans Docker) - **Authentication required** in production (`MCP_REQUIRE_AUTH=true` in Docker)
- Les cles API sont gerees depuis **Parametres > MCP** dans Memento - API keys are managed from **Settings > MCP** in Memento
- Chaque cle est scopee a un utilisateur : seules ses notes sont accessibles - Each key is scoped to a single user: only their notes are accessible
- Les cles sont hashees en base (SHA256), seul le raw key est montre a la creation - Keys are hashed in the database (SHA256); the raw key is only shown once at creation
## Ports ## Ports

571
mcp-server/N8N-EXAMPLES.md Normal file
View File

@@ -0,0 +1,571 @@
# N8N Examples — Memento MCP Server
Practical, copy-paste-ready N8N workflow snippets for every Memento MCP tool.
All examples assume an MCP Client node configured with Streamable HTTP and `x-api-key` authentication.
## Note Types
| `type` value | Description |
|--------------|-------------|
| `richtext` | Rich text editor — **default** |
| `markdown` | Markdown rendered (preferred over deprecated `isMarkdown`) |
| `text` | Plain text |
| `checklist` | Interactive checklist with `checkItems` |
> **Note**: `isMarkdown: true` is deprecated. Use `"type": "markdown"` instead.
---
## Notes
### Create a simple text note
```json
{
"tool": "create_note",
"arguments": {
"title": "Quick idea",
"content": "Explore serverless architecture for the new API gateway.",
"color": "yellow",
"labels": ["idea", "architecture"]
}
}
```
### Create a pinned markdown note
```json
{
"tool": "create_note",
"arguments": {
"title": "Project README",
"content": "# My Project\n\nThis is the main documentation.\n\n## Getting Started\n\n```bash\nnpm install\nnpm run dev\n```",
"isMarkdown": true,
"isPinned": true,
"color": "blue",
"size": "large"
}
}
```
### Create a checklist note
```json
{
"tool": "create_note",
"arguments": {
"title": "Weekly tasks",
"content": "Tasks for this week",
"type": "checklist",
"checkItems": [
{ "id": "1", "text": "Review pull requests", "checked": false },
{ "id": "2", "text": "Update deployment docs", "checked": false },
{ "id": "3", "text": "Team standup notes", "checked": true }
],
"color": "teal"
}
}
```
### Create a note with a reminder
```json
{
"tool": "create_note",
"arguments": {
"title": "Dentist appointment",
"content": "Dr. Martin — 10:30 AM. Bring insurance card.",
"reminder": "2025-06-15T10:00:00.000Z",
"reminderRecurrence": "yearly",
"color": "red",
"labels": ["health", "personal"]
}
}
```
### Create a note in a specific notebook
```json
{
"tool": "create_note",
"arguments": {
"title": "Sprint planning notes",
"content": "**Goal**: Ship user authentication by Friday.\n\n- Design review at 2PM\n- Backend API ready by Wednesday",
"isMarkdown": true,
"notebookId": "{{ $json.notebookId }}",
"labels": ["sprint", "planning"],
"color": "purple"
}
}
```
### Create a note with external links
```json
{
"tool": "create_note",
"arguments": {
"title": "Research resources",
"content": "Curated links for the ML paper review.",
"links": [
"https://arxiv.org/abs/2304.15004",
"https://huggingface.co/papers",
"https://paperswithcode.com"
],
"labels": ["research", "ml"],
"color": "green"
}
}
```
### Get all notes (lightweight)
```json
{
"tool": "get_notes",
"arguments": {
"limit": 50
}
}
```
### Get notes from a specific notebook
```json
{
"tool": "get_notes",
"arguments": {
"notebookId": "{{ $json.notebookId }}",
"limit": 100
}
}
```
### Get unfiled notes (Inbox)
```json
{
"tool": "get_notes",
"arguments": {
"notebookId": "inbox",
"limit": 50
}
}
```
### Get all notes including archived
```json
{
"tool": "get_notes",
"arguments": {
"includeArchived": true,
"limit": 200,
"fullDetails": true
}
}
```
### Get a single note by ID
```json
{
"tool": "get_note",
"arguments": {
"id": "{{ $json.noteId }}"
}
}
```
### Search notes by keyword
```json
{
"tool": "search_notes",
"arguments": {
"query": "deployment kubernetes"
}
}
```
### Search in a specific notebook
```json
{
"tool": "search_notes",
"arguments": {
"query": "{{ $json.searchTerm }}",
"notebookId": "{{ $json.notebookId }}",
"includeArchived": false
}
}
```
### Update a note's content
```json
{
"tool": "update_note",
"arguments": {
"id": "{{ $json.noteId }}",
"content": "{{ $json.newContent }}",
"color": "green"
}
}
```
### Mark a checklist item as done
```json
{
"tool": "update_note",
"arguments": {
"id": "{{ $json.noteId }}",
"checkItems": [
{ "id": "1", "text": "Review pull requests", "checked": true },
{ "id": "2", "text": "Update deployment docs", "checked": false }
]
}
}
```
### Add labels to an existing note
```json
{
"tool": "update_note",
"arguments": {
"id": "{{ $json.noteId }}",
"labels": ["ai-generated", "reviewed", "{{ $json.category }}"]
}
}
```
### Mark a reminder as done
```json
{
"tool": "update_note",
"arguments": {
"id": "{{ $json.noteId }}",
"isReminderDone": true
}
}
```
### Move a note to a notebook
```json
{
"tool": "move_note",
"arguments": {
"id": "{{ $json.noteId }}",
"notebookId": "{{ $json.targetNotebookId }}"
}
}
```
### Move a note to Inbox (no notebook)
```json
{
"tool": "move_note",
"arguments": {
"id": "{{ $json.noteId }}",
"notebookId": null
}
}
```
### Pin a note
```json
{
"tool": "toggle_pin",
"arguments": {
"id": "{{ $json.noteId }}"
}
}
```
### Archive a note
```json
{
"tool": "toggle_archive",
"arguments": {
"id": "{{ $json.noteId }}"
}
}
```
### Delete a note permanently
```json
{
"tool": "delete_note",
"arguments": {
"id": "{{ $json.noteId }}"
}
}
```
### Export all notes as JSON backup
```json
{
"tool": "export_notes",
"arguments": {}
}
```
### Import notes from a JSON backup
```json
{
"tool": "import_notes",
"arguments": {
"data": "{{ $json.exportPayload }}"
}
}
```
---
## Notebooks
### Create a notebook
```json
{
"tool": "create_notebook",
"arguments": {
"name": "Work Projects",
"icon": "💼",
"color": "#3B82F6"
}
}
```
### Create a notebook for a team/topic
```json
{
"tool": "create_notebook",
"arguments": {
"name": "Machine Learning Research",
"icon": "🤖",
"color": "#8B5CF6"
}
}
```
### List all notebooks
```json
{
"tool": "get_notebooks",
"arguments": {}
}
```
### Get a notebook with its notes
```json
{
"tool": "get_notebook",
"arguments": {
"id": "{{ $json.notebookId }}"
}
}
```
### Rename a notebook
```json
{
"tool": "update_notebook",
"arguments": {
"id": "{{ $json.notebookId }}",
"name": "Q3 2025 Projects",
"color": "#10B981"
}
}
```
### Reorder notebooks
```json
{
"tool": "reorder_notebooks",
"arguments": {
"notebookIds": [
"{{ $json.ids[0] }}",
"{{ $json.ids[1] }}",
"{{ $json.ids[2] }}"
]
}
}
```
### Delete a notebook (notes go to Inbox)
```json
{
"tool": "delete_notebook",
"arguments": {
"id": "{{ $json.notebookId }}"
}
}
```
---
## Labels
### Create a label inside a notebook
```json
{
"tool": "create_label",
"arguments": {
"name": "urgent",
"color": "red",
"notebookId": "{{ $json.notebookId }}"
}
}
```
### Create common project labels
```json
{
"tool": "create_label",
"arguments": {
"name": "in-progress",
"color": "orange",
"notebookId": "{{ $json.notebookId }}"
}
}
```
```json
{
"tool": "create_label",
"arguments": {
"name": "done",
"color": "green",
"notebookId": "{{ $json.notebookId }}"
}
}
```
### List labels in a notebook
```json
{
"tool": "get_labels",
"arguments": {
"notebookId": "{{ $json.notebookId }}"
}
}
```
### List all labels (global)
```json
{
"tool": "get_labels",
"arguments": {}
}
```
### Rename a label
```json
{
"tool": "update_label",
"arguments": {
"id": "{{ $json.labelId }}",
"name": "high-priority",
"color": "red"
}
}
```
### Delete a label
```json
{
"tool": "delete_label",
"arguments": {
"id": "{{ $json.labelId }}"
}
}
```
---
## Reminders
### Get all due reminders (for cron automation)
```json
{
"tool": "get_due_reminders",
"arguments": {}
}
```
> **Tip**: Schedule this every 30 minutes with a cron trigger (`0 */30 * * * *`), then loop over results and send notifications via Slack, Telegram, or email.
---
## Common Workflow Patterns
### Pattern: Email → Note
1. **Email Trigger (IMAP)** — fires on new email
2. **MCP Client**`create_note`
```json
{
"title": "{{ $json.subject }}",
"content": "**From**: {{ $json.from }}\n\n{{ $json.text }}",
"labels": ["email"],
"color": "blue",
"isMarkdown": true
}
```
### Pattern: Reminder Notifications
1. **Schedule Trigger** — every 30 minutes
2. **MCP Client** → `get_due_reminders`
3. **IF** — results not empty
4. **Loop over items** → send Slack/Telegram message per reminder
5. **MCP Client** → `update_note` with `isReminderDone: true`
### Pattern: AI Summarization
1. **MCP Client** → `search_notes` with a topic query
2. **OpenAI / Anthropic node** — summarize the returned notes
3. **MCP Client** → `create_note` with the summary as content, labeled `ai-summary`
### Pattern: Daily Digest
1. **Schedule Trigger** — every day at 8 AM
2. **MCP Client** → `get_notes` with `limit: 20` (most recent)
3. **AI node** — generate a brief digest
4. **Email / Slack node** — send the digest
5. *(Optional)* **MCP Client** → `create_note` to archive the digest
### Pattern: Webhook → Structured Note
1. **Webhook Trigger** — receives JSON payload (e.g., from a form or CI/CD pipeline)
2. **Code node** — format the payload into markdown
3. **MCP Client** → `create_note`
```json
{
"title": "Build #{{ $json.buildNumber }} — {{ $json.status }}",
"content": "{{ $node['Code'].json.markdown }}",
"isMarkdown": true,
"color": "{{ $json.status === 'success' ? 'green' : 'red' }}",
"labels": ["ci-cd", "{{ $json.repo }}"]
}
```

View File

@@ -1,24 +1,24 @@
# Workflows N8N pour Memento MCP # N8N Workflows for Memento MCP
## Introduction ## Introduction
Exemples de workflows N8N utilisant le serveur MCP de Memento via le transport **Streamable HTTP**. Example N8N workflows using the Memento MCP server via **Streamable HTTP** transport.
**Prerequis** : une cle API generee depuis **Parametres > MCP** dans Memento. **Prerequisite**: an API key generated from **Settings > MCP** in Memento.
## Configuration du noeud MCP Client ## MCP Client Node Configuration
Dans chaque workflow, le noeud MCP Client doit etre configure ainsi : In every workflow, the MCP Client node must be configured as follows:
- **Server Transport** : `Streamable HTTP` - **Server Transport**: `Streamable HTTP`
- **MCP Endpoint URL** : `http://memento-mcp:3001/mcp` (Docker) ou `http://VOTRE_IP:3001/mcp` - **MCP Endpoint URL**: `http://memento-mcp:3001/mcp` (Docker) or `http://YOUR_IP:3001/mcp`
- **Authentication** : Header Auth avec `x-api-key` = votre cle API - **Authentication**: Header Auth with `x-api-key` = your API key
## Workflows disponibles ## Available Workflows
### 1. Create Note (`n8n-workflow-create-note.json`) ### 1. Create Note (`n8n-workflow-create-note.json`)
Cree des notes dans Memento avec classification par IA. Creates notes in Memento with AI-based classification.
```json ```json
{ {
@@ -34,7 +34,7 @@ Cree des notes dans Memento avec classification par IA.
### 2. Search & Summary (`n8n-workflow-search-summary.json`) ### 2. Search & Summary (`n8n-workflow-search-summary.json`)
Recherche des notes et genere un resume. Searches notes and generates a summary.
```json ```json
{ {
@@ -47,7 +47,7 @@ Recherche des notes et genere un resume.
### 3. Notebook Manager (`n8n-workflow-notebook-management.json`) ### 3. Notebook Manager (`n8n-workflow-notebook-management.json`)
CRUD complet des notebooks. Full CRUD for notebooks.
```json ```json
{ {
@@ -62,14 +62,14 @@ CRUD complet des notebooks.
### 4. Reminder Notifications (`n8n-workflow-reminder-notifications.json`) ### 4. Reminder Notifications (`n8n-workflow-reminder-notifications.json`)
Automatisation des rappels avec notifications. Automates reminders with notifications.
- Declencheur : Schedule (cron: `0 */30 * * * *`) - Trigger: Schedule (cron: `0 */30 * * * *`)
- Appelle `get_due_reminders` et envoie les notifications - Calls `get_due_reminders` and sends notifications
### 5. Label Manager (`n8n-workflow-label-management.json`) ### 5. Label Manager (`n8n-workflow-label-management.json`)
Gestion des labels. Label management.
```json ```json
{ {
@@ -84,20 +84,20 @@ Gestion des labels.
### 6. Email to Note (`n8n-workflow-email-integration.json`) ### 6. Email to Note (`n8n-workflow-email-integration.json`)
Conversion automatique d'emails en notes. Automatically converts emails into notes.
- Declencheur : Email Trigger (IMAP) - Trigger: Email Trigger (IMAP)
- Cree une note avec le contenu de l'email - Creates a note from the email content
## Importation ## Import Instructions
1. Ouvrir N8N 1. Open N8N
2. **Import from File** -> selectionner le fichier JSON 2. **Import from File** select the JSON file
3. Configurer le noeud MCP Client (URL + cle API) 3. Configure the MCP Client node (URL + API key)
4. Activer le workflow 4. Activate the workflow
## Securite ## Security
- Toujours utiliser une cle API dediee par workflow - Always use a dedicated API key per workflow
- Ne jamais exposer la cle dans les logs - Never expose the key in logs
- Restreindre l'acces reseau au port 3001 si possible - Restrict network access to port 3001 when possible

View File

@@ -0,0 +1,137 @@
{
"name": "Memento MCP — Daily Digest",
"nodes": [
{
"parameters": {
"rule": {
"interval": [
{
"field": "cronExpression",
"expression": "0 0 8 * * 1-5"
}
]
}
},
"id": "schedule-trigger",
"name": "Daily 8AM (Mon-Fri)",
"type": "n8n-nodes-base.scheduleTrigger",
"typeVersion": 1.2,
"position": [240, 300]
},
{
"parameters": {
"method": "POST",
"url": "http://memento-mcp:3001/mcp",
"sendHeaders": true,
"headerParameters": {
"parameters": [
{ "name": "Content-Type", "value": "application/json" },
{ "name": "x-api-key", "value": "={{ $vars.MEMENTO_API_KEY }}" }
]
},
"sendBody": true,
"specifyBody": "json",
"jsonBody": "{\n \"jsonrpc\": \"2.0\",\n \"id\": 1,\n \"method\": \"tools/call\",\n \"params\": {\n \"name\": \"get_notes\",\n \"arguments\": {\n \"limit\": 30,\n \"includeArchived\": false\n }\n }\n}"
},
"id": "mcp-get-notes",
"name": "MCP — Get Recent Notes",
"type": "n8n-nodes-base.httpRequest",
"typeVersion": 4.2,
"position": [480, 300]
},
{
"parameters": {
"method": "POST",
"url": "http://memento-mcp:3001/mcp",
"sendHeaders": true,
"headerParameters": {
"parameters": [
{ "name": "Content-Type", "value": "application/json" },
{ "name": "x-api-key", "value": "={{ $vars.MEMENTO_API_KEY }}" }
]
},
"sendBody": true,
"specifyBody": "json",
"jsonBody": "{\n \"jsonrpc\": \"2.0\",\n \"id\": 2,\n \"method\": \"tools/call\",\n \"params\": {\n \"name\": \"get_due_reminders\",\n \"arguments\": {}\n }\n}"
},
"id": "mcp-get-reminders",
"name": "MCP — Get Due Reminders",
"type": "n8n-nodes-base.httpRequest",
"typeVersion": 4.2,
"position": [480, 480]
},
{
"parameters": {
"jsCode": "const notesResponse = $('MCP — Get Recent Notes').item.json;\nconst remindersResponse = $('MCP — Get Due Reminders').item.json;\n\nconst notes = JSON.parse(notesResponse.result?.content?.[0]?.text || '[]');\nconst reminders = JSON.parse(remindersResponse.result?.content?.[0]?.text || '[]');\n\nconst today = new Date().toLocaleDateString('en-GB', { weekday: 'long', year: 'numeric', month: 'long', day: 'numeric' });\n\nconst pinnedNotes = notes.filter(n => n.isPinned).slice(0, 5);\nconst recentNotes = notes.filter(n => !n.isPinned).slice(0, 10);\n\nlet digest = `# 📋 Daily Digest — ${today}\\n\\n`;\n\nif (reminders.length > 0) {\n digest += `## ⏰ Reminders Due (${reminders.length})\\n\\n`;\n reminders.forEach(r => {\n const time = new Date(r.reminder).toLocaleTimeString('en-GB', { hour: '2-digit', minute: '2-digit' });\n digest += `- **${r.title || 'Untitled'}** @ ${time}\\n`;\n });\n digest += '\\n';\n}\n\nif (pinnedNotes.length > 0) {\n digest += `## 📌 Pinned Notes (${pinnedNotes.length})\\n\\n`;\n pinnedNotes.forEach(n => {\n digest += `- **${n.title || 'Untitled'}**\\n`;\n });\n digest += '\\n';\n}\n\ndigest += `## 📝 Recent Notes (${recentNotes.length})\\n\\n`;\nrecentNotes.forEach(n => {\n const date = new Date(n.updatedAt).toLocaleDateString('en-GB');\n digest += `- ${n.title || 'Untitled'} *(${date})*\\n`;\n});\n\nreturn [{ json: { digest, noteCount: notes.length, reminderCount: reminders.length, today } }];"
},
"id": "build-digest",
"name": "Build Digest",
"type": "n8n-nodes-base.code",
"typeVersion": 2,
"position": [720, 390]
},
{
"parameters": {
"method": "POST",
"url": "http://memento-mcp:3001/mcp",
"sendHeaders": true,
"headerParameters": {
"parameters": [
{ "name": "Content-Type", "value": "application/json" },
{ "name": "x-api-key", "value": "={{ $vars.MEMENTO_API_KEY }}" }
]
},
"sendBody": true,
"specifyBody": "json",
"jsonBody": "={\n \"jsonrpc\": \"2.0\",\n \"id\": 3,\n \"method\": \"tools/call\",\n \"params\": {\n \"name\": \"create_note\",\n \"arguments\": {\n \"title\": \"Daily Digest — {{ $json.today }}\",\n \"content\": {{ JSON.stringify($json.digest) }},\n \"isMarkdown\": true,\n \"color\": \"teal\",\n \"isPinned\": false,\n \"labels\": [\"digest\", \"auto\"]\n }\n }\n}"
},
"id": "mcp-save-digest",
"name": "MCP — Save Digest as Note",
"type": "n8n-nodes-base.httpRequest",
"typeVersion": 4.2,
"position": [960, 390]
},
{
"parameters": {
"channel": "general",
"text": "📋 *Daily Digest ready!*\n\n📝 {{ $('Build Digest').item.json.noteCount }} notes · ⏰ {{ $('Build Digest').item.json.reminderCount }} reminders\n\nView in Memento: http://memento:3000",
"additionalFields": { "parse_mode": "Markdown" }
},
"id": "slack-digest",
"name": "Slack — Share Digest",
"type": "n8n-nodes-base.slack",
"typeVersion": 2.1,
"position": [1200, 390],
"continueOnFail": true
}
],
"connections": {
"Daily 8AM (Mon-Fri)": {
"main": [
[
{ "node": "MCP — Get Recent Notes", "type": "main", "index": 0 },
{ "node": "MCP — Get Due Reminders", "type": "main", "index": 0 }
]
]
},
"MCP — Get Recent Notes": {
"main": [[{ "node": "Build Digest", "type": "main", "index": 0 }]]
},
"MCP — Get Due Reminders": {
"main": [[{ "node": "Build Digest", "type": "main", "index": 0 }]]
},
"Build Digest": {
"main": [[{ "node": "MCP — Save Digest as Note", "type": "main", "index": 0 }]]
},
"MCP — Save Digest as Note": {
"main": [[{ "node": "Slack — Share Digest", "type": "main", "index": 0 }]]
}
},
"pinData": {},
"settings": { "executionOrder": "v1" },
"staticData": null,
"tags": [{ "id": "memento-mcp", "name": "Memento MCP" }],
"triggerCount": 1,
"updatedAt": "2026-05-03T00:00:00.000Z",
"versionId": "1"
}

View File

@@ -0,0 +1,109 @@
{
"name": "Memento MCP — Email to Note",
"nodes": [
{
"parameters": {
"pollTimes": {
"item": [{ "mode": "everyMinute" }]
},
"filters": {
"hasReadStatus": true,
"readStatus": "unread"
}
},
"id": "email-trigger",
"name": "Email Trigger (IMAP)",
"type": "n8n-nodes-base.emailTrigger",
"typeVersion": 1.1,
"position": [240, 300]
},
{
"parameters": {
"jsCode": "const email = $input.item.json;\nconst subject = email.subject || 'Email (no subject)';\nconst from = email.from?.value?.[0]?.address || email.from || 'unknown';\nconst body = email.text || email.html?.replace(/<[^>]+>/g, '') || '';\nconst date = email.date ? new Date(email.date).toISOString() : new Date().toISOString();\n\nconst isUrgent = /(urgent|asap|important|deadline|critical)/i.test(subject + body);\n\nreturn [{\n json: {\n title: `📧 ${subject}`,\n content: `**From:** ${from}\\n**Date:** ${new Date(date).toLocaleString('en-GB')}\\n\\n---\\n\\n${body.trim().substring(0, 5000)}`,\n isMarkdown: true,\n color: isUrgent ? 'red' : 'blue',\n isPinned: isUrgent,\n labels: isUrgent ? ['email', 'urgent'] : ['email'],\n isUrgent\n }\n}];"
},
"id": "format-email",
"name": "Format Email as Note",
"type": "n8n-nodes-base.code",
"typeVersion": 2,
"position": [480, 300]
},
{
"parameters": {
"method": "POST",
"url": "http://memento-mcp:3001/mcp",
"sendHeaders": true,
"headerParameters": {
"parameters": [
{ "name": "Content-Type", "value": "application/json" },
{ "name": "x-api-key", "value": "={{ $vars.MEMENTO_API_KEY }}" }
]
},
"sendBody": true,
"specifyBody": "json",
"jsonBody": "={\n \"jsonrpc\": \"2.0\",\n \"id\": 1,\n \"method\": \"tools/call\",\n \"params\": {\n \"name\": \"create_note\",\n \"arguments\": {\n \"title\": \"{{ $json.title }}\",\n \"content\": \"{{ $json.content }}\",\n \"isMarkdown\": {{ $json.isMarkdown }},\n \"color\": \"{{ $json.color }}\",\n \"isPinned\": {{ $json.isPinned }},\n \"labels\": {{ JSON.stringify($json.labels) }}\n }\n }\n}"
},
"id": "mcp-create-note",
"name": "MCP — Create Note",
"type": "n8n-nodes-base.httpRequest",
"typeVersion": 4.2,
"position": [720, 300]
},
{
"parameters": {
"conditions": {
"conditions": [
{
"id": "urgent",
"leftValue": "={{ $('Format Email as Note').item.json.isUrgent }}",
"rightValue": true,
"operator": { "type": "boolean", "operation": "equals" }
}
],
"combinator": "and"
}
},
"id": "if-urgent",
"name": "Urgent?",
"type": "n8n-nodes-base.if",
"typeVersion": 2.1,
"position": [960, 300]
},
{
"parameters": {
"channel": "alerts",
"text": "🚨 *Urgent email saved to Memento!*\n\n{{ $('Format Email as Note').item.json.title }}\n\nOpen: http://memento:3000",
"additionalFields": { "parse_mode": "Markdown" }
},
"id": "slack-alert",
"name": "Slack Alert",
"type": "n8n-nodes-base.slack",
"typeVersion": 2.1,
"position": [1200, 200],
"continueOnFail": true
}
],
"connections": {
"Email Trigger (IMAP)": {
"main": [[{ "node": "Format Email as Note", "type": "main", "index": 0 }]]
},
"Format Email as Note": {
"main": [[{ "node": "MCP — Create Note", "type": "main", "index": 0 }]]
},
"MCP — Create Note": {
"main": [[{ "node": "Urgent?", "type": "main", "index": 0 }]]
},
"Urgent?": {
"main": [
[{ "node": "Slack Alert", "type": "main", "index": 0 }],
[]
]
}
},
"pinData": {},
"settings": { "executionOrder": "v1" },
"staticData": null,
"tags": [{ "id": "memento-mcp", "name": "Memento MCP" }],
"triggerCount": 1,
"updatedAt": "2026-05-03T00:00:00.000Z",
"versionId": "1"
}

View File

@@ -0,0 +1,155 @@
{
"name": "Memento MCP — Reminder Bot",
"nodes": [
{
"parameters": {
"rule": {
"interval": [
{
"field": "cronExpression",
"expression": "0 */30 * * * *"
}
]
}
},
"id": "schedule-trigger",
"name": "Every 30 Minutes",
"type": "n8n-nodes-base.scheduleTrigger",
"typeVersion": 1.2,
"position": [240, 300]
},
{
"parameters": {
"method": "POST",
"url": "http://memento-mcp:3001/mcp",
"authentication": "genericCredentialType",
"genericAuthType": "httpHeaderAuth",
"sendHeaders": true,
"headerParameters": {
"parameters": [
{
"name": "Content-Type",
"value": "application/json"
},
{
"name": "x-api-key",
"value": "={{ $vars.MEMENTO_API_KEY }}"
}
]
},
"sendBody": true,
"specifyBody": "json",
"jsonBody": "{\n \"jsonrpc\": \"2.0\",\n \"id\": 1,\n \"method\": \"tools/call\",\n \"params\": {\n \"name\": \"get_due_reminders\",\n \"arguments\": {}\n }\n}"
},
"id": "mcp-get-reminders",
"name": "MCP — Get Due Reminders",
"type": "n8n-nodes-base.httpRequest",
"typeVersion": 4.2,
"position": [480, 300]
},
{
"parameters": {
"jsCode": "// Parse MCP response\nconst response = $input.item.json;\nconst result = JSON.parse(response.result?.content?.[0]?.text || '[]');\n\nif (!Array.isArray(result) || result.length === 0) {\n return [];\n}\n\nreturn result.map(note => ({\n json: {\n noteId: note.id,\n title: note.title || 'Untitled reminder',\n reminder: note.reminder,\n reminderFormatted: new Date(note.reminder).toLocaleString('en-GB'),\n content: (note.content || '').substring(0, 200),\n labels: Array.isArray(note.labels) ? note.labels.join(', ') : ''\n }\n}));"
},
"id": "parse-reminders",
"name": "Parse Reminders",
"type": "n8n-nodes-base.code",
"typeVersion": 2,
"position": [720, 300]
},
{
"parameters": {
"conditions": {
"options": { "caseSensitive": true },
"conditions": [
{
"id": "has-items",
"leftValue": "={{ $items().length }}",
"rightValue": 0,
"operator": {
"type": "number",
"operation": "gt"
}
}
],
"combinator": "and"
}
},
"id": "check-has-reminders",
"name": "Has Reminders?",
"type": "n8n-nodes-base.if",
"typeVersion": 2.1,
"position": [960, 300]
},
{
"parameters": {
"chatId": "={{ $vars.TELEGRAM_CHAT_ID }}",
"text": "⏰ *Reminder — Memento*\n\n📌 *{{ $json.title }}*\n🕐 {{ $json.reminderFormatted }}\n\n{{ $json.content }}{{ $json.labels ? '\n🏷 ' + $json.labels : '' }}",
"additionalFields": {
"parse_mode": "Markdown"
}
},
"id": "send-telegram",
"name": "Send Telegram Notification",
"type": "n8n-nodes-base.telegram",
"typeVersion": 1.2,
"position": [1200, 200],
"continueOnFail": true
},
{
"parameters": {
"method": "POST",
"url": "http://memento-mcp:3001/mcp",
"sendHeaders": true,
"headerParameters": {
"parameters": [
{
"name": "Content-Type",
"value": "application/json"
},
{
"name": "x-api-key",
"value": "={{ $vars.MEMENTO_API_KEY }}"
}
]
},
"sendBody": true,
"specifyBody": "json",
"jsonBody": "={\n \"jsonrpc\": \"2.0\",\n \"id\": 2,\n \"method\": \"tools/call\",\n \"params\": {\n \"name\": \"update_note\",\n \"arguments\": {\n \"id\": \"{{ $json.noteId }}\",\n \"isReminderDone\": true\n }\n }\n}"
},
"id": "mcp-mark-done",
"name": "MCP — Mark Reminder Done",
"type": "n8n-nodes-base.httpRequest",
"typeVersion": 4.2,
"position": [1440, 200],
"continueOnFail": true
}
],
"connections": {
"Every 30 Minutes": {
"main": [[{ "node": "MCP — Get Due Reminders", "type": "main", "index": 0 }]]
},
"MCP — Get Due Reminders": {
"main": [[{ "node": "Parse Reminders", "type": "main", "index": 0 }]]
},
"Parse Reminders": {
"main": [[{ "node": "Has Reminders?", "type": "main", "index": 0 }]]
},
"Has Reminders?": {
"main": [
[{ "node": "Send Telegram Notification", "type": "main", "index": 0 }],
[]
]
},
"Send Telegram Notification": {
"main": [[{ "node": "MCP — Mark Reminder Done", "type": "main", "index": 0 }]]
}
},
"pinData": {},
"settings": { "executionOrder": "v1" },
"staticData": null,
"tags": [{ "id": "memento-mcp", "name": "Memento MCP" }],
"triggerCount": 1,
"updatedAt": "2026-05-03T00:00:00.000Z",
"versionId": "1"
}

View File

@@ -0,0 +1,92 @@
{
"name": "Memento MCP — Webhook to Note",
"nodes": [
{
"parameters": {
"httpMethod": "POST",
"path": "memento-note",
"responseMode": "responseNode",
"options": {}
},
"id": "webhook-trigger",
"name": "Webhook",
"type": "n8n-nodes-base.webhook",
"typeVersion": 2,
"position": [240, 300],
"webhookId": "memento-create-note"
},
{
"parameters": {
"jsCode": "// Flexible input: accept anything and build a note from it\nconst body = $input.item.json.body || $input.item.json;\n\nconst title = body.title || body.subject || body.name || null;\nconst content = body.content || body.text || body.message || body.description || JSON.stringify(body, null, 2);\nconst color = body.color || 'default';\nconst labels = Array.isArray(body.labels) ? body.labels : body.labels ? [body.labels] : [];\nconst notebookId = body.notebookId || body.notebook_id || null;\nconst isPinned = body.isPinned === true || body.pinned === true;\nconst isMarkdown = body.isMarkdown === true || body.markdown === true;\nconst reminder = body.reminder || body.dueDate || body.due_date || null;\nconst color_valid = ['default','red','orange','yellow','green','teal','blue','purple','pink','gray'].includes(color) ? color : 'default';\n\nreturn [{ json: { title, content, color: color_valid, labels, notebookId, isPinned, isMarkdown, reminder } }];"
},
"id": "prepare-note",
"name": "Prepare Note Data",
"type": "n8n-nodes-base.code",
"typeVersion": 2,
"position": [480, 300]
},
{
"parameters": {
"method": "POST",
"url": "http://memento-mcp:3001/mcp",
"sendHeaders": true,
"headerParameters": {
"parameters": [
{ "name": "Content-Type", "value": "application/json" },
{ "name": "x-api-key", "value": "={{ $vars.MEMENTO_API_KEY }}" }
]
},
"sendBody": true,
"specifyBody": "json",
"jsonBody": "={\n \"jsonrpc\": \"2.0\",\n \"id\": 1,\n \"method\": \"tools/call\",\n \"params\": {\n \"name\": \"create_note\",\n \"arguments\": {\n \"title\": {{ $json.title ? JSON.stringify($json.title) : 'null' }},\n \"content\": {{ JSON.stringify($json.content) }},\n \"color\": \"{{ $json.color }}\",\n \"labels\": {{ JSON.stringify($json.labels) }},\n \"isPinned\": {{ $json.isPinned }},\n \"isMarkdown\": {{ $json.isMarkdown }}\n {{ $json.notebookId ? ', \"notebookId\": \"' + $json.notebookId + '\"' : '' }}\n {{ $json.reminder ? ', \"reminder\": \"' + $json.reminder + '\"' : '' }}\n }\n }\n}"
},
"id": "mcp-create-note",
"name": "MCP — Create Note",
"type": "n8n-nodes-base.httpRequest",
"typeVersion": 4.2,
"position": [720, 300]
},
{
"parameters": {
"jsCode": "const response = $input.item.json;\nconst result = JSON.parse(response.result?.content?.[0]?.text || '{}');\n\nreturn [{\n json: {\n success: true,\n noteId: result.id,\n title: result.title,\n createdAt: result.createdAt\n }\n}];"
},
"id": "format-response",
"name": "Format Response",
"type": "n8n-nodes-base.code",
"typeVersion": 2,
"position": [960, 300]
},
{
"parameters": {
"respondWith": "json",
"responseBody": "={{ $json }}"
},
"id": "webhook-response",
"name": "Respond to Webhook",
"type": "n8n-nodes-base.respondToWebhook",
"typeVersion": 1.1,
"position": [1200, 300]
}
],
"connections": {
"Webhook": {
"main": [[{ "node": "Prepare Note Data", "type": "main", "index": 0 }]]
},
"Prepare Note Data": {
"main": [[{ "node": "MCP — Create Note", "type": "main", "index": 0 }]]
},
"MCP — Create Note": {
"main": [[{ "node": "Format Response", "type": "main", "index": 0 }]]
},
"Format Response": {
"main": [[{ "node": "Respond to Webhook", "type": "main", "index": 0 }]]
}
},
"pinData": {},
"settings": { "executionOrder": "v1" },
"staticData": null,
"tags": [{ "id": "memento-mcp", "name": "Memento MCP" }],
"triggerCount": 1,
"updatedAt": "2026-05-03T00:00:00.000Z",
"versionId": "1"
}

View File

@@ -78,10 +78,15 @@ const toolDefinitions = [
title: { type: 'string', description: 'Note title' }, title: { type: 'string', description: 'Note title' },
content: { type: 'string', description: 'Note body text (required)' }, content: { type: 'string', description: 'Note body text (required)' },
color: { type: 'string', description: `Color: ${NOTE_COLORS}`, default: 'default' }, color: { type: 'string', description: `Color: ${NOTE_COLORS}`, default: 'default' },
type: { type: 'string', enum: ['text', 'checklist'], description: 'Note type', default: 'text' }, type: {
type: 'string',
enum: ['text', 'markdown', 'richtext', 'checklist'],
description: 'Note type. "text" = plain text, "markdown" = Markdown rendered, "richtext" = rich text editor (default), "checklist" = interactive checklist',
default: 'richtext',
},
checkItems: { checkItems: {
type: 'array', type: 'array',
description: 'Checklist items (when type=list)', description: 'Checklist items (when type=checklist)',
items: { items: {
type: 'object', type: 'object',
properties: { id: { type: 'string' }, text: { type: 'string' }, checked: { type: 'boolean' } }, properties: { id: { type: 'string' }, text: { type: 'string' }, checked: { type: 'boolean' } },
@@ -97,7 +102,7 @@ const toolDefinitions = [
isReminderDone: { type: 'boolean', default: false }, isReminderDone: { type: 'boolean', default: false },
reminderRecurrence: { type: 'string', description: 'daily, weekly, monthly, yearly' }, reminderRecurrence: { type: 'string', description: 'daily, weekly, monthly, yearly' },
reminderLocation: { type: 'string', description: 'Location string' }, reminderLocation: { type: 'string', description: 'Location string' },
isMarkdown: { type: 'boolean', description: 'Render as markdown', default: false }, isMarkdown: { type: 'boolean', description: '(Deprecated — use type="markdown" instead) Render as markdown', default: false },
size: { type: 'string', enum: ['small', 'medium', 'large'], default: 'small' }, size: { type: 'string', enum: ['small', 'medium', 'large'], default: 'small' },
notebookId: { type: 'string', description: 'Assign to notebook' }, notebookId: { type: 'string', description: 'Assign to notebook' },
}, },
@@ -137,7 +142,11 @@ const toolDefinitions = [
title: { type: 'string' }, title: { type: 'string' },
content: { type: 'string' }, content: { type: 'string' },
color: { type: 'string', description: `One of: ${NOTE_COLORS}` }, color: { type: 'string', description: `One of: ${NOTE_COLORS}` },
type: { type: 'string', enum: ['text', 'checklist'] }, type: {
type: 'string',
enum: ['text', 'markdown', 'richtext', 'checklist'],
description: 'Note type. "text" = plain text, "markdown" = Markdown rendered, "richtext" = rich text editor, "checklist" = interactive checklist',
},
checkItems: { checkItems: {
type: 'array', type: 'array',
items: { items: {
@@ -154,7 +163,7 @@ const toolDefinitions = [
isReminderDone: { type: 'boolean' }, isReminderDone: { type: 'boolean' },
reminderRecurrence: { type: 'string' }, reminderRecurrence: { type: 'string' },
reminderLocation: { type: 'string' }, reminderLocation: { type: 'string' },
isMarkdown: { type: 'boolean' }, isMarkdown: { type: 'boolean', description: '(Deprecated — use type="markdown" instead)' },
size: { type: 'string', enum: ['small', 'medium', 'large'] }, size: { type: 'string', enum: ['small', 'medium', 'large'] },
notebookId: { type: 'string' }, notebookId: { type: 'string' },
}, },

View File

@@ -5,6 +5,7 @@ import Link from 'next/link'
import { usePathname, useRouter, useSearchParams } from 'next/navigation' import { usePathname, useRouter, useSearchParams } from 'next/navigation'
import { cn } from '@/lib/utils' import { cn } from '@/lib/utils'
import { StickyNote, Plus, Tag, Folder, ChevronDown, ChevronRight } from 'lucide-react' import { StickyNote, Plus, Tag, Folder, ChevronDown, ChevronRight } from 'lucide-react'
import { Tooltip, TooltipContent, TooltipProvider, TooltipTrigger } from '@/components/ui/tooltip'
import { useNotebooks } from '@/context/notebooks-context' import { useNotebooks } from '@/context/notebooks-context'
import { useNotebookDrag } from '@/context/notebook-drag-context' import { useNotebookDrag } from '@/context/notebook-drag-context'
import { Button } from '@/components/ui/button' import { Button } from '@/components/ui/button'
@@ -245,23 +246,30 @@ export function NotebooksList() {
isDragOver && "ring-2 ring-blue-500 ring-dashed rounded-e-full me-2" isDragOver && "ring-2 ring-blue-500 ring-dashed rounded-e-full me-2"
)} )}
> >
<button <TooltipProvider delayDuration={600}>
onClick={() => handleSelectNotebook(notebook.id)} <Tooltip>
className={cn( <TooltipTrigger asChild>
"pointer-events-auto flex items-center gap-4 px-6 py-3 rounded-e-full me-2 text-gray-600 dark:text-gray-400 hover:bg-gray-100 dark:hover:bg-gray-800/50 transition-colors w-full pe-24", <button
isDragOver && "opacity-50" onClick={() => handleSelectNotebook(notebook.id)}
)} className={cn(
> "pointer-events-auto flex items-center gap-4 px-6 py-3 rounded-e-full me-2 text-gray-600 dark:text-gray-400 hover:bg-gray-100 dark:hover:bg-gray-800/50 transition-colors w-full pe-14",
<NotebookIcon className="w-5 h-5 flex-shrink-0" /> isDragOver && "opacity-50"
<NotebookName name={notebook.name}> )}
<span className="text-[15px] font-medium tracking-wide text-start"> >
<NotebookIcon className="w-5 h-5 flex-shrink-0" />
<span className="text-[15px] font-medium tracking-wide text-start truncate min-w-0 flex-1">
{notebook.name}
</span>
{(notebook as any).notesCount > 0 && (
<span className="text-xs text-gray-400 ms-auto flex-shrink-0">({new Intl.NumberFormat(language).format((notebook as any).notesCount)})</span>
)}
</button>
</TooltipTrigger>
<TooltipContent side="right" className="text-sm font-medium">
{notebook.name} {notebook.name}
</span> </TooltipContent>
</NotebookName> </Tooltip>
{(notebook as any).notesCount > 0 && ( </TooltipProvider>
<span className="text-xs text-gray-400 ms-2 flex-shrink-0">({new Intl.NumberFormat(language).format((notebook as any).notesCount)})</span>
)}
</button>
{/* Actions + expand on the right — always rendered, visible on hover */} {/* Actions + expand on the right — always rendered, visible on hover */}
<div className="absolute end-3 top-1/2 -translate-y-1/2 flex items-center gap-1 opacity-0 group-hover:opacity-100 transition-opacity z-10"> <div className="absolute end-3 top-1/2 -translate-y-1/2 flex items-center gap-1 opacity-0 group-hover:opacity-100 transition-opacity z-10">