basic-memory 0.14.4__py3-none-any.whl → 0.15.0__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 -1
- basic_memory/alembic/versions/a1b2c3d4e5f6_fix_project_foreign_keys.py +5 -9
- basic_memory/api/app.py +10 -4
- basic_memory/api/routers/knowledge_router.py +25 -8
- basic_memory/api/routers/project_router.py +99 -4
- basic_memory/cli/app.py +9 -28
- basic_memory/cli/auth.py +277 -0
- basic_memory/cli/commands/cloud/__init__.py +5 -0
- basic_memory/cli/commands/cloud/api_client.py +112 -0
- basic_memory/cli/commands/cloud/bisync_commands.py +818 -0
- basic_memory/cli/commands/cloud/core_commands.py +288 -0
- basic_memory/cli/commands/cloud/mount_commands.py +295 -0
- basic_memory/cli/commands/cloud/rclone_config.py +288 -0
- basic_memory/cli/commands/cloud/rclone_installer.py +198 -0
- basic_memory/cli/commands/command_utils.py +60 -0
- basic_memory/cli/commands/import_memory_json.py +0 -4
- basic_memory/cli/commands/mcp.py +16 -4
- basic_memory/cli/commands/project.py +139 -142
- basic_memory/cli/commands/status.py +34 -22
- basic_memory/cli/commands/sync.py +45 -228
- basic_memory/cli/commands/tool.py +87 -16
- basic_memory/cli/main.py +1 -0
- basic_memory/config.py +76 -12
- basic_memory/db.py +104 -3
- basic_memory/deps.py +20 -3
- basic_memory/file_utils.py +37 -13
- basic_memory/ignore_utils.py +295 -0
- basic_memory/markdown/plugins.py +9 -7
- basic_memory/mcp/async_client.py +22 -10
- basic_memory/mcp/project_context.py +141 -0
- basic_memory/mcp/prompts/ai_assistant_guide.py +49 -4
- basic_memory/mcp/prompts/continue_conversation.py +1 -1
- basic_memory/mcp/prompts/recent_activity.py +116 -32
- basic_memory/mcp/prompts/search.py +1 -1
- basic_memory/mcp/prompts/utils.py +11 -4
- basic_memory/mcp/resources/ai_assistant_guide.md +179 -41
- basic_memory/mcp/resources/project_info.py +20 -6
- basic_memory/mcp/server.py +0 -37
- basic_memory/mcp/tools/__init__.py +5 -6
- basic_memory/mcp/tools/build_context.py +29 -19
- basic_memory/mcp/tools/canvas.py +19 -8
- basic_memory/mcp/tools/chatgpt_tools.py +178 -0
- basic_memory/mcp/tools/delete_note.py +67 -34
- basic_memory/mcp/tools/edit_note.py +55 -39
- basic_memory/mcp/tools/headers.py +44 -0
- basic_memory/mcp/tools/list_directory.py +18 -8
- basic_memory/mcp/tools/move_note.py +119 -41
- basic_memory/mcp/tools/project_management.py +61 -228
- basic_memory/mcp/tools/read_content.py +28 -12
- basic_memory/mcp/tools/read_note.py +83 -46
- basic_memory/mcp/tools/recent_activity.py +441 -42
- basic_memory/mcp/tools/search.py +82 -70
- basic_memory/mcp/tools/sync_status.py +5 -4
- basic_memory/mcp/tools/utils.py +19 -0
- basic_memory/mcp/tools/view_note.py +31 -6
- basic_memory/mcp/tools/write_note.py +65 -14
- basic_memory/models/knowledge.py +12 -6
- basic_memory/models/project.py +6 -2
- basic_memory/repository/entity_repository.py +29 -82
- basic_memory/repository/relation_repository.py +13 -0
- basic_memory/repository/repository.py +2 -2
- basic_memory/repository/search_repository.py +4 -2
- basic_memory/schemas/__init__.py +6 -0
- basic_memory/schemas/base.py +39 -11
- basic_memory/schemas/cloud.py +46 -0
- basic_memory/schemas/memory.py +90 -21
- basic_memory/schemas/project_info.py +9 -10
- basic_memory/schemas/sync_report.py +48 -0
- basic_memory/services/context_service.py +25 -11
- basic_memory/services/entity_service.py +75 -45
- basic_memory/services/initialization.py +30 -11
- basic_memory/services/project_service.py +13 -23
- basic_memory/sync/sync_service.py +145 -21
- basic_memory/sync/watch_service.py +101 -40
- basic_memory/utils.py +14 -4
- {basic_memory-0.14.4.dist-info → basic_memory-0.15.0.dist-info}/METADATA +7 -6
- basic_memory-0.15.0.dist-info/RECORD +147 -0
- basic_memory/mcp/project_session.py +0 -120
- basic_memory-0.14.4.dist-info/RECORD +0 -133
- {basic_memory-0.14.4.dist-info → basic_memory-0.15.0.dist-info}/WHEEL +0 -0
- {basic_memory-0.14.4.dist-info → basic_memory-0.15.0.dist-info}/entry_points.txt +0 -0
- {basic_memory-0.14.4.dist-info → basic_memory-0.15.0.dist-info}/licenses/LICENSE +0 -0
basic_memory/mcp/tools/search.py
CHANGED
|
@@ -4,15 +4,18 @@ from textwrap import dedent
|
|
|
4
4
|
from typing import List, Optional
|
|
5
5
|
|
|
6
6
|
from loguru import logger
|
|
7
|
+
from fastmcp import Context
|
|
7
8
|
|
|
8
9
|
from basic_memory.mcp.async_client import client
|
|
10
|
+
from basic_memory.mcp.project_context import get_active_project
|
|
9
11
|
from basic_memory.mcp.server import mcp
|
|
10
12
|
from basic_memory.mcp.tools.utils import call_post
|
|
11
|
-
from basic_memory.mcp.project_session import get_active_project
|
|
12
13
|
from basic_memory.schemas.search import SearchItemType, SearchQuery, SearchResponse
|
|
13
14
|
|
|
14
15
|
|
|
15
|
-
def _format_search_error_response(
|
|
16
|
+
def _format_search_error_response(
|
|
17
|
+
project: str, error_message: str, query: str, search_type: str = "text"
|
|
18
|
+
) -> str:
|
|
16
19
|
"""Format helpful error responses for search failures that guide users to successful searches."""
|
|
17
20
|
|
|
18
21
|
# FTS5 syntax errors
|
|
@@ -50,13 +53,13 @@ def _format_search_error_response(error_message: str, query: str, search_type: s
|
|
|
50
53
|
|
|
51
54
|
## Try again with:
|
|
52
55
|
```
|
|
53
|
-
search_notes("{clean_query}")
|
|
56
|
+
search_notes("{project}","{clean_query}")
|
|
54
57
|
```
|
|
55
58
|
|
|
56
59
|
## Alternative search strategies:
|
|
57
|
-
- Break into simpler terms: `search_notes("{" ".join(clean_query.split()[:2])}")`
|
|
58
|
-
- Try different search types: `search_notes("{clean_query}", search_type="title")`
|
|
59
|
-
- Use filtering: `search_notes("{clean_query}", types=["entity"])`
|
|
60
|
+
- Break into simpler terms: `search_notes("{project}", "{" ".join(clean_query.split()[:2])}")`
|
|
61
|
+
- Try different search types: `search_notes("{project}","{clean_query}", search_type="title")`
|
|
62
|
+
- Use filtering: `search_notes("{project}","{clean_query}", types=["entity"])`
|
|
60
63
|
""").strip()
|
|
61
64
|
|
|
62
65
|
# Project not found errors (check before general "not found")
|
|
@@ -68,11 +71,9 @@ def _format_search_error_response(error_message: str, query: str, search_type: s
|
|
|
68
71
|
|
|
69
72
|
## How to resolve:
|
|
70
73
|
1. **Check available projects**: `list_projects()`
|
|
71
|
-
2. **Switch to valid project**: `switch_project("valid-project-name")`
|
|
72
74
|
3. **Verify project setup**: Ensure your project is properly configured
|
|
73
75
|
|
|
74
76
|
## Current session info:
|
|
75
|
-
- Check current project: `get_current_project()`
|
|
76
77
|
- See available projects: `list_projects()`
|
|
77
78
|
""").strip()
|
|
78
79
|
|
|
@@ -100,29 +101,28 @@ def _format_search_error_response(error_message: str, query: str, search_type: s
|
|
|
100
101
|
- Try synonyms or related terms
|
|
101
102
|
|
|
102
103
|
3. **Use different search approaches**:
|
|
103
|
-
- **Text search**: `search_notes("{query}", search_type="text")` (searches full content)
|
|
104
|
-
- **Title search**: `search_notes("{query}", search_type="title")` (searches only titles)
|
|
105
|
-
- **Permalink search**: `search_notes("{query}", search_type="permalink")` (searches file paths)
|
|
104
|
+
- **Text search**: `search_notes("{project}","{query}", search_type="text")` (searches full content)
|
|
105
|
+
- **Title search**: `search_notes("{project}","{query}", search_type="title")` (searches only titles)
|
|
106
|
+
- **Permalink search**: `search_notes("{project}","{query}", search_type="permalink")` (searches file paths)
|
|
106
107
|
|
|
107
108
|
4. **Try boolean operators for broader results**:
|
|
108
|
-
- OR search: `search_notes("{" OR ".join(query.split()[:3])}")`
|
|
109
|
+
- OR search: `search_notes("{project}","{" OR ".join(query.split()[:3])}")`
|
|
109
110
|
- Remove restrictive terms: Focus on the most important keywords
|
|
110
111
|
|
|
111
112
|
5. **Use filtering to narrow scope**:
|
|
112
|
-
- By content type: `search_notes("{query}", types=["entity"])`
|
|
113
|
-
- By recent content: `search_notes("{query}", after_date="1 week")`
|
|
114
|
-
- By entity type: `search_notes("{query}", entity_types=["observation"])`
|
|
113
|
+
- By content type: `search_notes("{project}","{query}", types=["entity"])`
|
|
114
|
+
- By recent content: `search_notes("{project}","{query}", after_date="1 week")`
|
|
115
|
+
- By entity type: `search_notes("{project}","{query}", entity_types=["observation"])`
|
|
115
116
|
|
|
116
117
|
6. **Try advanced search patterns**:
|
|
117
|
-
- Tag search: `search_notes("tag:your-tag")`
|
|
118
|
-
- Category search: `search_notes("category:observation")`
|
|
119
|
-
- Pattern matching: `search_notes("*{query}*", search_type="permalink")`
|
|
118
|
+
- Tag search: `search_notes("{project}","tag:your-tag")`
|
|
119
|
+
- Category search: `search_notes("{project}","category:observation")`
|
|
120
|
+
- Pattern matching: `search_notes("{project}","*{query}*", search_type="permalink")`
|
|
120
121
|
|
|
121
122
|
## Explore what content exists:
|
|
122
123
|
- **Recent activity**: `recent_activity(timeframe="7d")` - See what's been updated recently
|
|
123
|
-
- **List directories**: `list_directory("/")` - Browse all content
|
|
124
|
-
- **Browse by folder**: `list_directory("/notes")` or `list_directory("/docs")`
|
|
125
|
-
- **Check project**: `get_current_project()` - Verify you're in the right project
|
|
124
|
+
- **List directories**: `list_directory("{project}","/")` - Browse all content
|
|
125
|
+
- **Browse by folder**: `list_directory("{project}","/notes")` or `list_directory("/docs")`
|
|
126
126
|
""").strip()
|
|
127
127
|
|
|
128
128
|
# Server/API errors
|
|
@@ -138,9 +138,9 @@ def _format_search_error_response(error_message: str, query: str, search_type: s
|
|
|
138
138
|
3. **Check project status**: Ensure your project is properly synced
|
|
139
139
|
|
|
140
140
|
## Alternative approaches:
|
|
141
|
-
- Browse files directly: `list_directory("/")`
|
|
141
|
+
- Browse files directly: `list_directory("{project}","/")`
|
|
142
142
|
- Check recent activity: `recent_activity(timeframe="7d")`
|
|
143
|
-
- Try a different search type: `search_notes("{query}", search_type="title")`
|
|
143
|
+
- Try a different search type: `search_notes("{project}","{query}", search_type="title")`
|
|
144
144
|
|
|
145
145
|
## If the problem persists:
|
|
146
146
|
The search index might need to be rebuilt. Send a message to support@basicmachines.co or check the project sync status.
|
|
@@ -162,9 +162,7 @@ You don't have permission to search in the current project: {error_message}
|
|
|
162
162
|
3. **Check authentication**: You might need to re-authenticate
|
|
163
163
|
|
|
164
164
|
## Alternative actions:
|
|
165
|
-
- List available projects: `list_projects()`
|
|
166
|
-
- Switch to accessible project: `switch_project("project-name")`
|
|
167
|
-
- Check current project: `get_current_project()`"""
|
|
165
|
+
- List available projects: `list_projects()`"""
|
|
168
166
|
|
|
169
167
|
# Generic fallback
|
|
170
168
|
return f"""# Search Failed
|
|
@@ -179,17 +177,16 @@ Error searching for '{query}': {error_message}
|
|
|
179
177
|
|
|
180
178
|
## Alternative search approaches:
|
|
181
179
|
- **Different search types**:
|
|
182
|
-
- Title only: `search_notes("{query}", search_type="title")`
|
|
183
|
-
- Permalink patterns: `search_notes("{query}*", search_type="permalink")`
|
|
184
|
-
- **With filters**: `search_notes("{query}", types=["entity"])`
|
|
185
|
-
- **Recent content**: `search_notes("{query}", after_date="1 week")`
|
|
186
|
-
- **Boolean variations**: `search_notes("{" OR ".join(query.split()[:2])}")`
|
|
180
|
+
- Title only: `search_notes("{project}","{query}", search_type="title")`
|
|
181
|
+
- Permalink patterns: `search_notes("{project}","{query}*", search_type="permalink")`
|
|
182
|
+
- **With filters**: `search_notes("{project}","{query}", types=["entity"])`
|
|
183
|
+
- **Recent content**: `search_notes("{project}","{query}", after_date="1 week")`
|
|
184
|
+
- **Boolean variations**: `search_notes("{project}","{" OR ".join(query.split()[:2])}")`
|
|
187
185
|
|
|
188
186
|
## Explore your content:
|
|
189
|
-
- **Browse files**: `list_directory("/")` - See all available content
|
|
187
|
+
- **Browse files**: `list_directory("{project}","/")` - See all available content
|
|
190
188
|
- **Recent activity**: `recent_activity(timeframe="7d")` - Check what's been updated
|
|
191
|
-
- **
|
|
192
|
-
- **All projects**: `list_projects()` - Switch to different project if needed
|
|
189
|
+
- **All projects**: `list_projects()`
|
|
193
190
|
|
|
194
191
|
## Search syntax reference:
|
|
195
192
|
- **Basic**: `keyword` or `multiple words`
|
|
@@ -204,13 +201,14 @@ Error searching for '{query}': {error_message}
|
|
|
204
201
|
)
|
|
205
202
|
async def search_notes(
|
|
206
203
|
query: str,
|
|
204
|
+
project: Optional[str] = None,
|
|
207
205
|
page: int = 1,
|
|
208
206
|
page_size: int = 10,
|
|
209
207
|
search_type: str = "text",
|
|
210
208
|
types: Optional[List[str]] = None,
|
|
211
209
|
entity_types: Optional[List[str]] = None,
|
|
212
210
|
after_date: Optional[str] = None,
|
|
213
|
-
|
|
211
|
+
context: Context | None = None,
|
|
214
212
|
) -> SearchResponse | str:
|
|
215
213
|
"""Search across all content in the knowledge base with comprehensive syntax support.
|
|
216
214
|
|
|
@@ -218,51 +216,57 @@ async def search_notes(
|
|
|
218
216
|
or exact permalink lookup. It supports filtering by content type, entity type,
|
|
219
217
|
and date, with advanced boolean and phrase search capabilities.
|
|
220
218
|
|
|
219
|
+
Project Resolution:
|
|
220
|
+
Server resolves projects in this order: Single Project Mode → project parameter → default project.
|
|
221
|
+
If project unknown, use list_memory_projects() or recent_activity() first.
|
|
222
|
+
|
|
221
223
|
## Search Syntax Examples
|
|
222
224
|
|
|
223
225
|
### Basic Searches
|
|
224
|
-
- `search_notes("keyword")` - Find any content containing "keyword"
|
|
225
|
-
- `search_notes("exact phrase")` - Search for exact phrase match
|
|
226
|
+
- `search_notes("my-project", "keyword")` - Find any content containing "keyword"
|
|
227
|
+
- `search_notes("work-docs", "'exact phrase'")` - Search for exact phrase match
|
|
226
228
|
|
|
227
229
|
### Advanced Boolean Searches
|
|
228
|
-
- `search_notes("term1 term2")` - Find content with both terms (implicit AND)
|
|
229
|
-
- `search_notes("term1 AND term2")` - Explicit AND search (both terms required)
|
|
230
|
-
- `search_notes("term1 OR term2")` - Either term can be present
|
|
231
|
-
- `search_notes("term1 NOT term2")` - Include term1 but exclude term2
|
|
232
|
-
- `search_notes("(project OR planning) AND notes")` - Grouped boolean logic
|
|
230
|
+
- `search_notes("my-project", "term1 term2")` - Find content with both terms (implicit AND)
|
|
231
|
+
- `search_notes("my-project", "term1 AND term2")` - Explicit AND search (both terms required)
|
|
232
|
+
- `search_notes("my-project", "term1 OR term2")` - Either term can be present
|
|
233
|
+
- `search_notes("my-project", "term1 NOT term2")` - Include term1 but exclude term2
|
|
234
|
+
- `search_notes("my-project", "(project OR planning) AND notes")` - Grouped boolean logic
|
|
233
235
|
|
|
234
236
|
### Content-Specific Searches
|
|
235
|
-
- `search_notes("tag:example")` - Search within specific tags (if supported by content)
|
|
236
|
-
- `search_notes("category:observation")` - Filter by observation categories
|
|
237
|
-
- `search_notes("author:username")` - Find content by author (if metadata available)
|
|
237
|
+
- `search_notes("research", "tag:example")` - Search within specific tags (if supported by content)
|
|
238
|
+
- `search_notes("work-project", "category:observation")` - Filter by observation categories
|
|
239
|
+
- `search_notes("team-docs", "author:username")` - Find content by author (if metadata available)
|
|
238
240
|
|
|
239
241
|
### Search Type Examples
|
|
240
|
-
- `search_notes("Meeting", search_type="title")` - Search only in titles
|
|
241
|
-
- `search_notes("docs/meeting-*", search_type="permalink")` - Pattern match permalinks
|
|
242
|
-
- `search_notes("keyword", search_type="text")` - Full-text search (default)
|
|
242
|
+
- `search_notes("my-project", "Meeting", search_type="title")` - Search only in titles
|
|
243
|
+
- `search_notes("work-docs", "docs/meeting-*", search_type="permalink")` - Pattern match permalinks
|
|
244
|
+
- `search_notes("research", "keyword", search_type="text")` - Full-text search (default)
|
|
243
245
|
|
|
244
246
|
### Filtering Options
|
|
245
|
-
- `search_notes("query", types=["entity"])` - Search only entities
|
|
246
|
-
- `search_notes("query", types=["note", "person"])` - Multiple content types
|
|
247
|
-
- `search_notes("query", entity_types=["observation"])` - Filter by entity type
|
|
248
|
-
- `search_notes("query", after_date="2024-01-01")` - Recent content only
|
|
249
|
-
- `search_notes("query", after_date="1 week")` - Relative date filtering
|
|
247
|
+
- `search_notes("my-project", "query", types=["entity"])` - Search only entities
|
|
248
|
+
- `search_notes("work-docs", "query", types=["note", "person"])` - Multiple content types
|
|
249
|
+
- `search_notes("research", "query", entity_types=["observation"])` - Filter by entity type
|
|
250
|
+
- `search_notes("team-docs", "query", after_date="2024-01-01")` - Recent content only
|
|
251
|
+
- `search_notes("my-project", "query", after_date="1 week")` - Relative date filtering
|
|
250
252
|
|
|
251
253
|
### Advanced Pattern Examples
|
|
252
|
-
- `search_notes("project AND (meeting OR discussion)")` - Complex boolean logic
|
|
253
|
-
- `search_notes("\"exact phrase\" AND keyword")` - Combine phrase and keyword search
|
|
254
|
-
- `search_notes("bug NOT fixed")` - Exclude resolved issues
|
|
255
|
-
- `search_notes("docs/2024-*", search_type="permalink")` - Year-based permalink search
|
|
254
|
+
- `search_notes("work-project", "project AND (meeting OR discussion)")` - Complex boolean logic
|
|
255
|
+
- `search_notes("research", "\"exact phrase\" AND keyword")` - Combine phrase and keyword search
|
|
256
|
+
- `search_notes("dev-notes", "bug NOT fixed")` - Exclude resolved issues
|
|
257
|
+
- `search_notes("archive", "docs/2024-*", search_type="permalink")` - Year-based permalink search
|
|
256
258
|
|
|
257
259
|
Args:
|
|
258
260
|
query: The search query string (supports boolean operators, phrases, patterns)
|
|
261
|
+
project: Project name to search in. Optional - server will resolve using hierarchy.
|
|
262
|
+
If unknown, use list_memory_projects() to discover available projects.
|
|
259
263
|
page: The page number of results to return (default 1)
|
|
260
264
|
page_size: The number of results to return per page (default 10)
|
|
261
265
|
search_type: Type of search to perform, one of: "text", "title", "permalink" (default: "text")
|
|
262
266
|
types: Optional list of note types to search (e.g., ["note", "person"])
|
|
263
267
|
entity_types: Optional list of entity types to filter by (e.g., ["entity", "observation"])
|
|
264
268
|
after_date: Optional date filter for recent content (e.g., "1 week", "2d", "2024-01-01")
|
|
265
|
-
|
|
269
|
+
context: Optional FastMCP context for performance caching.
|
|
266
270
|
|
|
267
271
|
Returns:
|
|
268
272
|
SearchResponse with results and pagination info, or helpful error guidance if search fails
|
|
@@ -288,37 +292,43 @@ async def search_notes(
|
|
|
288
292
|
|
|
289
293
|
# Search with type filter
|
|
290
294
|
results = await search_notes(
|
|
291
|
-
|
|
295
|
+
"meeting notes",
|
|
292
296
|
types=["entity"],
|
|
293
297
|
)
|
|
294
298
|
|
|
295
299
|
# Search with entity type filter
|
|
296
300
|
results = await search_notes(
|
|
297
|
-
|
|
301
|
+
"meeting notes",
|
|
298
302
|
entity_types=["observation"],
|
|
299
303
|
)
|
|
300
304
|
|
|
301
305
|
# Search for recent content
|
|
302
306
|
results = await search_notes(
|
|
303
|
-
|
|
307
|
+
"bug report",
|
|
304
308
|
after_date="1 week"
|
|
305
309
|
)
|
|
306
310
|
|
|
307
311
|
# Pattern matching on permalinks
|
|
308
312
|
results = await search_notes(
|
|
309
|
-
|
|
313
|
+
"docs/meeting-*",
|
|
310
314
|
search_type="permalink"
|
|
311
315
|
)
|
|
312
316
|
|
|
313
|
-
#
|
|
314
|
-
results = await search_notes(
|
|
317
|
+
# Title-only search
|
|
318
|
+
results = await search_notes(
|
|
319
|
+
"Machine Learning",
|
|
320
|
+
search_type="title"
|
|
321
|
+
)
|
|
315
322
|
|
|
316
323
|
# Complex search with multiple filters
|
|
317
324
|
results = await search_notes(
|
|
318
|
-
|
|
325
|
+
"(bug OR issue) AND NOT resolved",
|
|
319
326
|
types=["entity"],
|
|
320
327
|
after_date="2024-01-01"
|
|
321
328
|
)
|
|
329
|
+
|
|
330
|
+
# Explicit project specification
|
|
331
|
+
results = await search_notes("project planning", project="my-project")
|
|
322
332
|
"""
|
|
323
333
|
# Create a SearchQuery object based on the parameters
|
|
324
334
|
search_query = SearchQuery()
|
|
@@ -343,10 +353,10 @@ async def search_notes(
|
|
|
343
353
|
if after_date:
|
|
344
354
|
search_query.after_date = after_date
|
|
345
355
|
|
|
346
|
-
active_project = get_active_project(project)
|
|
356
|
+
active_project = await get_active_project(client, project, context)
|
|
347
357
|
project_url = active_project.project_url
|
|
348
358
|
|
|
349
|
-
logger.info(f"Searching for {search_query}")
|
|
359
|
+
logger.info(f"Searching for {search_query} in project {active_project.name}")
|
|
350
360
|
|
|
351
361
|
try:
|
|
352
362
|
response = await call_post(
|
|
@@ -359,13 +369,15 @@ async def search_notes(
|
|
|
359
369
|
|
|
360
370
|
# Check if we got no results and provide helpful guidance
|
|
361
371
|
if not result.results:
|
|
362
|
-
logger.info(
|
|
372
|
+
logger.info(
|
|
373
|
+
f"Search returned no results for query: {query} in project {active_project.name}"
|
|
374
|
+
)
|
|
363
375
|
# Don't treat this as an error, but the user might want guidance
|
|
364
376
|
# We return the empty result as normal - the user can decide if they need help
|
|
365
377
|
|
|
366
378
|
return result
|
|
367
379
|
|
|
368
380
|
except Exception as e:
|
|
369
|
-
logger.error(f"Search failed for query '{query}': {e}")
|
|
381
|
+
logger.error(f"Search failed for query '{query}': {e}, project: {active_project.name}")
|
|
370
382
|
# Return formatted error message as string for better user experience
|
|
371
|
-
return _format_search_error_response(str(e), query, search_type)
|
|
383
|
+
return _format_search_error_response(active_project.name, str(e), query, search_type)
|
|
@@ -3,10 +3,12 @@
|
|
|
3
3
|
from typing import Optional
|
|
4
4
|
|
|
5
5
|
from loguru import logger
|
|
6
|
+
from fastmcp import Context
|
|
6
7
|
|
|
7
8
|
from basic_memory.config import ConfigManager
|
|
9
|
+
from basic_memory.mcp.async_client import client
|
|
8
10
|
from basic_memory.mcp.server import mcp
|
|
9
|
-
from basic_memory.mcp.
|
|
11
|
+
from basic_memory.mcp.project_context import get_active_project
|
|
10
12
|
from basic_memory.services.sync_status_service import sync_status_tracker
|
|
11
13
|
|
|
12
14
|
|
|
@@ -79,7 +81,7 @@ def _get_all_projects_status() -> list[str]:
|
|
|
79
81
|
- Background processing of knowledge graphs
|
|
80
82
|
""",
|
|
81
83
|
)
|
|
82
|
-
async def sync_status(project: Optional[str] = None) -> str:
|
|
84
|
+
async def sync_status(project: Optional[str] = None, context: Context | None = None) -> str:
|
|
83
85
|
"""Get current sync status and system readiness information.
|
|
84
86
|
|
|
85
87
|
This tool provides detailed information about any ongoing or completed
|
|
@@ -220,14 +222,13 @@ async def sync_status(project: Optional[str] = None) -> str:
|
|
|
220
222
|
[
|
|
221
223
|
"",
|
|
222
224
|
"**Note**: All configured projects will be automatically synced during startup.",
|
|
223
|
-
"You don't need to manually switch projects - Basic Memory handles this for you.",
|
|
224
225
|
]
|
|
225
226
|
)
|
|
226
227
|
|
|
227
228
|
# Add project context if provided
|
|
228
229
|
if project:
|
|
229
230
|
try:
|
|
230
|
-
active_project = get_active_project(project)
|
|
231
|
+
active_project = await get_active_project(client, project, context)
|
|
231
232
|
status_lines.extend(
|
|
232
233
|
[
|
|
233
234
|
"",
|
basic_memory/mcp/tools/utils.py
CHANGED
|
@@ -23,6 +23,8 @@ from httpx._types import (
|
|
|
23
23
|
from loguru import logger
|
|
24
24
|
from mcp.server.fastmcp.exceptions import ToolError
|
|
25
25
|
|
|
26
|
+
from basic_memory.mcp.tools.headers import inject_auth_header
|
|
27
|
+
|
|
26
28
|
|
|
27
29
|
def get_error_message(
|
|
28
30
|
status_code: int, url: URL | str, method: str, msg: Optional[str] = None
|
|
@@ -107,6 +109,8 @@ async def call_get(
|
|
|
107
109
|
"""
|
|
108
110
|
logger.debug(f"Calling GET '{url}' params: '{params}'")
|
|
109
111
|
error_message = None
|
|
112
|
+
|
|
113
|
+
headers = inject_auth_header(headers)
|
|
110
114
|
try:
|
|
111
115
|
response = await client.get(
|
|
112
116
|
url,
|
|
@@ -192,6 +196,9 @@ async def call_put(
|
|
|
192
196
|
logger.debug(f"Calling PUT '{url}'")
|
|
193
197
|
error_message = None
|
|
194
198
|
|
|
199
|
+
# Inject JWT from FastMCP context if available
|
|
200
|
+
headers = inject_auth_header(headers)
|
|
201
|
+
|
|
195
202
|
try:
|
|
196
203
|
response = await client.put(
|
|
197
204
|
url,
|
|
@@ -280,6 +287,10 @@ async def call_patch(
|
|
|
280
287
|
ToolError: If the request fails with an appropriate error message
|
|
281
288
|
"""
|
|
282
289
|
logger.debug(f"Calling PATCH '{url}'")
|
|
290
|
+
|
|
291
|
+
# Inject JWT from FastMCP context if available
|
|
292
|
+
headers = inject_auth_header(headers)
|
|
293
|
+
|
|
283
294
|
try:
|
|
284
295
|
response = await client.patch(
|
|
285
296
|
url,
|
|
@@ -384,6 +395,10 @@ async def call_post(
|
|
|
384
395
|
"""
|
|
385
396
|
logger.debug(f"Calling POST '{url}'")
|
|
386
397
|
error_message = None
|
|
398
|
+
|
|
399
|
+
# Inject JWT from FastMCP context if available
|
|
400
|
+
headers = inject_auth_header(headers)
|
|
401
|
+
|
|
387
402
|
try:
|
|
388
403
|
response = await client.post(
|
|
389
404
|
url=url,
|
|
@@ -465,6 +480,10 @@ async def call_delete(
|
|
|
465
480
|
"""
|
|
466
481
|
logger.debug(f"Calling DELETE '{url}'")
|
|
467
482
|
error_message = None
|
|
483
|
+
|
|
484
|
+
# Inject JWT from FastMCP context if available
|
|
485
|
+
headers = inject_auth_header(headers)
|
|
486
|
+
|
|
468
487
|
try:
|
|
469
488
|
response = await client.delete(
|
|
470
489
|
url=url,
|
|
@@ -4,6 +4,7 @@ from textwrap import dedent
|
|
|
4
4
|
from typing import Optional
|
|
5
5
|
|
|
6
6
|
from loguru import logger
|
|
7
|
+
from fastmcp import Context
|
|
7
8
|
|
|
8
9
|
from basic_memory.mcp.server import mcp
|
|
9
10
|
from basic_memory.mcp.tools.read_note import read_note
|
|
@@ -13,12 +14,17 @@ from basic_memory.mcp.tools.read_note import read_note
|
|
|
13
14
|
description="View a note as a formatted artifact for better readability.",
|
|
14
15
|
)
|
|
15
16
|
async def view_note(
|
|
16
|
-
identifier: str,
|
|
17
|
+
identifier: str,
|
|
18
|
+
project: Optional[str] = None,
|
|
19
|
+
page: int = 1,
|
|
20
|
+
page_size: int = 10,
|
|
21
|
+
context: Context | None = None,
|
|
17
22
|
) -> str:
|
|
18
23
|
"""View a markdown note as a formatted artifact.
|
|
19
24
|
|
|
20
25
|
This tool reads a note using the same logic as read_note but displays the content
|
|
21
26
|
as a markdown artifact for better viewing experience in Claude Desktop.
|
|
27
|
+
Project parameter optional with server resolution.
|
|
22
28
|
|
|
23
29
|
After calling this tool, create an artifact using the returned content to display
|
|
24
30
|
the note in a readable format. The tool returns the note content that should be
|
|
@@ -26,21 +32,40 @@ async def view_note(
|
|
|
26
32
|
|
|
27
33
|
Args:
|
|
28
34
|
identifier: The title or permalink of the note to view
|
|
35
|
+
project: Project name to read from. Optional - server will resolve using hierarchy.
|
|
36
|
+
If unknown, use list_memory_projects() to discover available projects.
|
|
29
37
|
page: Page number for paginated results (default: 1)
|
|
30
38
|
page_size: Number of items per page (default: 10)
|
|
31
|
-
|
|
39
|
+
context: Optional FastMCP context for performance caching.
|
|
32
40
|
|
|
33
41
|
Returns:
|
|
34
42
|
The note content as a markdown artifact with a confirmation message.
|
|
43
|
+
|
|
44
|
+
Examples:
|
|
45
|
+
# View a note by title
|
|
46
|
+
view_note("Meeting Notes")
|
|
47
|
+
|
|
48
|
+
# View a note by permalink
|
|
49
|
+
view_note("meetings/weekly-standup")
|
|
50
|
+
|
|
51
|
+
# View with pagination
|
|
52
|
+
view_note("large-document", page=2, page_size=5)
|
|
53
|
+
|
|
54
|
+
# Explicit project specification
|
|
55
|
+
view_note("Meeting Notes", project="my-project")
|
|
56
|
+
|
|
57
|
+
Raises:
|
|
58
|
+
HTTPError: If project doesn't exist or is inaccessible
|
|
59
|
+
SecurityError: If identifier attempts path traversal
|
|
35
60
|
"""
|
|
36
61
|
|
|
37
|
-
logger.info(f"Viewing note: {identifier}")
|
|
62
|
+
logger.info(f"Viewing note: {identifier} in project: {project}")
|
|
38
63
|
|
|
39
64
|
# Call the existing read_note logic
|
|
40
|
-
content = await read_note.fn(identifier, page, page_size,
|
|
65
|
+
content = await read_note.fn(identifier, project, page, page_size, context)
|
|
41
66
|
|
|
42
67
|
# Check if this is an error message (note not found)
|
|
43
|
-
if "# Note Not Found
|
|
68
|
+
if "# Note Not Found" in content:
|
|
44
69
|
return content # Return error message directly instead of creating artifact
|
|
45
70
|
|
|
46
71
|
# Extract title from content if possible
|
|
@@ -61,6 +86,6 @@ async def view_note(
|
|
|
61
86
|
|
|
62
87
|
return dedent(f"""
|
|
63
88
|
<instructions>
|
|
64
|
-
Create an artifact using the returned
|
|
89
|
+
Create an artifact using the returned content to display the note in a readable format.
|
|
65
90
|
</instructions>
|
|
66
91
|
{artifact}\n\n✅ Note displayed as artifact: **{title}**""")
|
|
@@ -5,10 +5,11 @@ from typing import List, Union, Optional
|
|
|
5
5
|
from loguru import logger
|
|
6
6
|
|
|
7
7
|
from basic_memory.mcp.async_client import client
|
|
8
|
+
from basic_memory.mcp.project_context import get_active_project, add_project_metadata
|
|
8
9
|
from basic_memory.mcp.server import mcp
|
|
9
10
|
from basic_memory.mcp.tools.utils import call_put
|
|
10
|
-
from basic_memory.mcp.project_session import get_active_project
|
|
11
11
|
from basic_memory.schemas import EntityResponse
|
|
12
|
+
from fastmcp import Context
|
|
12
13
|
from basic_memory.schemas.base import Entity
|
|
13
14
|
from basic_memory.utils import parse_tags, validate_project_path
|
|
14
15
|
|
|
@@ -26,14 +27,20 @@ async def write_note(
|
|
|
26
27
|
title: str,
|
|
27
28
|
content: str,
|
|
28
29
|
folder: str,
|
|
29
|
-
tags=None, # Remove type hint completely to avoid schema issues
|
|
30
|
-
entity_type: str = "note",
|
|
31
30
|
project: Optional[str] = None,
|
|
31
|
+
tags=None,
|
|
32
|
+
entity_type: str = "note",
|
|
33
|
+
context: Context | None = None,
|
|
32
34
|
) -> str:
|
|
33
35
|
"""Write a markdown note to the knowledge base.
|
|
34
36
|
|
|
35
|
-
|
|
36
|
-
|
|
37
|
+
Creates or updates a markdown note with semantic observations and relations.
|
|
38
|
+
|
|
39
|
+
Project Resolution:
|
|
40
|
+
Server resolves projects in this order: Single Project Mode → project parameter → default project.
|
|
41
|
+
If project unknown, use list_memory_projects() or recent_activity() first.
|
|
42
|
+
|
|
43
|
+
The content can include semantic observations and relations using markdown syntax:
|
|
37
44
|
|
|
38
45
|
Observations format:
|
|
39
46
|
`- [category] Observation text #tag1 #tag2 (optional context)`
|
|
@@ -50,30 +57,72 @@ async def write_note(
|
|
|
50
57
|
Examples:
|
|
51
58
|
`- depends_on [[Content Parser]] (Need for semantic extraction)`
|
|
52
59
|
`- implements [[Search Spec]] (Initial implementation)`
|
|
53
|
-
`- This feature extends [[Base Design]]
|
|
60
|
+
`- This feature extends [[Base Design]] and uses [[Core Utils]]`
|
|
54
61
|
|
|
55
62
|
Args:
|
|
56
63
|
title: The title of the note
|
|
57
64
|
content: Markdown content for the note, can include observations and relations
|
|
58
65
|
folder: Folder path relative to project root where the file should be saved.
|
|
59
66
|
Use forward slashes (/) as separators. Examples: "notes", "projects/2025", "research/ml"
|
|
67
|
+
project: Project name to write to. Optional - server will resolve using the
|
|
68
|
+
hierarchy above. If unknown, use list_memory_projects() to discover
|
|
69
|
+
available projects.
|
|
60
70
|
tags: Tags to categorize the note. Can be a list of strings, a comma-separated string, or None.
|
|
61
71
|
Note: If passing from external MCP clients, use a string format (e.g. "tag1,tag2,tag3")
|
|
62
72
|
entity_type: Type of entity to create. Defaults to "note". Can be "guide", "report", "config", etc.
|
|
63
|
-
|
|
73
|
+
context: Optional FastMCP context for performance caching.
|
|
64
74
|
|
|
65
75
|
Returns:
|
|
66
76
|
A markdown formatted summary of the semantic content, including:
|
|
67
|
-
- Creation/update status
|
|
77
|
+
- Creation/update status with project name
|
|
68
78
|
- File path and checksum
|
|
69
79
|
- Observation counts by category
|
|
70
80
|
- Relation counts (resolved/unresolved)
|
|
71
81
|
- Tags if present
|
|
82
|
+
- Session tracking metadata for project awareness
|
|
83
|
+
|
|
84
|
+
Examples:
|
|
85
|
+
# Assistant flow when project is unknown
|
|
86
|
+
# 1. list_memory_projects() -> Ask user which project
|
|
87
|
+
# 2. User: "Use my-research"
|
|
88
|
+
# 3. write_note(...) and remember "my-research" for session
|
|
89
|
+
|
|
90
|
+
# Create a simple note
|
|
91
|
+
write_note(
|
|
92
|
+
project="my-research",
|
|
93
|
+
title="Meeting Notes",
|
|
94
|
+
folder="meetings",
|
|
95
|
+
content="# Weekly Standup\\n\\n- [decision] Use SQLite for storage #tech"
|
|
96
|
+
)
|
|
97
|
+
|
|
98
|
+
# Create a note with tags and entity type
|
|
99
|
+
write_note(
|
|
100
|
+
project="work-project",
|
|
101
|
+
title="API Design",
|
|
102
|
+
folder="specs",
|
|
103
|
+
content="# REST API Specification\\n\\n- implements [[Authentication]]",
|
|
104
|
+
tags=["api", "design"],
|
|
105
|
+
entity_type="guide"
|
|
106
|
+
)
|
|
107
|
+
|
|
108
|
+
# Update existing note (same title/folder)
|
|
109
|
+
write_note(
|
|
110
|
+
project="my-research",
|
|
111
|
+
title="Meeting Notes",
|
|
112
|
+
folder="meetings",
|
|
113
|
+
content="# Weekly Standup\\n\\n- [decision] Use PostgreSQL instead #tech"
|
|
114
|
+
)
|
|
115
|
+
|
|
116
|
+
Raises:
|
|
117
|
+
HTTPError: If project doesn't exist or is inaccessible
|
|
118
|
+
SecurityError: If folder path attempts path traversal
|
|
72
119
|
"""
|
|
73
|
-
logger.info(
|
|
120
|
+
logger.info(
|
|
121
|
+
f"MCP tool call tool=write_note project={project} folder={folder}, title={title}, tags={tags}"
|
|
122
|
+
)
|
|
74
123
|
|
|
75
|
-
# Get the
|
|
76
|
-
active_project = get_active_project(project)
|
|
124
|
+
# Get and validate the project (supports optional project parameter)
|
|
125
|
+
active_project = await get_active_project(client, project, context)
|
|
77
126
|
|
|
78
127
|
# Validate folder path to prevent path traversal attacks
|
|
79
128
|
project_path = active_project.home
|
|
@@ -104,7 +153,7 @@ async def write_note(
|
|
|
104
153
|
content=content,
|
|
105
154
|
entity_metadata=metadata,
|
|
106
155
|
)
|
|
107
|
-
project_url = active_project.
|
|
156
|
+
project_url = active_project.permalink
|
|
108
157
|
|
|
109
158
|
# Create or update via knowledge API
|
|
110
159
|
logger.debug(f"Creating entity via API permalink={entity.permalink}")
|
|
@@ -116,6 +165,7 @@ async def write_note(
|
|
|
116
165
|
action = "Created" if response.status_code == 201 else "Updated"
|
|
117
166
|
summary = [
|
|
118
167
|
f"# {action} note",
|
|
168
|
+
f"project: {active_project.name}",
|
|
119
169
|
f"file_path: {result.file_path}",
|
|
120
170
|
f"permalink: {result.permalink}",
|
|
121
171
|
f"checksum: {result.checksum[:8] if result.checksum else 'unknown'}",
|
|
@@ -152,6 +202,7 @@ async def write_note(
|
|
|
152
202
|
|
|
153
203
|
# Log the response with structured data
|
|
154
204
|
logger.info(
|
|
155
|
-
f"MCP tool response: tool=write_note action={action} permalink={result.permalink} observations_count={len(result.observations)} relations_count={len(result.relations)} resolved_relations={resolved} unresolved_relations={unresolved} status_code={response.status_code}"
|
|
205
|
+
f"MCP tool response: tool=write_note project={active_project.name} action={action} permalink={result.permalink} observations_count={len(result.observations)} relations_count={len(result.relations)} resolved_relations={resolved} unresolved_relations={unresolved} status_code={response.status_code}"
|
|
156
206
|
)
|
|
157
|
-
|
|
207
|
+
result = "\n".join(summary)
|
|
208
|
+
return add_project_metadata(result, active_project.name)
|