more tools, append endpoint, doc updates
This commit is contained in:
parent
50b7d08bb2
commit
eab16aafff
|
|
@ -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):
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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:
|
||||
|
|
|
|||
|
|
@ -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
|
||||
# =========================================================================
|
||||
|
|
|
|||
|
|
@ -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()
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
Loading…
Reference in New Issue