Files
basicmachines-co-basic-memory/src/basic_memory/mcp/tools/delete_note.py
T
2026-02-17 22:48:06 -06:00

326 lines
13 KiB
Python

from textwrap import dedent
from typing import Optional
from loguru import logger
from fastmcp import Context
from mcp.server.fastmcp.exceptions import ToolError
from basic_memory.mcp.project_context import get_project_client
from basic_memory.mcp.server import mcp
def _format_delete_error_response(project: str, error_message: str, identifier: str) -> str:
"""Format helpful error responses for delete failures that guide users to successful deletions."""
# Note not found errors
if "entity not found" in error_message.lower() or "not found" in error_message.lower():
search_term = identifier.split("/")[-1] if "/" in identifier else identifier
title_format = (
identifier.split("/")[-1].replace("-", " ").title() if "/" in identifier else identifier
)
permalink_format = identifier.lower().replace(" ", "-")
return dedent(f"""
# Delete Failed - Note Not Found
The note '{identifier}' could not be found for deletion in {project}.
## This might mean:
1. **Already deleted**: The note may have been deleted previously
2. **Wrong identifier**: The identifier format might be incorrect
3. **Different project**: The note might be in a different project
## How to verify:
1. **Search for the note**: Use `search_notes("{project}", "{search_term}")` to find it
2. **Try different formats**:
- If you used a permalink like "folder/note-title", try just the title: "{title_format}"
- If you used a title, try the permalink format: "{permalink_format}"
3. **Check if already deleted**: Use `list_directory("/")` to see what notes exist
4. **List notes in project**: Use `list_directory("/")` to see what notes exist in the current project
## If the note actually exists:
```
# First, find the correct identifier:
search_notes("{project}", "{identifier}")
# Then delete using the correct identifier:
delete_note("{project}", "correct-identifier-from-search")
```
## If you want to delete multiple similar notes:
Use search to find all related notes and delete them one by one.
""").strip()
# Permission/access errors
if (
"permission" in error_message.lower()
or "access" in error_message.lower()
or "forbidden" in error_message.lower()
):
return f"""# Delete Failed - Permission Error
You don't have permission to delete '{identifier}': {error_message}
## How to resolve:
1. **Check permissions**: Verify you have delete/write access to this project
2. **File locks**: The note might be open in another application
3. **Project access**: Ensure you're in the correct project with proper permissions
## Alternative actions:
- List available projects: `list_memory_projects()`
- Specify the correct project: `delete_note("{identifier}", project="project-name")`
- Verify note exists first: `read_note("{identifier}", project="project-name")`
## If you have read-only access:
Ask someone with write access to delete the note."""
# Server/filesystem errors
if (
"server error" in error_message.lower()
or "filesystem" in error_message.lower()
or "disk" in error_message.lower()
):
return f"""# Delete Failed - System Error
A system error occurred while deleting '{identifier}': {error_message}
## Immediate steps:
1. **Try again**: The error might be temporary
2. **Check file status**: Verify the file isn't locked or in use
3. **Check disk space**: Ensure the system has adequate storage
## Troubleshooting:
- Verify note exists: `read_note("{project}","{identifier}")`
- Try again in a few moments
## If problem persists:
Send a message to support@basicmachines.co - there may be a filesystem or database issue."""
# Database/sync errors
if "database" in error_message.lower() or "sync" in error_message.lower():
return f"""# Delete Failed - Database Error
A database error occurred while deleting '{identifier}': {error_message}
## This usually means:
1. **Sync conflict**: The file system and database are out of sync
2. **Database lock**: Another operation is accessing the database
3. **Corrupted entry**: The database entry might be corrupted
## Steps to resolve:
1. **Try again**: Wait a moment and retry the deletion
2. **Check note status**: `read_note("{project}","{identifier}")` to see current state
3. **Manual verification**: Use `list_directory()` to see if file still exists
## If the note appears gone but database shows it exists:
Send a message to support@basicmachines.co - a manual database cleanup may be needed."""
# Generic fallback
return f"""# Delete Failed
Error deleting note '{identifier}': {error_message}
## General troubleshooting:
1. **Verify the note exists**: `read_note("{project}", "{identifier}")` or `search_notes("{project}", "{identifier}")`
2. **Check permissions**: Ensure you can edit/delete files in this project
3. **Try again**: The error might be temporary
4. **Check project**: Make sure you're in the correct project
## Step-by-step approach:
```
# 1. Confirm note exists and get correct identifier
search_notes("{project}", "{identifier}")
# 2. Read the note to verify access
read_note("{project}", "correct-identifier-from-search")
# 3. Try deletion with correct identifier
delete_note("{project}", "correct-identifier-from-search")
```
## Alternative approaches:
- Check what notes exist: `list_directory("{project}", "/")`
## Need help?
If the note should be deleted but the operation keeps failing, send a message to support@basicmemory.com."""
@mcp.tool(description="Delete a note or directory by title, permalink, or path")
async def delete_note(
identifier: str,
is_directory: bool = False,
project: Optional[str] = None,
workspace: Optional[str] = None,
context: Context | None = None,
) -> bool | str:
"""Delete a note or directory from the knowledge base.
Permanently removes a note or directory from the specified project. For single notes,
they are identified by title or permalink. For directories, use is_directory=True and
provide the directory path. If the note/directory doesn't exist, the operation returns
False without error. If deletion fails, helpful error messages are provided.
Project Resolution:
Server resolves projects in this order: Single Project Mode → project parameter → default project.
If project unknown, use list_memory_projects() or recent_activity() first.
Args:
identifier: For files: note title or permalink to delete.
For directories: the directory path (e.g., "docs", "projects/2025").
Can be a title like "Meeting Notes" or permalink like "notes/meeting-notes"
is_directory: If True, deletes an entire directory and all its contents.
When True, identifier should be a directory path
(without file extensions). Defaults to False.
project: Project name to delete from. Optional - server will resolve using hierarchy.
If unknown, use list_memory_projects() to discover available projects.
context: Optional FastMCP context for performance caching.
Returns:
True if note was successfully deleted, False if note was not found.
For directories, returns a formatted summary of deleted files.
On errors, returns a formatted string with helpful troubleshooting guidance.
Examples:
# Delete by title
delete_note("Meeting Notes: Project Planning")
# Delete by permalink
delete_note("notes/project-planning")
# Delete with explicit project
delete_note("experiments/ml-model-results", project="research")
# Delete entire directory
delete_note("docs", is_directory=True)
# Delete nested directory
delete_note("projects/2024", is_directory=True)
# Common usage pattern
if delete_note("old-draft"):
print("Note deleted successfully")
else:
print("Note not found or already deleted")
Raises:
HTTPError: If project doesn't exist or is inaccessible
SecurityError: If identifier attempts path traversal
Warning:
This operation is permanent and cannot be undone. The note/directory files
will be removed from the filesystem and all references will be lost.
Note:
If the note is not found, this function provides helpful error messages
with suggestions for finding the correct identifier, including search
commands and alternative formats to try.
"""
async with get_project_client(project, workspace, context) as (client, active_project):
logger.debug(
f"Deleting {'directory' if is_directory else 'note'}: {identifier} in project: {active_project.name}"
)
# Import here to avoid circular import
from basic_memory.mcp.clients import KnowledgeClient
# Use typed KnowledgeClient for API calls
knowledge_client = KnowledgeClient(client, active_project.external_id)
# Handle directory deletes
if is_directory:
try:
result = await knowledge_client.delete_directory(identifier)
# Build success message for directory delete
result_lines = [
"# Directory Deleted Successfully",
"",
f"**Directory:** `{identifier}`",
"",
"## Summary",
f"- Total files: {result.total_files}",
f"- Successfully deleted: {result.successful_deletes}",
f"- Failed: {result.failed_deletes}",
]
if result.deleted_files:
result_lines.extend(["", "## Deleted Files"])
for file_path in result.deleted_files[:10]: # Show first 10
result_lines.append(f"- `{file_path}`")
if len(result.deleted_files) > 10:
result_lines.append(f"- ... and {len(result.deleted_files) - 10} more")
if result.errors: # pragma: no cover
result_lines.extend(["", "## Errors"])
for error in result.errors[:5]: # Show first 5 errors
result_lines.append(f"- `{error.path}`: {error.error}")
if len(result.errors) > 5:
result_lines.append(f"- ... and {len(result.errors) - 5} more errors")
result_lines.extend(["", f"<!-- Project: {active_project.name} -->"])
logger.info(
f"Directory delete completed: {identifier}, "
f"deleted={result.successful_deletes}, failed={result.failed_deletes}"
)
return "\n".join(result_lines)
except Exception as e: # pragma: no cover
logger.error(f"Directory delete failed for '{identifier}': {e}")
return f"""# Directory Delete Failed
Error deleting directory '{identifier}': {str(e)}
## Troubleshooting:
1. **Verify the directory exists**: Use `list_directory("{identifier}")` to check
2. **Check for permission issues**: Ensure you have delete access to the project
3. **Try individual deletes**: Delete files one at a time if bulk delete fails
## Alternative approach:
```
# List directory contents first
list_directory("{identifier}")
# Then delete individual files
delete_note("path/to/file.md")
```"""
# Handle single note deletes
try:
# Resolve identifier to entity ID
entity_id = await knowledge_client.resolve_entity(identifier)
except ToolError as e:
# If entity not found, return False (note doesn't exist)
if "Entity not found" in str(e) or "not found" in str(e).lower():
logger.warning(f"Note not found for deletion: {identifier}")
return False
# For other resolution errors, return formatted error message
logger.error( # pragma: no cover
f"Delete failed for '{identifier}': {e}, project: {active_project.name}"
)
return _format_delete_error_response( # pragma: no cover
active_project.name, str(e), identifier
)
try:
# Call the DELETE endpoint
result = await knowledge_client.delete_entity(entity_id)
if result.deleted:
logger.info(
f"Successfully deleted note: {identifier} in project: {active_project.name}"
)
return True
else:
logger.warning( # pragma: no cover
f"Delete operation completed but note was not deleted: {identifier}"
)
return False # pragma: no cover
except Exception as e: # pragma: no cover
logger.error(f"Delete failed for '{identifier}': {e}, project: {active_project.name}")
# Return formatted error message for better user experience
return _format_delete_error_response(active_project.name, str(e), identifier)