basic-memory 0.13.0b4__py3-none-any.whl → 0.13.0b6__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.

Potentially problematic release.


This version of basic-memory might be problematic. Click here for more details.

Files changed (41) hide show
  1. basic_memory/__init__.py +2 -7
  2. basic_memory/api/routers/knowledge_router.py +13 -0
  3. basic_memory/api/routers/memory_router.py +3 -4
  4. basic_memory/api/routers/project_router.py +6 -5
  5. basic_memory/api/routers/prompt_router.py +2 -2
  6. basic_memory/cli/commands/project.py +3 -3
  7. basic_memory/cli/commands/status.py +1 -1
  8. basic_memory/cli/commands/sync.py +1 -1
  9. basic_memory/cli/commands/tool.py +6 -6
  10. basic_memory/mcp/prompts/__init__.py +2 -0
  11. basic_memory/mcp/prompts/recent_activity.py +1 -1
  12. basic_memory/mcp/prompts/sync_status.py +116 -0
  13. basic_memory/mcp/server.py +6 -6
  14. basic_memory/mcp/tools/__init__.py +4 -0
  15. basic_memory/mcp/tools/build_context.py +32 -7
  16. basic_memory/mcp/tools/canvas.py +2 -1
  17. basic_memory/mcp/tools/delete_note.py +159 -4
  18. basic_memory/mcp/tools/edit_note.py +17 -11
  19. basic_memory/mcp/tools/move_note.py +252 -40
  20. basic_memory/mcp/tools/project_management.py +35 -3
  21. basic_memory/mcp/tools/read_note.py +11 -4
  22. basic_memory/mcp/tools/search.py +180 -8
  23. basic_memory/mcp/tools/sync_status.py +254 -0
  24. basic_memory/mcp/tools/utils.py +47 -0
  25. basic_memory/mcp/tools/view_note.py +66 -0
  26. basic_memory/mcp/tools/write_note.py +13 -2
  27. basic_memory/repository/search_repository.py +116 -38
  28. basic_memory/schemas/base.py +33 -5
  29. basic_memory/schemas/memory.py +58 -1
  30. basic_memory/services/entity_service.py +18 -5
  31. basic_memory/services/initialization.py +32 -5
  32. basic_memory/services/link_resolver.py +20 -5
  33. basic_memory/services/migration_service.py +168 -0
  34. basic_memory/services/project_service.py +121 -50
  35. basic_memory/services/sync_status_service.py +181 -0
  36. basic_memory/sync/sync_service.py +91 -13
  37. {basic_memory-0.13.0b4.dist-info → basic_memory-0.13.0b6.dist-info}/METADATA +2 -2
  38. {basic_memory-0.13.0b4.dist-info → basic_memory-0.13.0b6.dist-info}/RECORD +41 -36
  39. {basic_memory-0.13.0b4.dist-info → basic_memory-0.13.0b6.dist-info}/WHEEL +0 -0
  40. {basic_memory-0.13.0b4.dist-info → basic_memory-0.13.0b6.dist-info}/entry_points.txt +0 -0
  41. {basic_memory-0.13.0b4.dist-info → basic_memory-0.13.0b6.dist-info}/licenses/LICENSE +0 -0
@@ -1,5 +1,8 @@
1
+ from textwrap import dedent
1
2
  from typing import Optional
2
3
 
4
+ from loguru import logger
5
+
3
6
  from basic_memory.mcp.tools.utils import call_delete
4
7
  from basic_memory.mcp.server import mcp
5
8
  from basic_memory.mcp.async_client import client
@@ -7,8 +10,148 @@ from basic_memory.mcp.project_session import get_active_project
7
10
  from basic_memory.schemas import DeleteEntitiesResponse
8
11
 
9
12
 
13
+ def _format_delete_error_response(error_message: str, identifier: str) -> str:
14
+ """Format helpful error responses for delete failures that guide users to successful deletions."""
15
+
16
+ # Note not found errors
17
+ if "entity not found" in error_message.lower() or "not found" in error_message.lower():
18
+ search_term = identifier.split("/")[-1] if "/" in identifier else identifier
19
+ title_format = (
20
+ identifier.split("/")[-1].replace("-", " ").title() if "/" in identifier else identifier
21
+ )
22
+ permalink_format = identifier.lower().replace(" ", "-")
23
+
24
+ return dedent(f"""
25
+ # Delete Failed - Note Not Found
26
+
27
+ The note '{identifier}' could not be found for deletion.
28
+
29
+ ## This might mean:
30
+ 1. **Already deleted**: The note may have been deleted previously
31
+ 2. **Wrong identifier**: The identifier format might be incorrect
32
+ 3. **Different project**: The note might be in a different project
33
+
34
+ ## How to verify:
35
+ 1. **Search for the note**: Use `search_notes("{search_term}")` to find it
36
+ 2. **Try different formats**:
37
+ - If you used a permalink like "folder/note-title", try just the title: "{title_format}"
38
+ - If you used a title, try the permalink format: "{permalink_format}"
39
+
40
+ 3. **Check if already deleted**: Use `list_directory("/")` to see what notes exist
41
+ 4. **Check current project**: Use `get_current_project()` to verify you're in the right project
42
+
43
+ ## If the note actually exists:
44
+ ```
45
+ # First, find the correct identifier:
46
+ search_notes("{identifier}")
47
+
48
+ # Then delete using the correct identifier:
49
+ delete_note("correct-identifier-from-search")
50
+ ```
51
+
52
+ ## If you want to delete multiple similar notes:
53
+ Use search to find all related notes and delete them one by one.
54
+ """).strip()
55
+
56
+ # Permission/access errors
57
+ if (
58
+ "permission" in error_message.lower()
59
+ or "access" in error_message.lower()
60
+ or "forbidden" in error_message.lower()
61
+ ):
62
+ return f"""# Delete Failed - Permission Error
63
+
64
+ You don't have permission to delete '{identifier}': {error_message}
65
+
66
+ ## How to resolve:
67
+ 1. **Check permissions**: Verify you have delete/write access to this project
68
+ 2. **File locks**: The note might be open in another application
69
+ 3. **Project access**: Ensure you're in the correct project with proper permissions
70
+
71
+ ## Alternative actions:
72
+ - Check current project: `get_current_project()`
73
+ - Switch to correct project: `switch_project("project-name")`
74
+ - Verify note exists first: `read_note("{identifier}")`
75
+
76
+ ## If you have read-only access:
77
+ Send a message to support@basicmachines.co to request deletion, or ask someone with write access to delete the note."""
78
+
79
+ # Server/filesystem errors
80
+ if (
81
+ "server error" in error_message.lower()
82
+ or "filesystem" in error_message.lower()
83
+ or "disk" in error_message.lower()
84
+ ):
85
+ return f"""# Delete Failed - System Error
86
+
87
+ A system error occurred while deleting '{identifier}': {error_message}
88
+
89
+ ## Immediate steps:
90
+ 1. **Try again**: The error might be temporary
91
+ 2. **Check file status**: Verify the file isn't locked or in use
92
+ 3. **Check disk space**: Ensure the system has adequate storage
93
+
94
+ ## Troubleshooting:
95
+ - Verify note exists: `read_note("{identifier}")`
96
+ - Check project status: `get_current_project()`
97
+ - Try again in a few moments
98
+
99
+ ## If problem persists:
100
+ Send a message to support@basicmachines.co - there may be a filesystem or database issue."""
101
+
102
+ # Database/sync errors
103
+ if "database" in error_message.lower() or "sync" in error_message.lower():
104
+ return f"""# Delete Failed - Database Error
105
+
106
+ A database error occurred while deleting '{identifier}': {error_message}
107
+
108
+ ## This usually means:
109
+ 1. **Sync conflict**: The file system and database are out of sync
110
+ 2. **Database lock**: Another operation is accessing the database
111
+ 3. **Corrupted entry**: The database entry might be corrupted
112
+
113
+ ## Steps to resolve:
114
+ 1. **Try again**: Wait a moment and retry the deletion
115
+ 2. **Check note status**: `read_note("{identifier}")` to see current state
116
+ 3. **Manual verification**: Use `list_directory()` to see if file still exists
117
+
118
+ ## If the note appears gone but database shows it exists:
119
+ Send a message to support@basicmachines.co - a manual database cleanup may be needed."""
120
+
121
+ # Generic fallback
122
+ return f"""# Delete Failed
123
+
124
+ Error deleting note '{identifier}': {error_message}
125
+
126
+ ## General troubleshooting:
127
+ 1. **Verify the note exists**: `read_note("{identifier}")` or `search_notes("{identifier}")`
128
+ 2. **Check permissions**: Ensure you can edit/delete files in this project
129
+ 3. **Try again**: The error might be temporary
130
+ 4. **Check project**: Make sure you're in the correct project
131
+
132
+ ## Step-by-step approach:
133
+ ```
134
+ # 1. Confirm note exists and get correct identifier
135
+ search_notes("{identifier}")
136
+
137
+ # 2. Read the note to verify access
138
+ read_note("correct-identifier-from-search")
139
+
140
+ # 3. Try deletion with correct identifier
141
+ delete_note("correct-identifier-from-search")
142
+ ```
143
+
144
+ ## Alternative approaches:
145
+ - Check what notes exist: `list_directory("/")`
146
+ - Verify current project: `get_current_project()`
147
+ - Switch projects if needed: `switch_project("correct-project")`
148
+
149
+ ## Need help?
150
+ If the note should be deleted but the operation keeps failing, send a message to support@basicmachines.co."""
151
+
152
+
10
153
  @mcp.tool(description="Delete a note by title or permalink")
11
- async def delete_note(identifier: str, project: Optional[str] = None) -> bool:
154
+ async def delete_note(identifier: str, project: Optional[str] = None) -> bool | str:
12
155
  """Delete a note from the knowledge base.
13
156
 
14
157
  Args:
@@ -31,6 +174,18 @@ async def delete_note(identifier: str, project: Optional[str] = None) -> bool:
31
174
  active_project = get_active_project(project)
32
175
  project_url = active_project.project_url
33
176
 
34
- response = await call_delete(client, f"{project_url}/knowledge/entities/{identifier}")
35
- result = DeleteEntitiesResponse.model_validate(response.json())
36
- return result.deleted
177
+ try:
178
+ response = await call_delete(client, f"{project_url}/knowledge/entities/{identifier}")
179
+ result = DeleteEntitiesResponse.model_validate(response.json())
180
+
181
+ if result.deleted:
182
+ logger.info(f"Successfully deleted note: {identifier}")
183
+ return True
184
+ else:
185
+ logger.warning(f"Delete operation completed but note was not deleted: {identifier}")
186
+ return False
187
+
188
+ except Exception as e: # pragma: no cover
189
+ logger.error(f"Delete failed for '{identifier}': {e}")
190
+ # Return formatted error message for better user experience
191
+ return _format_delete_error_response(str(e), identifier)
@@ -24,14 +24,14 @@ def _format_error_response(
24
24
  if "Entity not found" in error_message or "entity not found" in error_message.lower():
25
25
  return f"""# Edit Failed - Note Not Found
26
26
 
27
- The note with identifier '{identifier}' could not be found.
27
+ The note with identifier '{identifier}' could not be found. Edit operations require an exact match (no fuzzy matching).
28
28
 
29
29
  ## Suggestions to try:
30
- 1. **Search for the note first**: Use `search_notes("{identifier.split("/")[-1]}")` to find similar notes
31
- 2. **Try different identifier formats**:
32
- - If you used a permalink like "folder/note-title", try just the title: "{identifier.split("/")[-1].replace("-", " ").title()}"
33
- - If you used a title, try the permalink format: "{identifier.lower().replace(" ", "-")}"
34
- - Use `read_note()` first to verify the note exists and get the correct identifiers
30
+ 1. **Search for the note first**: Use `search_notes("{identifier.split("/")[-1]}")` to find similar notes with exact identifiers
31
+ 2. **Try different exact identifier formats**:
32
+ - If you used a permalink like "folder/note-title", try the exact title: "{identifier.split("/")[-1].replace("-", " ").title()}"
33
+ - If you used a title, try the exact permalink format: "{identifier.lower().replace(" ", "-")}"
34
+ - Use `read_note()` first to verify the note exists and get the exact identifier
35
35
 
36
36
  ## Alternative approach:
37
37
  Use `write_note()` to create the note first, then edit it."""
@@ -142,7 +142,9 @@ async def edit_note(
142
142
  It supports various operations for different editing scenarios.
143
143
 
144
144
  Args:
145
- identifier: The title, permalink, or memory:// URL of the note to edit
145
+ identifier: The exact title, permalink, or memory:// URL of the note to edit.
146
+ Must be an exact match - fuzzy matching is not supported for edit operations.
147
+ Use search_notes() or read_note() first to find the correct identifier if uncertain.
146
148
  operation: The editing operation to perform:
147
149
  - "append": Add content to the end of the note
148
150
  - "prepend": Add content to the beginning of the note
@@ -179,10 +181,14 @@ async def edit_note(
179
181
  # Replace subsection with more specific header
180
182
  edit_note("docs/setup", "replace_section", "Updated install steps\\n", section="### Installation")
181
183
 
182
- # Using different identifier formats
183
- edit_note("Meeting Notes", "append", "\\n- Follow up on action items") # title
184
- edit_note("docs/meeting-notes", "append", "\\n- Follow up tasks") # permalink
185
- edit_note("docs/Meeting Notes", "append", "\\n- Next steps") # folder/title
184
+ # Using different identifier formats (must be exact matches)
185
+ edit_note("Meeting Notes", "append", "\\n- Follow up on action items") # exact title
186
+ edit_note("docs/meeting-notes", "append", "\\n- Follow up tasks") # exact permalink
187
+ edit_note("docs/Meeting Notes", "append", "\\n- Next steps") # exact folder/title
188
+
189
+ # If uncertain about identifier, search first:
190
+ # search_notes("meeting") # Find available notes
191
+ # edit_note("docs/meeting-notes-2025", "append", "content") # Use exact result
186
192
 
187
193
  # Add new section to document
188
194
  edit_note("project-plan", "replace_section", "TBD - needs research\\n", section="## Future Work")
@@ -1,5 +1,6 @@
1
1
  """Move note tool for Basic Memory MCP server."""
2
2
 
3
+ from textwrap import dedent
3
4
  from typing import Optional
4
5
 
5
6
  from loguru import logger
@@ -11,6 +12,203 @@ from basic_memory.mcp.project_session import get_active_project
11
12
  from basic_memory.schemas import EntityResponse
12
13
 
13
14
 
15
+ def _format_move_error_response(error_message: str, identifier: str, destination_path: str) -> str:
16
+ """Format helpful error responses for move failures that guide users to successful moves."""
17
+
18
+ # Note not found errors
19
+ if "entity not found" in error_message.lower() or "not found" in error_message.lower():
20
+ search_term = identifier.split("/")[-1] if "/" in identifier else identifier
21
+ title_format = (
22
+ identifier.split("/")[-1].replace("-", " ").title() if "/" in identifier else identifier
23
+ )
24
+ permalink_format = identifier.lower().replace(" ", "-")
25
+
26
+ return dedent(f"""
27
+ # Move Failed - Note Not Found
28
+
29
+ The note '{identifier}' could not be found for moving. Move operations require an exact match (no fuzzy matching).
30
+
31
+ ## Suggestions to try:
32
+ 1. **Search for the note first**: Use `search_notes("{search_term}")` to find it with exact identifiers
33
+ 2. **Try different exact identifier formats**:
34
+ - If you used a permalink like "folder/note-title", try the exact title: "{title_format}"
35
+ - If you used a title, try the exact permalink format: "{permalink_format}"
36
+ - Use `read_note()` first to verify the note exists and get the exact identifier
37
+
38
+ 3. **Check current project**: Use `get_current_project()` to verify you're in the right project
39
+ 4. **List available notes**: Use `list_directory("/")` to see what notes exist
40
+
41
+ ## Before trying again:
42
+ ```
43
+ # First, verify the note exists:
44
+ search_notes("{identifier}")
45
+
46
+ # Then use the exact identifier from search results:
47
+ move_note("correct-identifier-here", "{destination_path}")
48
+ ```
49
+ """).strip()
50
+
51
+ # Destination already exists errors
52
+ if "already exists" in error_message.lower() or "file exists" in error_message.lower():
53
+ return f"""# Move Failed - Destination Already Exists
54
+
55
+ Cannot move '{identifier}' to '{destination_path}' because a file already exists at that location.
56
+
57
+ ## How to resolve:
58
+ 1. **Choose a different destination**: Try a different filename or folder
59
+ - Add timestamp: `{destination_path.rsplit(".", 1)[0] if "." in destination_path else destination_path}-backup.md`
60
+ - Use different folder: `archive/{destination_path}` or `backup/{destination_path}`
61
+
62
+ 2. **Check the existing file**: Use `read_note("{destination_path}")` to see what's already there
63
+ 3. **Remove or rename existing**: If safe to do so, move the existing file first
64
+
65
+ ## Try these alternatives:
66
+ ```
67
+ # Option 1: Add timestamp to make unique
68
+ move_note("{identifier}", "{destination_path.rsplit(".", 1)[0] if "." in destination_path else destination_path}-backup.md")
69
+
70
+ # Option 2: Use archive folder
71
+ move_note("{identifier}", "archive/{destination_path}")
72
+
73
+ # Option 3: Check what's at destination first
74
+ read_note("{destination_path}")
75
+ ```"""
76
+
77
+ # Invalid path errors
78
+ if "invalid" in error_message.lower() and "path" in error_message.lower():
79
+ return f"""# Move Failed - Invalid Destination Path
80
+
81
+ The destination path '{destination_path}' is not valid: {error_message}
82
+
83
+ ## Path requirements:
84
+ 1. **Relative paths only**: Don't start with `/` (use `notes/file.md` not `/notes/file.md`)
85
+ 2. **Include file extension**: Add `.md` for markdown files
86
+ 3. **Use forward slashes**: For folder separators (`folder/subfolder/file.md`)
87
+ 4. **No special characters**: Avoid `\\`, `:`, `*`, `?`, `"`, `<`, `>`, `|`
88
+
89
+ ## Valid path examples:
90
+ - `notes/my-note.md`
91
+ - `projects/2025/meeting-notes.md`
92
+ - `archive/old-projects/legacy-note.md`
93
+
94
+ ## Try again with:
95
+ ```
96
+ move_note("{identifier}", "notes/{destination_path.split("/")[-1] if "/" in destination_path else destination_path}")
97
+ ```"""
98
+
99
+ # Permission/access errors
100
+ if (
101
+ "permission" in error_message.lower()
102
+ or "access" in error_message.lower()
103
+ or "forbidden" in error_message.lower()
104
+ ):
105
+ return f"""# Move Failed - Permission Error
106
+
107
+ You don't have permission to move '{identifier}': {error_message}
108
+
109
+ ## How to resolve:
110
+ 1. **Check file permissions**: Ensure you have write access to both source and destination
111
+ 2. **Verify project access**: Make sure you have edit permissions for this project
112
+ 3. **Check file locks**: The file might be open in another application
113
+
114
+ ## Alternative actions:
115
+ - Check current project: `get_current_project()`
116
+ - Switch projects if needed: `switch_project("project-name")`
117
+ - Try copying content instead: `read_note("{identifier}")` then `write_note()` to new location"""
118
+
119
+ # Source file not found errors
120
+ if "source" in error_message.lower() and (
121
+ "not found" in error_message.lower() or "missing" in error_message.lower()
122
+ ):
123
+ return f"""# Move Failed - Source File Missing
124
+
125
+ The source file for '{identifier}' was not found on disk: {error_message}
126
+
127
+ This usually means the database and filesystem are out of sync.
128
+
129
+ ## How to resolve:
130
+ 1. **Check if note exists in database**: `read_note("{identifier}")`
131
+ 2. **Run sync operation**: The file might need to be re-synced
132
+ 3. **Recreate the file**: If data exists in database, recreate the physical file
133
+
134
+ ## Troubleshooting steps:
135
+ ```
136
+ # Check if note exists in Basic Memory
137
+ read_note("{identifier}")
138
+
139
+ # If it exists, the file is missing on disk - send a message to support@basicmachines.co
140
+ # If it doesn't exist, use search to find the correct identifier
141
+ search_notes("{identifier}")
142
+ ```"""
143
+
144
+ # Server/filesystem errors
145
+ if (
146
+ "server error" in error_message.lower()
147
+ or "filesystem" in error_message.lower()
148
+ or "disk" in error_message.lower()
149
+ ):
150
+ return f"""# Move Failed - System Error
151
+
152
+ A system error occurred while moving '{identifier}': {error_message}
153
+
154
+ ## Immediate steps:
155
+ 1. **Try again**: The error might be temporary
156
+ 2. **Check disk space**: Ensure adequate storage is available
157
+ 3. **Verify filesystem permissions**: Check if the destination directory is writable
158
+
159
+ ## Alternative approaches:
160
+ - Copy content to new location: Use `read_note("{identifier}")` then `write_note()`
161
+ - Use a different destination folder that you know works
162
+ - Send a message to support@basicmachines.co if the problem persists
163
+
164
+ ## Backup approach:
165
+ ```
166
+ # Read current content
167
+ content = read_note("{identifier}")
168
+
169
+ # Create new note at desired location
170
+ write_note("New Note Title", content, "{destination_path.split("/")[0] if "/" in destination_path else "notes"}")
171
+
172
+ # Then delete original if successful
173
+ delete_note("{identifier}")
174
+ ```"""
175
+
176
+ # Generic fallback
177
+ return f"""# Move Failed
178
+
179
+ Error moving '{identifier}' to '{destination_path}': {error_message}
180
+
181
+ ## General troubleshooting:
182
+ 1. **Verify the note exists**: `read_note("{identifier}")` or `search_notes("{identifier}")`
183
+ 2. **Check destination path**: Ensure it's a valid relative path with `.md` extension
184
+ 3. **Verify permissions**: Make sure you can edit files in this project
185
+ 4. **Try a simpler path**: Use a basic folder structure like `notes/filename.md`
186
+
187
+ ## Step-by-step approach:
188
+ ```
189
+ # 1. Confirm note exists
190
+ read_note("{identifier}")
191
+
192
+ # 2. Try a simple destination first
193
+ move_note("{identifier}", "notes/{destination_path.split("/")[-1] if "/" in destination_path else destination_path}")
194
+
195
+ # 3. If that works, then try your original destination
196
+ ```
197
+
198
+ ## Alternative approach:
199
+ If moving continues to fail, you can copy the content manually:
200
+ ```
201
+ # Read current content
202
+ content = read_note("{identifier}")
203
+
204
+ # Create new note
205
+ write_note("Title", content, "target-folder")
206
+
207
+ # Delete original once confirmed
208
+ delete_note("{identifier}")
209
+ ```"""
210
+
211
+
14
212
  @mcp.tool(
15
213
  description="Move a note to a new location, updating database and maintaining links.",
16
214
  )
@@ -22,7 +220,9 @@ async def move_note(
22
220
  """Move a note to a new file location within the same project.
23
221
 
24
222
  Args:
25
- identifier: Entity identifier (title, permalink, or memory:// URL)
223
+ identifier: Exact entity identifier (title, permalink, or memory:// URL).
224
+ Must be an exact match - fuzzy matching is not supported for move operations.
225
+ Use search_notes() or read_note() first to find the correct identifier if uncertain.
26
226
  destination_path: New path relative to project root (e.g., "work/meetings/2025-05-26.md")
27
227
  project: Optional project name (defaults to current session project)
28
228
 
@@ -30,9 +230,18 @@ async def move_note(
30
230
  Success message with move details
31
231
 
32
232
  Examples:
33
- - Move to new folder: move_note("My Note", "work/notes/my-note.md")
34
- - Move by permalink: move_note("my-note-permalink", "archive/old-notes/my-note.md")
35
- - Specify project: move_note("My Note", "archive/my-note.md", project="work-project")
233
+ # Move to new folder (exact title match)
234
+ move_note("My Note", "work/notes/my-note.md")
235
+
236
+ # Move by exact permalink
237
+ move_note("my-note-permalink", "archive/old-notes/my-note.md")
238
+
239
+ # Specify project with exact identifier
240
+ move_note("My Note", "archive/my-note.md", project="work-project")
241
+
242
+ # If uncertain about identifier, search first:
243
+ # search_notes("my note") # Find available notes
244
+ # move_note("docs/my-note-2025", "archive/my-note.md") # Use exact result
36
245
 
37
246
  Note: This operation moves notes within the specified project only. Moving notes
38
247
  between different projects is not currently supported.
@@ -49,39 +258,42 @@ async def move_note(
49
258
  active_project = get_active_project(project)
50
259
  project_url = active_project.project_url
51
260
 
52
- # Prepare move request
53
- move_data = {
54
- "identifier": identifier,
55
- "destination_path": destination_path,
56
- "project": active_project.name,
57
- }
58
-
59
- # Call the move API endpoint
60
- url = f"{project_url}/knowledge/move"
61
- response = await call_post(client, url, json=move_data)
62
- result = EntityResponse.model_validate(response.json())
63
-
64
- # 10. Build success message
65
- result_lines = [
66
- "✅ Note moved successfully",
67
- "",
68
- f"📁 **{identifier}** → **{result.file_path}**",
69
- f"🔗 Permalink: {result.permalink}",
70
- "📊 Database and search index updated",
71
- "",
72
- f"<!-- Project: {active_project.name} -->",
73
- ]
74
-
75
- # Return the response text which contains the formatted success message
76
- result = "\n".join(result_lines)
77
-
78
- # Log the operation
79
- logger.info(
80
- "Move note completed",
81
- identifier=identifier,
82
- destination_path=destination_path,
83
- project=active_project.name,
84
- status_code=response.status_code,
85
- )
86
-
87
- return result
261
+ try:
262
+ # Prepare move request
263
+ move_data = {
264
+ "identifier": identifier,
265
+ "destination_path": destination_path,
266
+ "project": active_project.name,
267
+ }
268
+
269
+ # Call the move API endpoint
270
+ url = f"{project_url}/knowledge/move"
271
+ response = await call_post(client, url, json=move_data)
272
+ result = EntityResponse.model_validate(response.json())
273
+
274
+ # Build success message
275
+ result_lines = [
276
+ "✅ Note moved successfully",
277
+ "",
278
+ f"📁 **{identifier}** → **{result.file_path}**",
279
+ f"🔗 Permalink: {result.permalink}",
280
+ "📊 Database and search index updated",
281
+ "",
282
+ f"<!-- Project: {active_project.name} -->",
283
+ ]
284
+
285
+ # Log the operation
286
+ logger.info(
287
+ "Move note completed",
288
+ identifier=identifier,
289
+ destination_path=destination_path,
290
+ project=active_project.name,
291
+ status_code=response.status_code,
292
+ )
293
+
294
+ return "\n".join(result_lines)
295
+
296
+ except Exception as e:
297
+ logger.error(f"Move failed for '{identifier}' to '{destination_path}': {e}")
298
+ # Return formatted error message for better user experience
299
+ return _format_move_error_response(str(e), identifier, destination_path)
@@ -4,6 +4,8 @@ These tools allow users to switch between projects, list available projects,
4
4
  and manage project context during conversations.
5
5
  """
6
6
 
7
+ from textwrap import dedent
8
+
7
9
  from fastmcp import Context
8
10
  from loguru import logger
9
11
 
@@ -94,7 +96,11 @@ async def switch_project(project_name: str, ctx: Context | None = None) -> str:
94
96
 
95
97
  # Get project info to show summary
96
98
  try:
97
- response = await call_get(client, f"{project_config.project_url}/project/info")
99
+ response = await call_get(
100
+ client,
101
+ f"{project_config.project_url}/project/info",
102
+ params={"project_name": project_name},
103
+ )
98
104
  project_info = ProjectInfoResponse.model_validate(response.json())
99
105
 
100
106
  result = f"✓ Switched to {project_name} project\n\n"
@@ -115,7 +121,29 @@ async def switch_project(project_name: str, ctx: Context | None = None) -> str:
115
121
  logger.error(f"Error switching to project {project_name}: {e}")
116
122
  # Revert to previous project on error
117
123
  session.set_current_project(current_project)
118
- raise e
124
+
125
+ # Return user-friendly error message instead of raising exception
126
+ return dedent(f"""
127
+ # Project Switch Failed
128
+
129
+ Could not switch to project '{project_name}': {str(e)}
130
+
131
+ ## Current project: {current_project}
132
+ Your session remains on the previous project.
133
+
134
+ ## Troubleshooting:
135
+ 1. **Check available projects**: Use `list_projects()` to see valid project names
136
+ 2. **Verify spelling**: Ensure the project name is spelled correctly
137
+ 3. **Check permissions**: Verify you have access to the requested project
138
+ 4. **Try again**: The error might be temporary
139
+
140
+ ## Available options:
141
+ - See all projects: `list_projects()`
142
+ - Stay on current project: `get_current_project()`
143
+ - Try different project: `switch_project("correct-project-name")`
144
+
145
+ If the project should exist but isn't listed, send a message to support@basicmachines.co.
146
+ """).strip()
119
147
 
120
148
 
121
149
  @mcp.tool()
@@ -139,7 +167,11 @@ async def get_current_project(ctx: Context | None = None) -> str:
139
167
  result = f"Current project: {current_project}\n\n"
140
168
 
141
169
  # get project stats
142
- response = await call_get(client, f"{project_config.project_url}/project/info")
170
+ response = await call_get(
171
+ client,
172
+ f"{project_config.project_url}/project/info",
173
+ params={"project_name": current_project},
174
+ )
143
175
  project_info = ProjectInfoResponse.model_validate(response.json())
144
176
 
145
177
  result += f"• {project_info.statistics.total_entities} entities\n"
@@ -52,6 +52,13 @@ async def read_note(
52
52
  read_note("Meeting Notes", project="work-project")
53
53
  """
54
54
 
55
+ # Check migration status and wait briefly if needed
56
+ from basic_memory.mcp.tools.utils import wait_for_migration_or_return_status
57
+
58
+ migration_status = await wait_for_migration_or_return_status(timeout=5.0)
59
+ if migration_status: # pragma: no cover
60
+ return f"# System Status\n\n{migration_status}\n\nPlease wait for migration to complete before reading notes."
61
+
55
62
  active_project = get_active_project(project)
56
63
  project_url = active_project.project_url
57
64
 
@@ -74,7 +81,7 @@ async def read_note(
74
81
 
75
82
  # Fallback 1: Try title search via API
76
83
  logger.info(f"Search title for: {identifier}")
77
- title_results = await search_notes(query=identifier, search_type="title", project=project)
84
+ title_results = await search_notes.fn(query=identifier, search_type="title", project=project)
78
85
 
79
86
  if title_results and title_results.results:
80
87
  result = title_results.results[0] # Get the first/best match
@@ -98,7 +105,7 @@ async def read_note(
98
105
 
99
106
  # Fallback 2: Text search as a last resort
100
107
  logger.info(f"Title search failed, trying text search for: {identifier}")
101
- text_results = await search_notes(query=identifier, search_type="text", project=project)
108
+ text_results = await search_notes.fn(query=identifier, search_type="text", project=project)
102
109
 
103
110
  # We didn't find a direct match, construct a helpful error message
104
111
  if not text_results or not text_results.results:
@@ -114,7 +121,7 @@ def format_not_found_message(identifier: str) -> str:
114
121
  return dedent(f"""
115
122
  # Note Not Found: "{identifier}"
116
123
 
117
- I couldn't find any notes matching "{identifier}". Here are some suggestions:
124
+ I searched for "{identifier}" using multiple methods (direct lookup, title search, and text search) but couldn't find any matching notes. Here are some suggestions:
118
125
 
119
126
  ## Check Identifier Type
120
127
  - If you provided a title, try using the exact permalink instead
@@ -160,7 +167,7 @@ def format_related_results(identifier: str, results) -> str:
160
167
  message = dedent(f"""
161
168
  # Note Not Found: "{identifier}"
162
169
 
163
- I couldn't find an exact match for "{identifier}", but I found some related notes:
170
+ I searched for "{identifier}" using direct lookup and title search but couldn't find an exact match. However, I found some related notes through text search:
164
171
 
165
172
  """)
166
173