diff --git a/backend/main.py b/backend/main.py index 339547f..39d853c 100644 --- a/backend/main.py +++ b/backend/main.py @@ -1078,6 +1078,62 @@ async def create_or_update_note(request: Request, note_path: str, content: dict) raise HTTPException(status_code=500, detail=safe_error_message(e, "Failed to save note")) +@api_router.patch("/notes/{note_path:path}", tags=["Notes"]) +@limiter.limit("60/minute") +async def append_to_note(request: Request, note_path: str, data: dict): + """ + Append content to an existing note without overwriting. + + Perfect for journals, logs, or collecting ideas incrementally. + + Args: + note_path: Path to the note + data: Dictionary with 'content' to append and optional 'add_timestamp' boolean + """ + try: + content_to_append = data.get('content', '') + add_timestamp = data.get('add_timestamp', False) + + if not content_to_append: + raise HTTPException(status_code=400, detail="Content to append is required") + + # Get existing content + existing_content = get_note_content(config['storage']['notes_dir'], note_path) + + if existing_content is None: + raise HTTPException(status_code=404, detail="Note not found") + + # Build the appended content + if add_timestamp: + from datetime import datetime + timestamp = datetime.now().strftime("%Y-%m-%d %H:%M") + content_to_append = f"\n\n---\n\n**{timestamp}**\n\n{content_to_append}" + else: + content_to_append = f"\n\n{content_to_append}" + + new_content = existing_content + content_to_append + + # Run on_note_save hook + transformed_content = plugin_manager.run_hook('on_note_save', note_path=note_path, content=new_content) + if transformed_content is None: + transformed_content = new_content + + success = save_note(config['storage']['notes_dir'], note_path, transformed_content) + + if not success: + raise HTTPException(status_code=500, detail="Failed to append to note") + + return { + "success": True, + "path": note_path, + "message": "Content appended successfully" + } + except HTTPException: + raise + except Exception as e: + raise HTTPException(status_code=500, detail=safe_error_message(e, "Failed to append to note")) + + @api_router.delete("/notes/{note_path:path}", tags=["Notes"]) @limiter.limit("30/minute") async def remove_note(request: Request, note_path: str): diff --git a/documentation/API.md b/documentation/API.md index 27c32dd..259253a 100644 --- a/documentation/API.md +++ b/documentation/API.md @@ -70,6 +70,56 @@ DELETE /api/notes/{note_path} curl -X DELETE http://localhost:8000/api/notes/test.md ``` +### Append to Note +```http +PATCH /api/notes/{note_path} +Content-Type: application/json + +{ + "content": "Content to append...", + "add_timestamp": true +} +``` + +Append content to an existing note without overwriting. Perfect for journals, logs, or collecting ideas incrementally. + +**Parameters:** +| Parameter | Type | Required | Description | +|-----------|------|----------|-------------| +| `content` | string | Yes | Content to append to the note | +| `add_timestamp` | boolean | No | If `true`, prepends a timestamp header (default: `false`) | + +**Response:** +```json +{ + "success": true, + "path": "daily-journal.md", + "message": "Content appended successfully" +} +``` + +**Example with timestamp:** +```bash +curl -X PATCH http://localhost:8000/api/notes/daily-journal.md \ + -H "Content-Type: application/json" \ + -d '{"content": "Had a productive meeting about the roadmap.", "add_timestamp": true}' +``` + +This will append: +```markdown + +--- + +**2024-03-13 14:30** + +Had a productive meeting about the roadmap. +``` + +**Windows PowerShell:** +```powershell +curl.exe -X PATCH http://localhost:8000/api/notes/daily-journal.md -H "Content-Type: application/json" -d "{\"content\": \"New entry here\", \"add_timestamp\": true}" +``` + ### Move Note ```http POST /api/notes/move diff --git a/documentation/MCP.md b/documentation/MCP.md index da0df89..5d07fb6 100644 --- a/documentation/MCP.md +++ b/documentation/MCP.md @@ -10,6 +10,9 @@ MCP (Model Context Protocol) is an open standard that allows AI assistants to se - 📖 **Read** note contents - 🏷️ **Browse** by tags - 📝 **Create** new notes +- ✏️ **Append** to existing notes (journals, logs) +- 📂 **Organize** notes (move, rename, folders) +- 📋 **Use templates** to create structured notes - 🔗 **Explore** the knowledge graph ## Quick Setup @@ -114,6 +117,7 @@ The MCP server provides these tools to AI assistants: | `search_notes` | Full-text search across all notes | | `list_notes` | List all notes with metadata | | `get_note` | Read a specific note's content | +| `get_recent_notes` | Get recently modified notes (last N days) | ### Organization @@ -128,6 +132,8 @@ The MCP server provides these tools to AI assistants: | Tool | Description | |------|-------------| | `create_note` | Create or update a note | +| `append_to_note` | Append content to an existing note (great for journals/logs) | +| `move_note` | Move or rename a note | | `delete_note` | Delete a note | | `create_folder` | Create a new folder | @@ -137,6 +143,7 @@ The MCP server provides these tools to AI assistants: |------|-------------| | `list_templates` | List available templates | | `get_template` | Get template content | +| `create_note_from_template` | Create a note from a template with variable substitution | ### System @@ -144,6 +151,62 @@ The MCP server provides these tools to AI assistants: |------|-------------| | `health_check` | Verify server connectivity | +## Tool Details + +### `append_to_note` + +Append content to an existing note without overwriting. Perfect for journals, logs, or collecting ideas. + +| Parameter | Type | Required | Description | +|-----------|------|----------|-------------| +| `path` | string | Yes | Path to existing note | +| `content` | string | Yes | Content to append | +| `add_timestamp` | boolean | No | Add timestamp header before content | + +**Example prompt:** "Add this meeting summary to my daily-journal.md with a timestamp" + +--- + +### `move_note` + +Move or rename a note to a different location. + +| Parameter | Type | Required | Description | +|-----------|------|----------|-------------| +| `old_path` | string | Yes | Current note path | +| `new_path` | string | Yes | New path (can include folder) | + +**Example prompt:** "Move draft.md to published/final-article.md" + +--- + +### `get_recent_notes` + +Get recently modified notes. Useful for context about what you've been working on. + +| Parameter | Type | Required | Default | Description | +|-----------|------|----------|---------|-------------| +| `days` | integer | No | 7 | Notes modified in last N days | +| `limit` | integer | No | 10 | Max notes to return | + +**Example prompt:** "What was I working on this week?" + +--- + +### `create_note_from_template` + +Create a new note from a template with variable substitution. + +| Parameter | Type | Required | Description | +|-----------|------|----------|-------------| +| `template_name` | string | Yes | Template name (e.g., "meeting-notes") | +| `note_path` | string | Yes | Path for the new note | +| `variables` | object | No | Variables to substitute (e.g., `{"project": "Alpha"}`) | + +**Built-in placeholders:** `{{date}}`, `{{time}}`, `{{datetime}}`, `{{title}}`, `{{folder}}` + +**Example prompt:** "Create a new meeting note for Project Alpha using the meeting-notes template" + ## Usage Examples Once configured, you can interact with your notes naturally: @@ -166,6 +229,24 @@ Once configured, you can interact with your notes naturally: > > "You have 7 notes with the #project tag..." +> **User:** "Add this to my daily journal with a timestamp" +> +> **AI:** *Uses `append_to_note` with `add_timestamp: true`* +> +> "Done! I've appended your entry to 'daily-journal.md' with today's timestamp." + +> **User:** "What was I working on last week?" +> +> **AI:** *Uses `get_recent_notes` with `days: 7`* +> +> "You modified 5 notes in the last week: project-roadmap.md, meeting-notes.md..." + +> **User:** "Create a meeting note for the design review using my template" +> +> **AI:** *Uses `create_note_from_template` with the meeting-notes template* +> +> "Created 'meetings/design-review-2024-03-13.md' from your meeting-notes template." + ## Authentication If you have authentication enabled in NoteDiscovery: diff --git a/mcp_server/client.py b/mcp_server/client.py index e1ce7c1..01184bc 100644 --- a/mcp_server/client.py +++ b/mcp_server/client.py @@ -189,6 +189,68 @@ class NoteDiscoveryClient: encoded_path = urllib.parse.quote(path, safe="") return self._request("DELETE", f"/api/notes/{encoded_path}") + def append_to_note(self, path: str, content: str, add_timestamp: bool = False) -> APIResponse: + """ + Append content to an existing note. + + Args: + path: Note path + content: Content to append + add_timestamp: Whether to add a timestamp header + + Returns: + APIResponse with append result + """ + encoded_path = urllib.parse.quote(path, safe="") + return self._request( + "PATCH", + f"/api/notes/{encoded_path}", + data={"content": content, "add_timestamp": add_timestamp} + ) + + def move_note(self, old_path: str, new_path: str) -> APIResponse: + """ + Move or rename a note. + + Args: + old_path: Current note path + new_path: New note path + + Returns: + APIResponse with move result + """ + return self._request( + "POST", + "/api/notes/move", + data={"oldPath": old_path, "newPath": new_path} + ) + + def create_note_from_template( + self, + template_name: str, + note_path: str, + variables: dict | None = None + ) -> APIResponse: + """ + Create a note from a template. + + Args: + template_name: Name of the template + note_path: Path for the new note + variables: Variables to substitute in the template + + Returns: + APIResponse with creation result + """ + data = { + "template": template_name, + "path": note_path, + } + if variables: + data["variables"] = variables + + return self._request("POST", "/api/templates/create-note", data=data) + # ========================================================================= # Search API # ========================================================================= diff --git a/mcp_server/server.py b/mcp_server/server.py index 55f1ed1..77ae5f6 100644 --- a/mcp_server/server.py +++ b/mcp_server/server.py @@ -450,6 +450,113 @@ class MCPServer: return f"Template: {name}\n---\n{content}" + def _tool_append_to_note(self, args: dict) -> str: + """Append content to an existing note.""" + path = args.get("path", "") + content = args.get("content", "") + add_timestamp = args.get("add_timestamp", False) + + is_valid, error = self._validate_path(path) + if not is_valid: + return f"Error: {error}" + if not content: + return "Error: content is required" + + response = self.client.append_to_note(path, content, add_timestamp) + + if not response.success: + return f"Failed to append to note: {response.error}" + + return f"✅ Content appended to: {path}" + + def _tool_move_note(self, args: dict) -> str: + """Move or rename a note.""" + old_path = args.get("old_path", "") + new_path = args.get("new_path", "") + + is_valid, error = self._validate_path(old_path) + if not is_valid: + return f"Error: old_path - {error}" + + is_valid, error = self._validate_path(new_path) + if not is_valid: + return f"Error: new_path - {error}" + + response = self.client.move_note(old_path, new_path) + + if not response.success: + return f"Failed to move note: {response.error}" + + return f"✅ Note moved: {old_path} → {new_path}" + + def _tool_get_recent_notes(self, args: dict) -> str: + """Get recently modified notes.""" + days = args.get("days", 7) + limit = args.get("limit", 10) + + # Get all notes + response = self.client.list_notes() + + if not response.success: + return f"Failed to get notes: {response.error}" + + data = response.data or {} + notes = data.get("notes", []) + + if not notes: + return "No notes found." + + # Filter by date + from datetime import datetime, timedelta + cutoff = datetime.now() - timedelta(days=days) + + recent_notes = [] + for note in notes: + modified_str = note.get("modified", "") + if modified_str: + try: + # Parse ISO format datetime + modified = datetime.fromisoformat(modified_str.replace("Z", "+00:00")) + if modified.replace(tzinfo=None) >= cutoff: + recent_notes.append(note) + except (ValueError, TypeError): + continue + + # Sort by modified date (most recent first) and limit + recent_notes.sort(key=lambda x: x.get("modified", ""), reverse=True) + recent_notes = recent_notes[:limit] + + if not recent_notes: + return f"No notes modified in the last {days} day(s)." + + output = [f"📅 Notes modified in the last {days} day(s) (showing {len(recent_notes)}):\n"] + for note in recent_notes: + path = note.get("path", "unknown") + modified = note.get("modified", "")[:10] # Just the date part + output.append(f" 📝 {path} (modified: {modified})") + + return "\n".join(output) + + def _tool_create_note_from_template(self, args: dict) -> str: + """Create a note from a template.""" + template_name = args.get("template_name", "") + note_path = args.get("note_path", "") + variables = args.get("variables", {}) + + if not template_name: + return "Error: template_name is required" + + is_valid, error = self._validate_path(note_path) + if not is_valid: + return f"Error: note_path - {error}" + + response = self.client.create_note_from_template(template_name, note_path, variables) + + if not response.success: + return f"Failed to create note from template: {response.error}" + + return f"✅ Note created from template '{template_name}': {note_path}" + def _tool_health_check(self, args: dict) -> str: """Check server health.""" response = self.client.health_check() diff --git a/mcp_server/tools.py b/mcp_server/tools.py index dd64118..60974bb 100644 --- a/mcp_server/tools.py +++ b/mcp_server/tools.py @@ -139,6 +139,86 @@ TOOLS: list[dict[str, Any]] = [ "required": ["path"] } }, + { + "name": "append_to_note", + "description": "Append content to an existing note without overwriting. Perfect for journals, logs, meeting notes, or collecting ideas incrementally.", + "inputSchema": { + "type": "object", + "properties": { + "path": { + "type": "string", + "description": "Path to the existing note" + }, + "content": { + "type": "string", + "description": "Content to append to the note" + }, + "add_timestamp": { + "type": "boolean", + "description": "Whether to add a timestamp header before the appended content (default: false)" + } + }, + "required": ["path", "content"] + } + }, + { + "name": "move_note", + "description": "Move or rename a note to a different path. Use this to reorganize notes or rename them.", + "inputSchema": { + "type": "object", + "properties": { + "old_path": { + "type": "string", + "description": "Current path of the note" + }, + "new_path": { + "type": "string", + "description": "New path for the note (can be in a different folder)" + } + }, + "required": ["old_path", "new_path"] + } + }, + { + "name": "get_recent_notes", + "description": "Get recently modified notes. Useful for finding what you were working on recently.", + "inputSchema": { + "type": "object", + "properties": { + "days": { + "type": "integer", + "description": "Get notes modified in the last N days (default: 7)" + }, + "limit": { + "type": "integer", + "description": "Maximum number of notes to return (default: 10)" + } + }, + "required": [] + } + }, + { + "name": "create_note_from_template", + "description": "Create a new note from a template with variable substitution. Variables in the template like {{variable_name}} will be replaced.", + "inputSchema": { + "type": "object", + "properties": { + "template_name": { + "type": "string", + "description": "Name of the template to use (e.g., 'meeting-notes', 'daily-journal')" + }, + "note_path": { + "type": "string", + "description": "Path for the new note (e.g., 'meetings/2024-03-13.md')" + }, + "variables": { + "type": "object", + "description": "Variables to substitute in the template (e.g., {\"project\": \"Alpha\", \"date\": \"2024-03-13\"})" + } + }, + "required": ["template_name", "note_path"] + } + }, # ========================================================================= # Templates