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.
- basic_memory/__init__.py +1 -7
- basic_memory/api/routers/knowledge_router.py +13 -0
- basic_memory/api/routers/memory_router.py +3 -4
- basic_memory/api/routers/project_router.py +6 -5
- basic_memory/api/routers/prompt_router.py +2 -2
- basic_memory/cli/commands/project.py +2 -2
- basic_memory/cli/commands/status.py +1 -1
- basic_memory/cli/commands/sync.py +1 -1
- basic_memory/mcp/prompts/__init__.py +2 -0
- basic_memory/mcp/prompts/sync_status.py +116 -0
- basic_memory/mcp/server.py +6 -6
- basic_memory/mcp/tools/__init__.py +4 -0
- basic_memory/mcp/tools/build_context.py +32 -7
- basic_memory/mcp/tools/canvas.py +2 -1
- basic_memory/mcp/tools/delete_note.py +159 -4
- basic_memory/mcp/tools/edit_note.py +17 -11
- basic_memory/mcp/tools/move_note.py +252 -40
- basic_memory/mcp/tools/project_management.py +35 -3
- basic_memory/mcp/tools/read_note.py +9 -2
- basic_memory/mcp/tools/search.py +180 -8
- basic_memory/mcp/tools/sync_status.py +254 -0
- basic_memory/mcp/tools/utils.py +47 -0
- basic_memory/mcp/tools/view_note.py +66 -0
- basic_memory/mcp/tools/write_note.py +13 -2
- basic_memory/repository/search_repository.py +99 -26
- basic_memory/schemas/base.py +33 -5
- basic_memory/schemas/memory.py +58 -1
- basic_memory/services/entity_service.py +4 -4
- basic_memory/services/initialization.py +32 -5
- basic_memory/services/link_resolver.py +20 -5
- basic_memory/services/migration_service.py +168 -0
- basic_memory/services/project_service.py +97 -47
- basic_memory/services/sync_status_service.py +181 -0
- basic_memory/sync/sync_service.py +55 -2
- {basic_memory-0.13.0b4.dist-info → basic_memory-0.13.0b5.dist-info}/METADATA +2 -2
- {basic_memory-0.13.0b4.dist-info → basic_memory-0.13.0b5.dist-info}/RECORD +39 -34
- {basic_memory-0.13.0b4.dist-info → basic_memory-0.13.0b5.dist-info}/WHEEL +0 -0
- {basic_memory-0.13.0b4.dist-info → basic_memory-0.13.0b5.dist-info}/entry_points.txt +0 -0
- {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:
|
|
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
|
-
|
|
34
|
-
|
|
35
|
-
|
|
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
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
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(
|
|
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
|
-
|
|
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(
|
|
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
|
|
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
|
|
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
|
|
basic_memory/mcp/tools/search.py
CHANGED
|
@@ -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
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
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)
|