basic-memory 0.13.0b4__py3-none-any.whl → 0.13.0b5__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 (39) hide show
  1. basic_memory/__init__.py +1 -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 +2 -2
  7. basic_memory/cli/commands/status.py +1 -1
  8. basic_memory/cli/commands/sync.py +1 -1
  9. basic_memory/mcp/prompts/__init__.py +2 -0
  10. basic_memory/mcp/prompts/sync_status.py +116 -0
  11. basic_memory/mcp/server.py +6 -6
  12. basic_memory/mcp/tools/__init__.py +4 -0
  13. basic_memory/mcp/tools/build_context.py +32 -7
  14. basic_memory/mcp/tools/canvas.py +2 -1
  15. basic_memory/mcp/tools/delete_note.py +159 -4
  16. basic_memory/mcp/tools/edit_note.py +17 -11
  17. basic_memory/mcp/tools/move_note.py +252 -40
  18. basic_memory/mcp/tools/project_management.py +35 -3
  19. basic_memory/mcp/tools/read_note.py +9 -2
  20. basic_memory/mcp/tools/search.py +180 -8
  21. basic_memory/mcp/tools/sync_status.py +254 -0
  22. basic_memory/mcp/tools/utils.py +47 -0
  23. basic_memory/mcp/tools/view_note.py +66 -0
  24. basic_memory/mcp/tools/write_note.py +13 -2
  25. basic_memory/repository/search_repository.py +99 -26
  26. basic_memory/schemas/base.py +33 -5
  27. basic_memory/schemas/memory.py +58 -1
  28. basic_memory/services/entity_service.py +4 -4
  29. basic_memory/services/initialization.py +32 -5
  30. basic_memory/services/link_resolver.py +20 -5
  31. basic_memory/services/migration_service.py +168 -0
  32. basic_memory/services/project_service.py +97 -47
  33. basic_memory/services/sync_status_service.py +181 -0
  34. basic_memory/sync/sync_service.py +55 -2
  35. {basic_memory-0.13.0b4.dist-info → basic_memory-0.13.0b5.dist-info}/METADATA +2 -2
  36. {basic_memory-0.13.0b4.dist-info → basic_memory-0.13.0b5.dist-info}/RECORD +39 -34
  37. {basic_memory-0.13.0b4.dist-info → basic_memory-0.13.0b5.dist-info}/WHEEL +0 -0
  38. {basic_memory-0.13.0b4.dist-info → basic_memory-0.13.0b5.dist-info}/entry_points.txt +0 -0
  39. {basic_memory-0.13.0b4.dist-info → basic_memory-0.13.0b5.dist-info}/licenses/LICENSE +0 -0
@@ -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
 
@@ -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
 
@@ -1,5 +1,6 @@
1
1
  """Search tools for Basic Memory MCP server."""
2
2
 
3
+ from textwrap import dedent
3
4
  from typing import List, Optional
4
5
 
5
6
  from loguru import logger
@@ -11,6 +12,162 @@ from basic_memory.mcp.project_session import get_active_project
11
12
  from basic_memory.schemas.search import SearchItemType, SearchQuery, SearchResponse
12
13
 
13
14
 
15
+ def _format_search_error_response(error_message: str, query: str, search_type: str = "text") -> str:
16
+ """Format helpful error responses for search failures that guide users to successful searches."""
17
+
18
+ # FTS5 syntax errors
19
+ if "syntax error" in error_message.lower() or "fts5" in error_message.lower():
20
+ clean_query = (
21
+ query.replace('"', "")
22
+ .replace("(", "")
23
+ .replace(")", "")
24
+ .replace("+", "")
25
+ .replace("*", "")
26
+ )
27
+ return dedent(f"""
28
+ # Search Failed - Invalid Syntax
29
+
30
+ The search query '{query}' contains invalid syntax that the search engine cannot process.
31
+
32
+ ## Common syntax issues:
33
+ 1. **Special characters**: Characters like `+`, `*`, `"`, `(`, `)` have special meaning in search
34
+ 2. **Unmatched quotes**: Make sure quotes are properly paired
35
+ 3. **Invalid operators**: Check AND, OR, NOT operators are used correctly
36
+
37
+ ## How to fix:
38
+ 1. **Simplify your search**: Try using simple words instead: `{clean_query}`
39
+ 2. **Remove special characters**: Use alphanumeric characters and spaces
40
+ 3. **Use basic boolean operators**: `word1 AND word2`, `word1 OR word2`, `word1 NOT word2`
41
+
42
+ ## Examples of valid searches:
43
+ - Simple text: `project planning`
44
+ - Boolean AND: `project AND planning`
45
+ - Boolean OR: `meeting OR discussion`
46
+ - Boolean NOT: `project NOT archived`
47
+ - Grouped: `(project OR planning) AND notes`
48
+
49
+ ## Try again with:
50
+ ```
51
+ search_notes("INSERT_CLEAN_QUERY_HERE")
52
+ ```
53
+
54
+ Replace INSERT_CLEAN_QUERY_HERE with your simplified search terms.
55
+ """).strip()
56
+
57
+ # Project not found errors (check before general "not found")
58
+ if "project not found" in error_message.lower():
59
+ return dedent(f"""
60
+ # Search Failed - Project Not Found
61
+
62
+ The current project is not accessible or doesn't exist: {error_message}
63
+
64
+ ## How to resolve:
65
+ 1. **Check available projects**: `list_projects()`
66
+ 2. **Switch to valid project**: `switch_project("valid-project-name")`
67
+ 3. **Verify project setup**: Ensure your project is properly configured
68
+
69
+ ## Current session info:
70
+ - Check current project: `get_current_project()`
71
+ - See available projects: `list_projects()`
72
+ """).strip()
73
+
74
+ # No results found
75
+ if "no results" in error_message.lower() or "not found" in error_message.lower():
76
+ simplified_query = (
77
+ " ".join(query.split()[:2])
78
+ if len(query.split()) > 2
79
+ else query.split()[0]
80
+ if query.split()
81
+ else "notes"
82
+ )
83
+ return dedent(f"""
84
+ # Search Complete - No Results Found
85
+
86
+ No content found matching '{query}' in the current project.
87
+
88
+ ## Suggestions to try:
89
+ 1. **Broaden your search**: Try fewer or more general terms
90
+ - Instead of: `{query}`
91
+ - Try: `{simplified_query}`
92
+
93
+ 2. **Check spelling**: Verify terms are spelled correctly
94
+ 3. **Try different search types**:
95
+ - Text search: `search_notes("{query}", search_type="text")`
96
+ - Title search: `search_notes("{query}", search_type="title")`
97
+ - Permalink search: `search_notes("{query}", search_type="permalink")`
98
+
99
+ 4. **Use boolean operators**:
100
+ - Try OR search for broader results
101
+
102
+ ## Check what content exists:
103
+ - Recent activity: `recent_activity(timeframe="7d")`
104
+ - List files: `list_directory("/")`
105
+ - Browse by folder: `list_directory("/notes")` or `list_directory("/docs")`
106
+ """).strip()
107
+
108
+ # Server/API errors
109
+ if "server error" in error_message.lower() or "internal" in error_message.lower():
110
+ return dedent(f"""
111
+ # Search Failed - Server Error
112
+
113
+ The search service encountered an error while processing '{query}': {error_message}
114
+
115
+ ## Immediate steps:
116
+ 1. **Try again**: The error might be temporary
117
+ 2. **Simplify the query**: Use simpler search terms
118
+ 3. **Check project status**: Ensure your project is properly synced
119
+
120
+ ## Alternative approaches:
121
+ - Browse files directly: `list_directory("/")`
122
+ - Check recent activity: `recent_activity(timeframe="7d")`
123
+ - Try a different search type: `search_notes("{query}", search_type="title")`
124
+
125
+ ## If the problem persists:
126
+ The search index might need to be rebuilt. Send a message to support@basicmachines.co or check the project sync status.
127
+ """).strip()
128
+
129
+ # Permission/access errors
130
+ if (
131
+ "permission" in error_message.lower()
132
+ or "access" in error_message.lower()
133
+ or "forbidden" in error_message.lower()
134
+ ):
135
+ return f"""# Search Failed - Access Error
136
+
137
+ You don't have permission to search in the current project: {error_message}
138
+
139
+ ## How to resolve:
140
+ 1. **Check your project access**: Verify you have read permissions for this project
141
+ 2. **Switch projects**: Try searching in a different project you have access to
142
+ 3. **Check authentication**: You might need to re-authenticate
143
+
144
+ ## Alternative actions:
145
+ - List available projects: `list_projects()`
146
+ - Switch to accessible project: `switch_project("project-name")`
147
+ - Check current project: `get_current_project()`"""
148
+
149
+ # Generic fallback
150
+ return f"""# Search Failed
151
+
152
+ Error searching for '{query}': {error_message}
153
+
154
+ ## General troubleshooting:
155
+ 1. **Check your query**: Ensure it uses valid search syntax
156
+ 2. **Try simpler terms**: Use basic words without special characters
157
+ 3. **Verify project access**: Make sure you can access the current project
158
+ 4. **Check recent activity**: `recent_activity(timeframe="7d")` to see if content exists
159
+
160
+ ## Alternative approaches:
161
+ - Browse files: `list_directory("/")`
162
+ - Try different search type: `search_notes("{query}", search_type="title")`
163
+ - Search with filters: `search_notes("{query}", types=["entity"])`
164
+
165
+ ## Need help?
166
+ - View recent changes: `recent_activity()`
167
+ - List projects: `list_projects()`
168
+ - Check current project: `get_current_project()`"""
169
+
170
+
14
171
  @mcp.tool(
15
172
  description="Search across all content in the knowledge base.",
16
173
  )
@@ -23,7 +180,7 @@ async def search_notes(
23
180
  entity_types: Optional[List[str]] = None,
24
181
  after_date: Optional[str] = None,
25
182
  project: Optional[str] = None,
26
- ) -> SearchResponse:
183
+ ) -> SearchResponse | str:
27
184
  """Search across all content in the knowledge base.
28
185
 
29
186
  This tool searches the knowledge base using full-text search, pattern matching,
@@ -113,10 +270,25 @@ async def search_notes(
113
270
  project_url = active_project.project_url
114
271
 
115
272
  logger.info(f"Searching for {search_query}")
116
- response = await call_post(
117
- client,
118
- f"{project_url}/search/",
119
- json=search_query.model_dump(),
120
- params={"page": page, "page_size": page_size},
121
- )
122
- return SearchResponse.model_validate(response.json())
273
+
274
+ try:
275
+ response = await call_post(
276
+ client,
277
+ f"{project_url}/search/",
278
+ json=search_query.model_dump(),
279
+ params={"page": page, "page_size": page_size},
280
+ )
281
+ result = SearchResponse.model_validate(response.json())
282
+
283
+ # Check if we got no results and provide helpful guidance
284
+ if not result.results:
285
+ logger.info(f"Search returned no results for query: {query}")
286
+ # Don't treat this as an error, but the user might want guidance
287
+ # We return the empty result as normal - the user can decide if they need help
288
+
289
+ return result
290
+
291
+ except Exception as e:
292
+ logger.error(f"Search failed for query '{query}': {e}")
293
+ # Return formatted error message as string for better user experience
294
+ return _format_search_error_response(str(e), query, search_type)