more tools, append endpoint, doc updates

This commit is contained in:
Gamosoft 2026-03-13 18:33:27 +01:00
parent 50b7d08bb2
commit eab16aafff
6 changed files with 436 additions and 0 deletions

View File

@ -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):

View File

@ -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

View File

@ -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:

View File

@ -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
# =========================================================================

View File

@ -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()

View File

@ -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