basic-memory 0.12.2__py3-none-any.whl → 0.13.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 (117) hide show
  1. basic_memory/__init__.py +2 -1
  2. basic_memory/alembic/env.py +1 -1
  3. basic_memory/alembic/versions/5fe1ab1ccebe_add_projects_table.py +108 -0
  4. basic_memory/alembic/versions/647e7a75e2cd_project_constraint_fix.py +104 -0
  5. basic_memory/alembic/versions/cc7172b46608_update_search_index_schema.py +0 -6
  6. basic_memory/api/app.py +43 -13
  7. basic_memory/api/routers/__init__.py +4 -2
  8. basic_memory/api/routers/directory_router.py +63 -0
  9. basic_memory/api/routers/importer_router.py +152 -0
  10. basic_memory/api/routers/knowledge_router.py +139 -37
  11. basic_memory/api/routers/management_router.py +78 -0
  12. basic_memory/api/routers/memory_router.py +6 -62
  13. basic_memory/api/routers/project_router.py +234 -0
  14. basic_memory/api/routers/prompt_router.py +260 -0
  15. basic_memory/api/routers/search_router.py +3 -21
  16. basic_memory/api/routers/utils.py +130 -0
  17. basic_memory/api/template_loader.py +292 -0
  18. basic_memory/cli/app.py +20 -21
  19. basic_memory/cli/commands/__init__.py +2 -1
  20. basic_memory/cli/commands/auth.py +136 -0
  21. basic_memory/cli/commands/db.py +3 -3
  22. basic_memory/cli/commands/import_chatgpt.py +31 -207
  23. basic_memory/cli/commands/import_claude_conversations.py +16 -142
  24. basic_memory/cli/commands/import_claude_projects.py +33 -143
  25. basic_memory/cli/commands/import_memory_json.py +26 -83
  26. basic_memory/cli/commands/mcp.py +71 -18
  27. basic_memory/cli/commands/project.py +102 -70
  28. basic_memory/cli/commands/status.py +19 -9
  29. basic_memory/cli/commands/sync.py +44 -58
  30. basic_memory/cli/commands/tool.py +6 -6
  31. basic_memory/cli/main.py +1 -5
  32. basic_memory/config.py +143 -87
  33. basic_memory/db.py +6 -4
  34. basic_memory/deps.py +227 -30
  35. basic_memory/importers/__init__.py +27 -0
  36. basic_memory/importers/base.py +79 -0
  37. basic_memory/importers/chatgpt_importer.py +222 -0
  38. basic_memory/importers/claude_conversations_importer.py +172 -0
  39. basic_memory/importers/claude_projects_importer.py +148 -0
  40. basic_memory/importers/memory_json_importer.py +93 -0
  41. basic_memory/importers/utils.py +58 -0
  42. basic_memory/markdown/entity_parser.py +5 -2
  43. basic_memory/mcp/auth_provider.py +270 -0
  44. basic_memory/mcp/external_auth_provider.py +321 -0
  45. basic_memory/mcp/project_session.py +103 -0
  46. basic_memory/mcp/prompts/__init__.py +2 -0
  47. basic_memory/mcp/prompts/continue_conversation.py +18 -68
  48. basic_memory/mcp/prompts/recent_activity.py +20 -4
  49. basic_memory/mcp/prompts/search.py +14 -140
  50. basic_memory/mcp/prompts/sync_status.py +116 -0
  51. basic_memory/mcp/prompts/utils.py +3 -3
  52. basic_memory/mcp/{tools → resources}/project_info.py +6 -2
  53. basic_memory/mcp/server.py +86 -13
  54. basic_memory/mcp/supabase_auth_provider.py +463 -0
  55. basic_memory/mcp/tools/__init__.py +24 -0
  56. basic_memory/mcp/tools/build_context.py +43 -8
  57. basic_memory/mcp/tools/canvas.py +17 -3
  58. basic_memory/mcp/tools/delete_note.py +168 -5
  59. basic_memory/mcp/tools/edit_note.py +303 -0
  60. basic_memory/mcp/tools/list_directory.py +154 -0
  61. basic_memory/mcp/tools/move_note.py +299 -0
  62. basic_memory/mcp/tools/project_management.py +332 -0
  63. basic_memory/mcp/tools/read_content.py +15 -6
  64. basic_memory/mcp/tools/read_note.py +28 -9
  65. basic_memory/mcp/tools/recent_activity.py +47 -16
  66. basic_memory/mcp/tools/search.py +189 -8
  67. basic_memory/mcp/tools/sync_status.py +254 -0
  68. basic_memory/mcp/tools/utils.py +184 -12
  69. basic_memory/mcp/tools/view_note.py +66 -0
  70. basic_memory/mcp/tools/write_note.py +24 -17
  71. basic_memory/models/__init__.py +3 -2
  72. basic_memory/models/knowledge.py +16 -4
  73. basic_memory/models/project.py +78 -0
  74. basic_memory/models/search.py +8 -5
  75. basic_memory/repository/__init__.py +2 -0
  76. basic_memory/repository/entity_repository.py +8 -3
  77. basic_memory/repository/observation_repository.py +35 -3
  78. basic_memory/repository/project_info_repository.py +3 -2
  79. basic_memory/repository/project_repository.py +85 -0
  80. basic_memory/repository/relation_repository.py +8 -2
  81. basic_memory/repository/repository.py +107 -15
  82. basic_memory/repository/search_repository.py +192 -54
  83. basic_memory/schemas/__init__.py +6 -0
  84. basic_memory/schemas/base.py +33 -5
  85. basic_memory/schemas/directory.py +30 -0
  86. basic_memory/schemas/importer.py +34 -0
  87. basic_memory/schemas/memory.py +84 -13
  88. basic_memory/schemas/project_info.py +112 -2
  89. basic_memory/schemas/prompt.py +90 -0
  90. basic_memory/schemas/request.py +56 -2
  91. basic_memory/schemas/search.py +1 -1
  92. basic_memory/services/__init__.py +2 -1
  93. basic_memory/services/context_service.py +208 -95
  94. basic_memory/services/directory_service.py +167 -0
  95. basic_memory/services/entity_service.py +399 -6
  96. basic_memory/services/exceptions.py +6 -0
  97. basic_memory/services/file_service.py +14 -15
  98. basic_memory/services/initialization.py +170 -66
  99. basic_memory/services/link_resolver.py +35 -12
  100. basic_memory/services/migration_service.py +168 -0
  101. basic_memory/services/project_service.py +671 -0
  102. basic_memory/services/search_service.py +77 -2
  103. basic_memory/services/sync_status_service.py +181 -0
  104. basic_memory/sync/background_sync.py +25 -0
  105. basic_memory/sync/sync_service.py +102 -21
  106. basic_memory/sync/watch_service.py +63 -39
  107. basic_memory/templates/prompts/continue_conversation.hbs +110 -0
  108. basic_memory/templates/prompts/search.hbs +101 -0
  109. basic_memory/utils.py +67 -17
  110. {basic_memory-0.12.2.dist-info → basic_memory-0.13.0.dist-info}/METADATA +26 -4
  111. basic_memory-0.13.0.dist-info/RECORD +138 -0
  112. basic_memory/api/routers/project_info_router.py +0 -274
  113. basic_memory/mcp/main.py +0 -24
  114. basic_memory-0.12.2.dist-info/RECORD +0 -100
  115. {basic_memory-0.12.2.dist-info → basic_memory-0.13.0.dist-info}/WHEEL +0 -0
  116. {basic_memory-0.12.2.dist-info → basic_memory-0.13.0.dist-info}/entry_points.txt +0 -0
  117. {basic_memory-0.12.2.dist-info → basic_memory-0.13.0.dist-info}/licenses/LICENSE +0 -0
@@ -10,10 +10,12 @@ from basic_memory.mcp.prompts import continue_conversation
10
10
  from basic_memory.mcp.prompts import recent_activity
11
11
  from basic_memory.mcp.prompts import search
12
12
  from basic_memory.mcp.prompts import ai_assistant_guide
13
+ from basic_memory.mcp.prompts import sync_status
13
14
 
14
15
  __all__ = [
15
16
  "ai_assistant_guide",
16
17
  "continue_conversation",
17
18
  "recent_activity",
18
19
  "search",
20
+ "sync_status",
19
21
  ]
@@ -4,20 +4,17 @@ These prompts help users continue conversations and work across sessions,
4
4
  providing context from previous interactions to maintain continuity.
5
5
  """
6
6
 
7
- from textwrap import dedent
8
7
  from typing import Annotated, Optional
9
8
 
10
9
  from loguru import logger
11
10
  from pydantic import Field
12
11
 
13
- from basic_memory.mcp.prompts.utils import PromptContext, PromptContextItem, format_prompt_context
12
+ from basic_memory.config import get_project_config
13
+ from basic_memory.mcp.async_client import client
14
14
  from basic_memory.mcp.server import mcp
15
- from basic_memory.mcp.tools.build_context import build_context
16
- from basic_memory.mcp.tools.recent_activity import recent_activity
17
- from basic_memory.mcp.tools.search import search_notes
15
+ from basic_memory.mcp.tools.utils import call_post
18
16
  from basic_memory.schemas.base import TimeFrame
19
- from basic_memory.schemas.memory import GraphContext
20
- from basic_memory.schemas.search import SearchItemType
17
+ from basic_memory.schemas.prompt import ContinueConversationRequest
21
18
 
22
19
 
23
20
  @mcp.prompt(
@@ -45,67 +42,20 @@ async def continue_conversation(
45
42
  """
46
43
  logger.info(f"Continuing session, topic: {topic}, timeframe: {timeframe}")
47
44
 
48
- # If topic provided, search for it
49
- if topic:
50
- search_results = await search_notes(
51
- query=topic, after_date=timeframe, entity_types=[SearchItemType.ENTITY]
52
- )
45
+ # Create request model
46
+ request = ContinueConversationRequest( # pyright: ignore [reportCallIssue]
47
+ topic=topic, timeframe=timeframe
48
+ )
53
49
 
54
- # Build context from results
55
- contexts = []
56
- for result in search_results.results:
57
- if hasattr(result, "permalink") and result.permalink:
58
- context: GraphContext = await build_context(f"memory://{result.permalink}")
59
- if context.primary_results:
60
- contexts.append(
61
- PromptContextItem(
62
- primary_results=context.primary_results[:1], # pyright: ignore
63
- related_results=context.related_results[:3], # pyright: ignore
64
- )
65
- )
50
+ project_url = get_project_config().project_url
66
51
 
67
- # get context for the top 3 results
68
- prompt_context = format_prompt_context(
69
- PromptContext(topic=topic, timeframe=timeframe, results=contexts) # pyright: ignore
70
- )
52
+ # Call the prompt API endpoint
53
+ response = await call_post(
54
+ client,
55
+ f"{project_url}/prompt/continue-conversation",
56
+ json=request.model_dump(exclude_none=True),
57
+ )
71
58
 
72
- else:
73
- # If no topic, get recent activity
74
- timeframe = timeframe or "7d"
75
- recent: GraphContext = await recent_activity(
76
- timeframe=timeframe, type=[SearchItemType.ENTITY]
77
- )
78
- prompt_context = format_prompt_context(
79
- PromptContext(
80
- topic=f"Recent Activity from ({timeframe})",
81
- timeframe=timeframe,
82
- results=[
83
- PromptContextItem(
84
- primary_results=recent.primary_results[:5], # pyright: ignore
85
- related_results=recent.related_results[:2], # pyright: ignore
86
- )
87
- ],
88
- )
89
- )
90
-
91
- # Add next steps with strong encouragement to write
92
- next_steps = dedent(f"""
93
- ## Next Steps
94
-
95
- You can:
96
- - Explore more with: `search_notes({{"text": "{topic}"}})`
97
- - See what's changed: `recent_activity(timeframe="{timeframe or "7d"}")`
98
- - **Record new learnings or decisions from this conversation:** `write_note(title="[Create a meaningful title]", content="[Content with observations and relations]")`
99
-
100
- ## Knowledge Capture Recommendation
101
-
102
- As you continue this conversation, **actively look for opportunities to:**
103
- 1. Record key information, decisions, or insights that emerge
104
- 2. Link new knowledge to existing topics
105
- 3. Suggest capturing important context when appropriate
106
- 4. Create forward references to topics that might be created later
107
-
108
- Remember that capturing knowledge during conversations is one of the most valuable aspects of Basic Memory.
109
- """)
110
-
111
- return prompt_context + next_steps
59
+ # Extract the rendered prompt from the response
60
+ result = response.json()
61
+ return result["prompt"]
@@ -38,7 +38,19 @@ async def recent_activity_prompt(
38
38
  """
39
39
  logger.info(f"Getting recent activity, timeframe: {timeframe}")
40
40
 
41
- recent = await recent_activity(timeframe=timeframe, type=[SearchItemType.ENTITY])
41
+ recent = await recent_activity.fn(timeframe=timeframe, type=[SearchItemType.ENTITY])
42
+
43
+ # Extract primary results from the hierarchical structure
44
+ primary_results = []
45
+ related_results = []
46
+
47
+ if recent.results:
48
+ # Take up to 5 primary results
49
+ for item in recent.results[:5]:
50
+ primary_results.append(item.primary_result)
51
+ # Add up to 2 related results per primary item
52
+ if item.related_results:
53
+ related_results.extend(item.related_results[:2])
42
54
 
43
55
  prompt_context = format_prompt_context(
44
56
  PromptContext(
@@ -46,14 +58,18 @@ async def recent_activity_prompt(
46
58
  timeframe=timeframe,
47
59
  results=[
48
60
  PromptContextItem(
49
- primary_results=recent.primary_results[:5],
50
- related_results=recent.related_results[:2],
61
+ primary_results=primary_results,
62
+ related_results=related_results[:10], # Limit total related results
51
63
  )
52
64
  ],
53
65
  )
54
66
  )
55
67
 
56
68
  # Add suggestions for summarizing recent activity
69
+ first_title = "Recent Topic"
70
+ if primary_results and len(primary_results) > 0:
71
+ first_title = primary_results[0].title
72
+
57
73
  capture_suggestions = f"""
58
74
  ## Opportunity to Capture Activity Summary
59
75
 
@@ -76,7 +92,7 @@ async def recent_activity_prompt(
76
92
  - [insight] [Connection between different activities]
77
93
 
78
94
  ## Relations
79
- - summarizes [[{recent.primary_results[0].title if recent.primary_results else "Recent Topic"}]]
95
+ - summarizes [[{first_title}]]
80
96
  - relates_to [[Project Overview]]
81
97
  '''
82
98
  )
@@ -3,16 +3,17 @@
3
3
  These prompts help users search and explore their knowledge base.
4
4
  """
5
5
 
6
- from textwrap import dedent
7
6
  from typing import Annotated, Optional
8
7
 
9
8
  from loguru import logger
10
9
  from pydantic import Field
11
10
 
11
+ from basic_memory.config import get_project_config
12
+ from basic_memory.mcp.async_client import client
12
13
  from basic_memory.mcp.server import mcp
13
- from basic_memory.mcp.tools.search import search_notes as search_tool
14
+ from basic_memory.mcp.tools.utils import call_post
14
15
  from basic_memory.schemas.base import TimeFrame
15
- from basic_memory.schemas.search import SearchResponse
16
+ from basic_memory.schemas.prompt import SearchPromptRequest
16
17
 
17
18
 
18
19
  @mcp.prompt(
@@ -40,143 +41,16 @@ async def search_prompt(
40
41
  """
41
42
  logger.info(f"Searching knowledge base, query: {query}, timeframe: {timeframe}")
42
43
 
43
- search_results = await search_tool(query=query, after_date=timeframe)
44
- return format_search_results(query, search_results, timeframe)
44
+ # Create request model
45
+ request = SearchPromptRequest(query=query, timeframe=timeframe)
45
46
 
47
+ project_url = get_project_config().project_url
46
48
 
47
- def format_search_results(
48
- query: str, results: SearchResponse, timeframe: Optional[TimeFrame] = None
49
- ) -> str:
50
- """Format search results into a helpful summary.
51
-
52
- Args:
53
- query: The search query
54
- results: Search results object
55
- timeframe: How far back results were searched
56
-
57
- Returns:
58
- Formatted search results summary
59
- """
60
- if not results.results:
61
- return dedent(f"""
62
- # Search Results for: "{query}"
63
-
64
- I couldn't find any results for this query.
65
-
66
- ## Opportunity to Capture Knowledge!
67
-
68
- This is an excellent opportunity to create new knowledge on this topic. Consider:
69
-
70
- ```python
71
- await write_note(
72
- title="{query.capitalize()}",
73
- content=f'''
74
- # {query.capitalize()}
75
-
76
- ## Overview
77
- [Summary of what we've discussed about {query}]
78
-
79
- ## Observations
80
- - [category] [First observation about {query}]
81
- - [category] [Second observation about {query}]
82
-
83
- ## Relations
84
- - relates_to [[Other Relevant Topic]]
85
- '''
86
- )
87
- ```
88
-
89
- ## Other Suggestions
90
- - Try a different search term
91
- - Broaden your search criteria
92
- - Check recent activity with `recent_activity(timeframe="1w")`
93
- """)
94
-
95
- # Start building our summary with header
96
- time_info = f" (after {timeframe})" if timeframe else ""
97
- summary = dedent(f"""
98
- # Search Results for: "{query}"{time_info}
99
-
100
- This is a memory search session.
101
- Please use the available basic-memory tools to gather relevant context before responding.
102
- I found {len(results.results)} results that match your query.
103
-
104
- Here are the most relevant results:
105
- """)
106
-
107
- # Add each search result
108
- for i, result in enumerate(results.results[:5]): # Limit to top 5 results
109
- summary += dedent(f"""
110
- ## {i + 1}. {result.title}
111
- - **Type**: {result.type.value}
112
- """)
113
-
114
- # Add creation date if available in metadata
115
- if result.metadata and "created_at" in result.metadata:
116
- created_at = result.metadata["created_at"]
117
- if hasattr(created_at, "strftime"):
118
- summary += (
119
- f"- **Created**: {created_at.strftime('%Y-%m-%d %H:%M')}\n" # pragma: no cover
120
- )
121
- elif isinstance(created_at, str):
122
- summary += f"- **Created**: {created_at}\n"
123
-
124
- # Add score and excerpt
125
- summary += f"- **Relevance Score**: {result.score:.2f}\n"
126
-
127
- # Add excerpt if available in metadata
128
- if result.content:
129
- summary += f"- **Excerpt**:\n{result.content}\n"
130
-
131
- # Add permalink for retrieving content
132
- if result.permalink:
133
- summary += dedent(f"""
134
- You can view this content with: `read_note("{result.permalink}")`
135
- Or explore its context with: `build_context("memory://{result.permalink}")`
136
- """)
137
- else:
138
- summary += dedent(f"""
139
- You can view this file with: `read_file("{result.file_path}")`
140
- """) # pragma: no cover
141
-
142
- # Add next steps with strong write encouragement
143
- summary += dedent(f"""
144
- ## Next Steps
145
-
146
- You can:
147
- - Refine your search: `search_notes("{query} AND additional_term")`
148
- - Exclude terms: `search_notes("{query} NOT exclude_term")`
149
- - View more results: `search_notes("{query}", after_date=None)`
150
- - Check recent activity: `recent_activity()`
151
-
152
- ## Synthesize and Capture Knowledge
153
-
154
- Consider creating a new note that synthesizes what you've learned:
155
-
156
- ```python
157
- await write_note(
158
- title="Synthesis of {query.capitalize()} Information",
159
- content='''
160
- # Synthesis of {query.capitalize()} Information
161
-
162
- ## Overview
163
- [Synthesis of the search results and your conversation]
164
-
165
- ## Key Insights
166
- [Summary of main points learned from these results]
167
-
168
- ## Observations
169
- - [insight] [Important observation from search results]
170
- - [connection] [How this connects to other topics]
171
-
172
- ## Relations
173
- - relates_to [[{results.results[0].title if results.results else "Related Topic"}]]
174
- - extends [[Another Relevant Topic]]
175
- '''
176
- )
177
- ```
178
-
179
- Remember that capturing synthesized knowledge is one of the most valuable features of Basic Memory.
180
- """)
49
+ # Call the prompt API endpoint
50
+ response = await call_post(
51
+ client, f"{project_url}/prompt/search", json=request.model_dump(exclude_none=True)
52
+ )
181
53
 
182
- return summary
54
+ # Extract the rendered prompt from the response
55
+ result = response.json()
56
+ return result["prompt"]
@@ -0,0 +1,116 @@
1
+ """Sync status prompt for Basic Memory MCP server."""
2
+
3
+ from basic_memory.mcp.server import mcp
4
+
5
+
6
+ @mcp.prompt(
7
+ description="""Get sync status with recommendations for AI assistants.
8
+
9
+ This prompt provides both current sync status and guidance on how
10
+ AI assistants should respond when sync operations are in progress or completed.
11
+ """,
12
+ )
13
+ async def sync_status_prompt() -> str:
14
+ """Get sync status with AI assistant guidance.
15
+
16
+ This prompt provides detailed sync status information along with
17
+ recommendations for how AI assistants should handle different sync states.
18
+
19
+ Returns:
20
+ Formatted sync status with AI assistant guidance
21
+ """
22
+ try: # pragma: no cover
23
+ from basic_memory.services.migration_service import migration_manager
24
+
25
+ state = migration_manager.state
26
+
27
+ # Build status report
28
+ lines = [
29
+ "# Basic Memory Sync Status",
30
+ "",
31
+ f"**Current Status**: {state.status.value.replace('_', ' ').title()}",
32
+ f"**System Ready**: {'Yes' if migration_manager.is_ready else 'No'}",
33
+ "",
34
+ ]
35
+
36
+ if migration_manager.is_ready:
37
+ lines.extend(
38
+ [
39
+ "✅ **All sync operations completed** - System is fully operational",
40
+ "",
41
+ "All Basic Memory tools are available and functioning normally.",
42
+ "File indexing is complete and knowledge graphs are up to date.",
43
+ "You can proceed with any knowledge management tasks.",
44
+ ]
45
+ )
46
+ else:
47
+ lines.append(f"**Status Message**: {state.message}")
48
+
49
+ if state.status.value == "in_progress":
50
+ if state.projects_total > 0:
51
+ progress = f" ({state.projects_migrated}/{state.projects_total})"
52
+ lines.append(f"**Progress**: {progress}")
53
+
54
+ lines.extend(
55
+ [
56
+ "",
57
+ "🔄 **File synchronization in progress** - Processing files and building knowledge graphs",
58
+ "",
59
+ "**Impact**: Some tools may show status messages instead of normal responses",
60
+ "until sync completes (usually 1-3 minutes).",
61
+ ]
62
+ )
63
+
64
+ elif state.status.value == "failed":
65
+ lines.extend(
66
+ [
67
+ "",
68
+ f"❌ **Sync failed**: {state.error or 'Unknown error'}",
69
+ "",
70
+ "**Impact**: System may have limited functionality until issue is resolved.",
71
+ ]
72
+ )
73
+
74
+ # Add AI assistant recommendations
75
+ if not migration_manager.is_ready:
76
+ lines.extend(
77
+ [
78
+ "",
79
+ "---",
80
+ "",
81
+ "## AI Assistant Recommendations",
82
+ "",
83
+ "**When sync is in progress:**",
84
+ "- Inform the user about the background file processing",
85
+ "- Suggest using `sync_status()` tool to check progress",
86
+ "- Explain that tools will work normally once sync completes",
87
+ "- Avoid creating complex workflows until sync is done",
88
+ "",
89
+ "**What to tell users:**",
90
+ "- 'Basic Memory is processing your files and building knowledge graphs'",
91
+ "- 'This usually takes 1-3 minutes depending on your content size'",
92
+ "- 'You can check progress anytime with the sync_status tool'",
93
+ "- 'Full functionality will be available once processing completes'",
94
+ "",
95
+ "**User-friendly language:**",
96
+ "- Say 'processing files' instead of 'migration' or 'sync'",
97
+ "- Say 'building knowledge graphs' instead of 'indexing'",
98
+ "- Say 'setting up your knowledge base' instead of 'running migrations'",
99
+ ]
100
+ )
101
+
102
+ return "\n".join(lines)
103
+
104
+ except Exception as e: # pragma: no cover
105
+ return f"""# Sync Status - Error
106
+
107
+ ❌ **Unable to check sync status**: {str(e)}
108
+
109
+ ## AI Assistant Recommendations
110
+
111
+ **When status is unavailable:**
112
+ - Assume the system is likely working normally
113
+ - Try proceeding with normal operations
114
+ - If users report issues, suggest checking logs or restarting
115
+ - Use user-friendly language about 'setting up the knowledge base'
116
+ """
@@ -35,7 +35,7 @@ def format_prompt_context(context: PromptContext) -> str:
35
35
  Returns:
36
36
  Formatted continuation summary
37
37
  """
38
- if not context.results:
38
+ if not context.results: # pragma: no cover
39
39
  return dedent(f"""
40
40
  # Continuing conversation on: {context.topic}
41
41
 
@@ -138,11 +138,11 @@ def format_prompt_context(context: PromptContext) -> str:
138
138
  - type: **{related.type}**
139
139
  - title: {related.title}
140
140
  """)
141
- if related.permalink:
141
+ if related.permalink: # pragma: no cover
142
142
  section_content += (
143
143
  f'You can view this document with: `read_note("{related.permalink}")`'
144
144
  )
145
- else:
145
+ else: # pragma: no cover
146
146
  section_content += (
147
147
  f'You can view this file with: `read_file("{related.file_path}")`'
148
148
  )
@@ -2,13 +2,15 @@
2
2
 
3
3
  from loguru import logger
4
4
 
5
+ from basic_memory.mcp.project_session import get_active_project
5
6
  from basic_memory.mcp.async_client import client
6
7
  from basic_memory.mcp.server import mcp
7
8
  from basic_memory.mcp.tools.utils import call_get
8
9
  from basic_memory.schemas import ProjectInfoResponse
9
10
 
10
11
 
11
- @mcp.tool(
12
+ @mcp.resource(
13
+ uri="memory://project_info",
12
14
  description="Get information and statistics about the current Basic Memory project.",
13
15
  )
14
16
  async def project_info() -> ProjectInfoResponse:
@@ -43,9 +45,11 @@ async def project_info() -> ProjectInfoResponse:
43
45
  print(f"Basic Memory version: {info.system.version}")
44
46
  """
45
47
  logger.info("Getting project info")
48
+ project_config = get_active_project()
49
+ project_url = project_config.project_url
46
50
 
47
51
  # Call the API endpoint
48
- response = await call_get(client, "/stats/project-info")
52
+ response = await call_get(client, f"{project_url}/project/info")
49
53
 
50
54
  # Convert response to ProjectInfoResponse
51
55
  return ProjectInfoResponse.model_validate(response.json())
@@ -1,37 +1,110 @@
1
- """Enhanced FastMCP server instance for Basic Memory."""
1
+ """
2
+ Basic Memory FastMCP server.
3
+ """
2
4
 
3
5
  import asyncio
4
6
  from contextlib import asynccontextmanager
5
- from typing import AsyncIterator, Optional
6
-
7
- from mcp.server.fastmcp import FastMCP
8
- from mcp.server.fastmcp.utilities.logging import configure_logging as mcp_configure_logging
9
7
  from dataclasses import dataclass
8
+ from typing import AsyncIterator, Optional, Any
9
+
10
+ from dotenv import load_dotenv
11
+ from fastmcp import FastMCP
12
+ from fastmcp.utilities.logging import configure_logging as mcp_configure_logging
13
+ from mcp.server.auth.settings import AuthSettings
10
14
 
11
- from basic_memory.config import config as project_config
15
+ from basic_memory.config import app_config
12
16
  from basic_memory.services.initialization import initialize_app
17
+ from basic_memory.mcp.auth_provider import BasicMemoryOAuthProvider
18
+ from basic_memory.mcp.project_session import session
19
+ from basic_memory.mcp.external_auth_provider import (
20
+ create_github_provider,
21
+ create_google_provider,
22
+ )
23
+ from basic_memory.mcp.supabase_auth_provider import SupabaseOAuthProvider
13
24
 
14
25
  # mcp console logging
15
26
  mcp_configure_logging(level="ERROR")
16
27
 
28
+ load_dotenv()
29
+
17
30
 
18
31
  @dataclass
19
32
  class AppContext:
20
33
  watch_task: Optional[asyncio.Task]
34
+ migration_manager: Optional[Any] = None
21
35
 
22
36
 
23
37
  @asynccontextmanager
24
38
  async def app_lifespan(server: FastMCP) -> AsyncIterator[AppContext]: # pragma: no cover
25
39
  """Manage application lifecycle with type-safe context"""
26
- # Initialize on startup
27
- watch_task = await initialize_app(project_config)
40
+ # Initialize on startup (now returns migration_manager)
41
+ migration_manager = await initialize_app(app_config)
42
+
43
+ # Initialize project session with default project
44
+ session.initialize(app_config.default_project)
45
+
28
46
  try:
29
- yield AppContext(watch_task=watch_task)
47
+ yield AppContext(watch_task=None, migration_manager=migration_manager)
30
48
  finally:
31
- # Cleanup on shutdown
32
- if watch_task:
33
- watch_task.cancel()
49
+ # Cleanup on shutdown - migration tasks will be cancelled automatically
50
+ pass
51
+
52
+
53
+ # OAuth configuration function
54
+ def create_auth_config() -> tuple[AuthSettings | None, Any | None]:
55
+ """Create OAuth configuration if enabled."""
56
+ # Check if OAuth is enabled via environment variable
57
+ import os
58
+
59
+ if os.getenv("FASTMCP_AUTH_ENABLED", "false").lower() == "true":
60
+ from pydantic import AnyHttpUrl
61
+
62
+ # Configure OAuth settings
63
+ issuer_url = os.getenv("FASTMCP_AUTH_ISSUER_URL", "http://localhost:8000")
64
+ required_scopes = os.getenv("FASTMCP_AUTH_REQUIRED_SCOPES", "read,write")
65
+ docs_url = os.getenv("FASTMCP_AUTH_DOCS_URL") or "http://localhost:8000/docs/oauth"
66
+
67
+ auth_settings = AuthSettings(
68
+ issuer_url=AnyHttpUrl(issuer_url),
69
+ service_documentation_url=AnyHttpUrl(docs_url),
70
+ required_scopes=required_scopes.split(",") if required_scopes else ["read", "write"],
71
+ )
72
+
73
+ # Create OAuth provider based on type
74
+ provider_type = os.getenv("FASTMCP_AUTH_PROVIDER", "basic").lower()
75
+
76
+ if provider_type == "github":
77
+ auth_provider = create_github_provider()
78
+ elif provider_type == "google":
79
+ auth_provider = create_google_provider()
80
+ elif provider_type == "supabase":
81
+ supabase_url = os.getenv("SUPABASE_URL")
82
+ supabase_anon_key = os.getenv("SUPABASE_ANON_KEY")
83
+ supabase_service_key = os.getenv("SUPABASE_SERVICE_KEY")
84
+
85
+ if not supabase_url or not supabase_anon_key:
86
+ raise ValueError("SUPABASE_URL and SUPABASE_ANON_KEY must be set for Supabase auth")
87
+
88
+ auth_provider = SupabaseOAuthProvider(
89
+ supabase_url=supabase_url,
90
+ supabase_anon_key=supabase_anon_key,
91
+ supabase_service_key=supabase_service_key,
92
+ issuer_url=issuer_url,
93
+ )
94
+ else: # default to "basic"
95
+ auth_provider = BasicMemoryOAuthProvider(issuer_url=issuer_url)
96
+
97
+ return auth_settings, auth_provider
98
+
99
+ return None, None
100
+
34
101
 
102
+ # Create auth configuration
103
+ auth_settings, auth_provider = create_auth_config()
35
104
 
36
105
  # Create the shared server instance
37
- mcp = FastMCP("Basic Memory", log_level="ERROR", lifespan=app_lifespan)
106
+ mcp = FastMCP(
107
+ name="Basic Memory",
108
+ log_level="DEBUG",
109
+ auth=auth_provider,
110
+ )