basic-memory 0.14.3__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.

Files changed (90) hide show
  1. basic_memory/__init__.py +1 -1
  2. basic_memory/alembic/versions/a1b2c3d4e5f6_fix_project_foreign_keys.py +49 -0
  3. basic_memory/api/app.py +10 -4
  4. basic_memory/api/routers/knowledge_router.py +25 -8
  5. basic_memory/api/routers/project_router.py +99 -4
  6. basic_memory/api/routers/resource_router.py +3 -3
  7. basic_memory/cli/app.py +9 -28
  8. basic_memory/cli/auth.py +277 -0
  9. basic_memory/cli/commands/cloud/__init__.py +5 -0
  10. basic_memory/cli/commands/cloud/api_client.py +112 -0
  11. basic_memory/cli/commands/cloud/bisync_commands.py +818 -0
  12. basic_memory/cli/commands/cloud/core_commands.py +288 -0
  13. basic_memory/cli/commands/cloud/mount_commands.py +295 -0
  14. basic_memory/cli/commands/cloud/rclone_config.py +288 -0
  15. basic_memory/cli/commands/cloud/rclone_installer.py +198 -0
  16. basic_memory/cli/commands/command_utils.py +60 -0
  17. basic_memory/cli/commands/import_memory_json.py +0 -4
  18. basic_memory/cli/commands/mcp.py +16 -4
  19. basic_memory/cli/commands/project.py +141 -145
  20. basic_memory/cli/commands/status.py +34 -22
  21. basic_memory/cli/commands/sync.py +45 -228
  22. basic_memory/cli/commands/tool.py +87 -16
  23. basic_memory/cli/main.py +1 -0
  24. basic_memory/config.py +96 -20
  25. basic_memory/db.py +104 -3
  26. basic_memory/deps.py +20 -3
  27. basic_memory/file_utils.py +89 -0
  28. basic_memory/ignore_utils.py +295 -0
  29. basic_memory/importers/chatgpt_importer.py +1 -1
  30. basic_memory/importers/utils.py +2 -2
  31. basic_memory/markdown/entity_parser.py +2 -2
  32. basic_memory/markdown/markdown_processor.py +2 -2
  33. basic_memory/markdown/plugins.py +39 -21
  34. basic_memory/markdown/utils.py +1 -1
  35. basic_memory/mcp/async_client.py +22 -10
  36. basic_memory/mcp/project_context.py +141 -0
  37. basic_memory/mcp/prompts/ai_assistant_guide.py +49 -4
  38. basic_memory/mcp/prompts/continue_conversation.py +1 -1
  39. basic_memory/mcp/prompts/recent_activity.py +116 -32
  40. basic_memory/mcp/prompts/search.py +1 -1
  41. basic_memory/mcp/prompts/utils.py +11 -4
  42. basic_memory/mcp/resources/ai_assistant_guide.md +179 -41
  43. basic_memory/mcp/resources/project_info.py +20 -6
  44. basic_memory/mcp/server.py +0 -37
  45. basic_memory/mcp/tools/__init__.py +5 -6
  46. basic_memory/mcp/tools/build_context.py +39 -19
  47. basic_memory/mcp/tools/canvas.py +19 -8
  48. basic_memory/mcp/tools/chatgpt_tools.py +178 -0
  49. basic_memory/mcp/tools/delete_note.py +67 -34
  50. basic_memory/mcp/tools/edit_note.py +55 -39
  51. basic_memory/mcp/tools/headers.py +44 -0
  52. basic_memory/mcp/tools/list_directory.py +18 -8
  53. basic_memory/mcp/tools/move_note.py +119 -41
  54. basic_memory/mcp/tools/project_management.py +77 -229
  55. basic_memory/mcp/tools/read_content.py +28 -12
  56. basic_memory/mcp/tools/read_note.py +97 -57
  57. basic_memory/mcp/tools/recent_activity.py +441 -42
  58. basic_memory/mcp/tools/search.py +82 -70
  59. basic_memory/mcp/tools/sync_status.py +5 -4
  60. basic_memory/mcp/tools/utils.py +19 -0
  61. basic_memory/mcp/tools/view_note.py +31 -6
  62. basic_memory/mcp/tools/write_note.py +65 -14
  63. basic_memory/models/knowledge.py +19 -2
  64. basic_memory/models/project.py +6 -2
  65. basic_memory/repository/entity_repository.py +31 -84
  66. basic_memory/repository/project_repository.py +1 -1
  67. basic_memory/repository/relation_repository.py +13 -0
  68. basic_memory/repository/repository.py +2 -2
  69. basic_memory/repository/search_repository.py +9 -3
  70. basic_memory/schemas/__init__.py +6 -0
  71. basic_memory/schemas/base.py +70 -12
  72. basic_memory/schemas/cloud.py +46 -0
  73. basic_memory/schemas/memory.py +99 -18
  74. basic_memory/schemas/project_info.py +9 -10
  75. basic_memory/schemas/sync_report.py +48 -0
  76. basic_memory/services/context_service.py +35 -11
  77. basic_memory/services/directory_service.py +7 -0
  78. basic_memory/services/entity_service.py +82 -52
  79. basic_memory/services/initialization.py +30 -11
  80. basic_memory/services/project_service.py +23 -33
  81. basic_memory/sync/sync_service.py +148 -24
  82. basic_memory/sync/watch_service.py +128 -44
  83. basic_memory/utils.py +181 -109
  84. {basic_memory-0.14.3.dist-info → basic_memory-0.15.0.dist-info}/METADATA +26 -96
  85. basic_memory-0.15.0.dist-info/RECORD +147 -0
  86. basic_memory/mcp/project_session.py +0 -120
  87. basic_memory-0.14.3.dist-info/RECORD +0 -132
  88. {basic_memory-0.14.3.dist-info → basic_memory-0.15.0.dist-info}/WHEEL +0 -0
  89. {basic_memory-0.14.3.dist-info → basic_memory-0.15.0.dist-info}/entry_points.txt +0 -0
  90. {basic_memory-0.14.3.dist-info → basic_memory-0.15.0.dist-info}/licenses/LICENSE +0 -0
@@ -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(error_message: str, query: str, search_type: str = "text") -> str:
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
- - **Project info**: `get_current_project()` - Verify current project
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
- project: Optional[str] = None,
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
- project: Optional project name to search in. If not provided, uses current active project.
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
- query="meeting notes",
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
- query="meeting notes",
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
- query="bug report",
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
- query="docs/meeting-*",
313
+ "docs/meeting-*",
310
314
  search_type="permalink"
311
315
  )
312
316
 
313
- # Search in specific project
314
- results = await search_notes("meeting notes", project="work-project")
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
- query="(bug OR issue) AND NOT resolved",
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(f"Search returned no results for query: {query}")
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.project_session import get_active_project
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
  "",
@@ -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, page: int = 1, page_size: int = 10, project: Optional[str] = None
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
- project: Optional project name to read from. If not provided, uses current active project.
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, project)
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:" in content:
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 artifact content to display the note in a readable format.
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
- The content can include semantic observations and relations using markdown syntax.
36
- Relations can be specified either explicitly or through inline wiki-style links:
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]] andst uses [[Core Utils]]`
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
- project: Optional project name to write to. If not provided, uses current active project.
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(f"MCP tool call tool=write_note folder={folder}, title={title}, tags={tags}")
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 active project first to check project-specific sync status
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.project_url
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
- return "\n".join(summary)
207
+ result = "\n".join(summary)
208
+ return add_project_metadata(result, active_project.name)