basic-memory 0.12.3__py3-none-any.whl → 0.13.0b1__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 +7 -1
- basic_memory/alembic/env.py +1 -1
- basic_memory/alembic/versions/5fe1ab1ccebe_add_projects_table.py +108 -0
- basic_memory/alembic/versions/cc7172b46608_update_search_index_schema.py +0 -5
- basic_memory/api/app.py +43 -13
- basic_memory/api/routers/__init__.py +4 -2
- basic_memory/api/routers/directory_router.py +63 -0
- basic_memory/api/routers/importer_router.py +152 -0
- basic_memory/api/routers/knowledge_router.py +127 -38
- basic_memory/api/routers/management_router.py +78 -0
- basic_memory/api/routers/memory_router.py +4 -59
- basic_memory/api/routers/project_router.py +230 -0
- basic_memory/api/routers/prompt_router.py +260 -0
- basic_memory/api/routers/search_router.py +3 -21
- basic_memory/api/routers/utils.py +130 -0
- basic_memory/api/template_loader.py +292 -0
- basic_memory/cli/app.py +20 -21
- basic_memory/cli/commands/__init__.py +2 -1
- basic_memory/cli/commands/auth.py +136 -0
- basic_memory/cli/commands/db.py +3 -3
- basic_memory/cli/commands/import_chatgpt.py +31 -207
- basic_memory/cli/commands/import_claude_conversations.py +16 -142
- basic_memory/cli/commands/import_claude_projects.py +33 -143
- basic_memory/cli/commands/import_memory_json.py +26 -83
- basic_memory/cli/commands/mcp.py +71 -18
- basic_memory/cli/commands/project.py +99 -67
- basic_memory/cli/commands/status.py +19 -9
- basic_memory/cli/commands/sync.py +44 -58
- basic_memory/cli/main.py +1 -5
- basic_memory/config.py +145 -88
- basic_memory/db.py +6 -4
- basic_memory/deps.py +227 -30
- basic_memory/importers/__init__.py +27 -0
- basic_memory/importers/base.py +79 -0
- basic_memory/importers/chatgpt_importer.py +222 -0
- basic_memory/importers/claude_conversations_importer.py +172 -0
- basic_memory/importers/claude_projects_importer.py +148 -0
- basic_memory/importers/memory_json_importer.py +93 -0
- basic_memory/importers/utils.py +58 -0
- basic_memory/markdown/entity_parser.py +5 -2
- basic_memory/mcp/auth_provider.py +270 -0
- basic_memory/mcp/external_auth_provider.py +321 -0
- basic_memory/mcp/project_session.py +103 -0
- basic_memory/mcp/prompts/continue_conversation.py +18 -68
- basic_memory/mcp/prompts/recent_activity.py +19 -3
- basic_memory/mcp/prompts/search.py +14 -140
- basic_memory/mcp/prompts/utils.py +3 -3
- basic_memory/mcp/{tools → resources}/project_info.py +6 -2
- basic_memory/mcp/server.py +82 -8
- basic_memory/mcp/supabase_auth_provider.py +463 -0
- basic_memory/mcp/tools/__init__.py +20 -0
- basic_memory/mcp/tools/build_context.py +11 -1
- basic_memory/mcp/tools/canvas.py +15 -2
- basic_memory/mcp/tools/delete_note.py +12 -4
- basic_memory/mcp/tools/edit_note.py +297 -0
- basic_memory/mcp/tools/list_directory.py +154 -0
- basic_memory/mcp/tools/move_note.py +87 -0
- basic_memory/mcp/tools/project_management.py +300 -0
- basic_memory/mcp/tools/read_content.py +15 -6
- basic_memory/mcp/tools/read_note.py +17 -5
- basic_memory/mcp/tools/recent_activity.py +11 -2
- basic_memory/mcp/tools/search.py +10 -1
- basic_memory/mcp/tools/utils.py +137 -12
- basic_memory/mcp/tools/write_note.py +11 -15
- basic_memory/models/__init__.py +3 -2
- basic_memory/models/knowledge.py +16 -4
- basic_memory/models/project.py +80 -0
- basic_memory/models/search.py +8 -5
- basic_memory/repository/__init__.py +2 -0
- basic_memory/repository/entity_repository.py +8 -3
- basic_memory/repository/observation_repository.py +35 -3
- basic_memory/repository/project_info_repository.py +3 -2
- basic_memory/repository/project_repository.py +85 -0
- basic_memory/repository/relation_repository.py +8 -2
- basic_memory/repository/repository.py +107 -15
- basic_memory/repository/search_repository.py +87 -27
- basic_memory/schemas/__init__.py +6 -0
- basic_memory/schemas/directory.py +30 -0
- basic_memory/schemas/importer.py +34 -0
- basic_memory/schemas/memory.py +26 -12
- basic_memory/schemas/project_info.py +112 -2
- basic_memory/schemas/prompt.py +90 -0
- basic_memory/schemas/request.py +56 -2
- basic_memory/schemas/search.py +1 -1
- basic_memory/services/__init__.py +2 -1
- basic_memory/services/context_service.py +208 -95
- basic_memory/services/directory_service.py +167 -0
- basic_memory/services/entity_service.py +385 -5
- basic_memory/services/exceptions.py +6 -0
- basic_memory/services/file_service.py +14 -15
- basic_memory/services/initialization.py +144 -67
- basic_memory/services/link_resolver.py +16 -8
- basic_memory/services/project_service.py +548 -0
- basic_memory/services/search_service.py +77 -2
- basic_memory/sync/background_sync.py +25 -0
- basic_memory/sync/sync_service.py +10 -9
- basic_memory/sync/watch_service.py +63 -39
- basic_memory/templates/prompts/continue_conversation.hbs +110 -0
- basic_memory/templates/prompts/search.hbs +101 -0
- {basic_memory-0.12.3.dist-info → basic_memory-0.13.0b1.dist-info}/METADATA +23 -1
- basic_memory-0.13.0b1.dist-info/RECORD +132 -0
- basic_memory/api/routers/project_info_router.py +0 -274
- basic_memory/mcp/main.py +0 -24
- basic_memory-0.12.3.dist-info/RECORD +0 -100
- {basic_memory-0.12.3.dist-info → basic_memory-0.13.0b1.dist-info}/WHEEL +0 -0
- {basic_memory-0.12.3.dist-info → basic_memory-0.13.0b1.dist-info}/entry_points.txt +0 -0
- {basic_memory-0.12.3.dist-info → basic_memory-0.13.0b1.dist-info}/licenses/LICENSE +0 -0
|
@@ -0,0 +1,300 @@
|
|
|
1
|
+
"""Project management tools for Basic Memory MCP server.
|
|
2
|
+
|
|
3
|
+
These tools allow users to switch between projects, list available projects,
|
|
4
|
+
and manage project context during conversations.
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
from fastmcp import Context
|
|
8
|
+
from loguru import logger
|
|
9
|
+
|
|
10
|
+
from basic_memory.config import get_project_config
|
|
11
|
+
from basic_memory.mcp.async_client import client
|
|
12
|
+
from basic_memory.mcp.project_session import session, add_project_metadata
|
|
13
|
+
from basic_memory.mcp.server import mcp
|
|
14
|
+
from basic_memory.mcp.tools.utils import call_get, call_put, call_post, call_delete
|
|
15
|
+
from basic_memory.schemas import ProjectInfoResponse
|
|
16
|
+
from basic_memory.schemas.project_info import ProjectList, ProjectStatusResponse, ProjectInfoRequest
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
@mcp.tool()
|
|
20
|
+
async def list_projects(ctx: Context | None = None) -> str:
|
|
21
|
+
"""List all available projects with their status.
|
|
22
|
+
|
|
23
|
+
Shows all Basic Memory projects that are available, indicating which one
|
|
24
|
+
is currently active and which is the default.
|
|
25
|
+
|
|
26
|
+
Returns:
|
|
27
|
+
Formatted list of projects with status indicators
|
|
28
|
+
|
|
29
|
+
Example:
|
|
30
|
+
list_projects()
|
|
31
|
+
"""
|
|
32
|
+
if ctx: # pragma: no cover
|
|
33
|
+
await ctx.info("Listing all available projects")
|
|
34
|
+
|
|
35
|
+
# Get projects from API
|
|
36
|
+
response = await call_get(client, "/projects/projects")
|
|
37
|
+
project_list = ProjectList.model_validate(response.json())
|
|
38
|
+
|
|
39
|
+
current = session.get_current_project()
|
|
40
|
+
|
|
41
|
+
result = "Available projects:\n"
|
|
42
|
+
|
|
43
|
+
for project in project_list.projects:
|
|
44
|
+
indicators = []
|
|
45
|
+
if project.name == current:
|
|
46
|
+
indicators.append("current")
|
|
47
|
+
if project.is_default:
|
|
48
|
+
indicators.append("default")
|
|
49
|
+
|
|
50
|
+
if indicators:
|
|
51
|
+
result += f"• {project.name} ({', '.join(indicators)})\n"
|
|
52
|
+
else:
|
|
53
|
+
result += f"• {project.name}\n"
|
|
54
|
+
|
|
55
|
+
return add_project_metadata(result, current)
|
|
56
|
+
|
|
57
|
+
|
|
58
|
+
@mcp.tool()
|
|
59
|
+
async def switch_project(project_name: str, ctx: Context | None = None) -> str:
|
|
60
|
+
"""Switch to a different project context.
|
|
61
|
+
|
|
62
|
+
Changes the active project context for all subsequent tool calls.
|
|
63
|
+
Shows a project summary after switching successfully.
|
|
64
|
+
|
|
65
|
+
Args:
|
|
66
|
+
project_name: Name of the project to switch to
|
|
67
|
+
|
|
68
|
+
Returns:
|
|
69
|
+
Confirmation message with project summary
|
|
70
|
+
|
|
71
|
+
Example:
|
|
72
|
+
switch_project("work-notes")
|
|
73
|
+
switch_project("personal-journal")
|
|
74
|
+
"""
|
|
75
|
+
if ctx: # pragma: no cover
|
|
76
|
+
await ctx.info(f"Switching to project: {project_name}")
|
|
77
|
+
|
|
78
|
+
current_project = session.get_current_project()
|
|
79
|
+
try:
|
|
80
|
+
# Validate project exists by getting project list
|
|
81
|
+
response = await call_get(client, "/projects/projects")
|
|
82
|
+
project_list = ProjectList.model_validate(response.json())
|
|
83
|
+
|
|
84
|
+
# Check if project exists
|
|
85
|
+
project_exists = any(p.name == project_name for p in project_list.projects)
|
|
86
|
+
if not project_exists:
|
|
87
|
+
available_projects = [p.name for p in project_list.projects]
|
|
88
|
+
return f"Error: Project '{project_name}' not found. Available projects: {', '.join(available_projects)}"
|
|
89
|
+
|
|
90
|
+
# Switch to the project
|
|
91
|
+
session.set_current_project(project_name)
|
|
92
|
+
current_project = session.get_current_project()
|
|
93
|
+
project_config = get_project_config(current_project)
|
|
94
|
+
|
|
95
|
+
# Get project info to show summary
|
|
96
|
+
try:
|
|
97
|
+
response = await call_get(client, f"{project_config.project_url}/project/info")
|
|
98
|
+
project_info = ProjectInfoResponse.model_validate(response.json())
|
|
99
|
+
|
|
100
|
+
result = f"✓ Switched to {project_name} project\n\n"
|
|
101
|
+
result += "Project Summary:\n"
|
|
102
|
+
result += f"• {project_info.statistics.total_entities} entities\n"
|
|
103
|
+
result += f"• {project_info.statistics.total_observations} observations\n"
|
|
104
|
+
result += f"• {project_info.statistics.total_relations} relations\n"
|
|
105
|
+
|
|
106
|
+
except Exception as e:
|
|
107
|
+
# If we can't get project info, still confirm the switch
|
|
108
|
+
logger.warning(f"Could not get project info for {project_name}: {e}")
|
|
109
|
+
result = f"✓ Switched to {project_name} project\n\n"
|
|
110
|
+
result += "Project summary unavailable.\n"
|
|
111
|
+
|
|
112
|
+
return add_project_metadata(result, project_name)
|
|
113
|
+
|
|
114
|
+
except Exception as e:
|
|
115
|
+
logger.error(f"Error switching to project {project_name}: {e}")
|
|
116
|
+
# Revert to previous project on error
|
|
117
|
+
session.set_current_project(current_project)
|
|
118
|
+
raise e
|
|
119
|
+
|
|
120
|
+
|
|
121
|
+
@mcp.tool()
|
|
122
|
+
async def get_current_project(ctx: Context | None = None) -> str:
|
|
123
|
+
"""Show the currently active project and basic stats.
|
|
124
|
+
|
|
125
|
+
Displays which project is currently active and provides basic information
|
|
126
|
+
about it.
|
|
127
|
+
|
|
128
|
+
Returns:
|
|
129
|
+
Current project name and basic statistics
|
|
130
|
+
|
|
131
|
+
Example:
|
|
132
|
+
get_current_project()
|
|
133
|
+
"""
|
|
134
|
+
if ctx: # pragma: no cover
|
|
135
|
+
await ctx.info("Getting current project information")
|
|
136
|
+
|
|
137
|
+
current_project = session.get_current_project()
|
|
138
|
+
project_config = get_project_config(current_project)
|
|
139
|
+
result = f"Current project: {current_project}\n\n"
|
|
140
|
+
|
|
141
|
+
# get project stats
|
|
142
|
+
response = await call_get(client, f"{project_config.project_url}/project/info")
|
|
143
|
+
project_info = ProjectInfoResponse.model_validate(response.json())
|
|
144
|
+
|
|
145
|
+
result += f"• {project_info.statistics.total_entities} entities\n"
|
|
146
|
+
result += f"• {project_info.statistics.total_observations} observations\n"
|
|
147
|
+
result += f"• {project_info.statistics.total_relations} relations\n"
|
|
148
|
+
|
|
149
|
+
default_project = session.get_default_project()
|
|
150
|
+
if current_project != default_project:
|
|
151
|
+
result += f"• Default project: {default_project}\n"
|
|
152
|
+
|
|
153
|
+
return add_project_metadata(result, current_project)
|
|
154
|
+
|
|
155
|
+
|
|
156
|
+
@mcp.tool()
|
|
157
|
+
async def set_default_project(project_name: str, ctx: Context | None = None) -> str:
|
|
158
|
+
"""Set default project in config. Requires restart to take effect.
|
|
159
|
+
|
|
160
|
+
Updates the configuration to use a different default project. This change
|
|
161
|
+
only takes effect after restarting the Basic Memory server.
|
|
162
|
+
|
|
163
|
+
Args:
|
|
164
|
+
project_name: Name of the project to set as default
|
|
165
|
+
|
|
166
|
+
Returns:
|
|
167
|
+
Confirmation message about config update
|
|
168
|
+
|
|
169
|
+
Example:
|
|
170
|
+
set_default_project("work-notes")
|
|
171
|
+
"""
|
|
172
|
+
if ctx: # pragma: no cover
|
|
173
|
+
await ctx.info(f"Setting default project to: {project_name}")
|
|
174
|
+
|
|
175
|
+
# Call API to set default project
|
|
176
|
+
response = await call_put(client, f"/projects/{project_name}/default")
|
|
177
|
+
status_response = ProjectStatusResponse.model_validate(response.json())
|
|
178
|
+
|
|
179
|
+
result = f"✓ {status_response.message}\n\n"
|
|
180
|
+
result += "Restart Basic Memory for this change to take effect:\n"
|
|
181
|
+
result += "basic-memory mcp\n"
|
|
182
|
+
|
|
183
|
+
if status_response.old_project:
|
|
184
|
+
result += f"\nPrevious default: {status_response.old_project.name}\n"
|
|
185
|
+
|
|
186
|
+
return add_project_metadata(result, session.get_current_project())
|
|
187
|
+
|
|
188
|
+
|
|
189
|
+
@mcp.tool()
|
|
190
|
+
async def create_project(
|
|
191
|
+
project_name: str, project_path: str, set_default: bool = False, ctx: Context | None = None
|
|
192
|
+
) -> str:
|
|
193
|
+
"""Create a new Basic Memory project.
|
|
194
|
+
|
|
195
|
+
Creates a new project with the specified name and path. The project directory
|
|
196
|
+
will be created if it doesn't exist. Optionally sets the new project as default.
|
|
197
|
+
|
|
198
|
+
Args:
|
|
199
|
+
project_name: Name for the new project (must be unique)
|
|
200
|
+
project_path: File system path where the project will be stored
|
|
201
|
+
set_default: Whether to set this project as the default (optional, defaults to False)
|
|
202
|
+
|
|
203
|
+
Returns:
|
|
204
|
+
Confirmation message with project details
|
|
205
|
+
|
|
206
|
+
Example:
|
|
207
|
+
create_project("my-research", "~/Documents/research")
|
|
208
|
+
create_project("work-notes", "/home/user/work", set_default=True)
|
|
209
|
+
"""
|
|
210
|
+
if ctx: # pragma: no cover
|
|
211
|
+
await ctx.info(f"Creating project: {project_name} at {project_path}")
|
|
212
|
+
|
|
213
|
+
# Create the project request
|
|
214
|
+
project_request = ProjectInfoRequest(
|
|
215
|
+
name=project_name, path=project_path, set_default=set_default
|
|
216
|
+
)
|
|
217
|
+
|
|
218
|
+
# Call API to create project
|
|
219
|
+
response = await call_post(client, "/projects/projects", json=project_request.model_dump())
|
|
220
|
+
status_response = ProjectStatusResponse.model_validate(response.json())
|
|
221
|
+
|
|
222
|
+
result = f"✓ {status_response.message}\n\n"
|
|
223
|
+
|
|
224
|
+
if status_response.new_project:
|
|
225
|
+
result += "Project Details:\n"
|
|
226
|
+
result += f"• Name: {status_response.new_project.name}\n"
|
|
227
|
+
result += f"• Path: {status_response.new_project.path}\n"
|
|
228
|
+
|
|
229
|
+
if set_default:
|
|
230
|
+
result += "• Set as default project\n"
|
|
231
|
+
|
|
232
|
+
result += "\nProject is now available for use.\n"
|
|
233
|
+
|
|
234
|
+
# If project was set as default, update session
|
|
235
|
+
if set_default:
|
|
236
|
+
session.set_current_project(project_name)
|
|
237
|
+
|
|
238
|
+
return add_project_metadata(result, session.get_current_project())
|
|
239
|
+
|
|
240
|
+
|
|
241
|
+
@mcp.tool()
|
|
242
|
+
async def delete_project(project_name: str, ctx: Context | None = None) -> str:
|
|
243
|
+
"""Delete a Basic Memory project.
|
|
244
|
+
|
|
245
|
+
Removes a project from the configuration and database. This does NOT delete
|
|
246
|
+
the actual files on disk - only removes the project from Basic Memory's
|
|
247
|
+
configuration and database records.
|
|
248
|
+
|
|
249
|
+
Args:
|
|
250
|
+
project_name: Name of the project to delete
|
|
251
|
+
|
|
252
|
+
Returns:
|
|
253
|
+
Confirmation message about project deletion
|
|
254
|
+
|
|
255
|
+
Example:
|
|
256
|
+
delete_project("old-project")
|
|
257
|
+
|
|
258
|
+
Warning:
|
|
259
|
+
This action cannot be undone. The project will need to be re-added
|
|
260
|
+
to access its content through Basic Memory again.
|
|
261
|
+
"""
|
|
262
|
+
if ctx: # pragma: no cover
|
|
263
|
+
await ctx.info(f"Deleting project: {project_name}")
|
|
264
|
+
|
|
265
|
+
current_project = session.get_current_project()
|
|
266
|
+
|
|
267
|
+
# Check if trying to delete current project
|
|
268
|
+
if project_name == current_project:
|
|
269
|
+
raise ValueError(
|
|
270
|
+
f"Cannot delete the currently active project '{project_name}'. Switch to a different project first."
|
|
271
|
+
)
|
|
272
|
+
|
|
273
|
+
# Get project info before deletion to validate it exists
|
|
274
|
+
response = await call_get(client, "/projects/projects")
|
|
275
|
+
project_list = ProjectList.model_validate(response.json())
|
|
276
|
+
|
|
277
|
+
# Check if project exists
|
|
278
|
+
project_exists = any(p.name == project_name for p in project_list.projects)
|
|
279
|
+
if not project_exists:
|
|
280
|
+
available_projects = [p.name for p in project_list.projects]
|
|
281
|
+
raise ValueError(
|
|
282
|
+
f"Project '{project_name}' not found. Available projects: {', '.join(available_projects)}"
|
|
283
|
+
)
|
|
284
|
+
|
|
285
|
+
# Call API to delete project
|
|
286
|
+
response = await call_delete(client, f"/projects/{project_name}")
|
|
287
|
+
status_response = ProjectStatusResponse.model_validate(response.json())
|
|
288
|
+
|
|
289
|
+
result = f"✓ {status_response.message}\n\n"
|
|
290
|
+
|
|
291
|
+
if status_response.old_project:
|
|
292
|
+
result += "Removed project details:\n"
|
|
293
|
+
result += f"• Name: {status_response.old_project.name}\n"
|
|
294
|
+
if hasattr(status_response.old_project, "path"):
|
|
295
|
+
result += f"• Path: {status_response.old_project.path}\n"
|
|
296
|
+
|
|
297
|
+
result += "Files remain on disk but project is no longer tracked by Basic Memory.\n"
|
|
298
|
+
result += "Re-add the project to access its content again.\n"
|
|
299
|
+
|
|
300
|
+
return add_project_metadata(result, session.get_current_project())
|
|
@@ -5,17 +5,19 @@ supporting various file types including text, images, and other binary files.
|
|
|
5
5
|
Files are read directly without any knowledge graph processing.
|
|
6
6
|
"""
|
|
7
7
|
|
|
8
|
+
from typing import Optional
|
|
9
|
+
import base64
|
|
10
|
+
import io
|
|
11
|
+
|
|
8
12
|
from loguru import logger
|
|
13
|
+
from PIL import Image as PILImage
|
|
9
14
|
|
|
10
15
|
from basic_memory.mcp.server import mcp
|
|
11
16
|
from basic_memory.mcp.async_client import client
|
|
12
17
|
from basic_memory.mcp.tools.utils import call_get
|
|
18
|
+
from basic_memory.mcp.project_session import get_active_project
|
|
13
19
|
from basic_memory.schemas.memory import memory_url_path
|
|
14
20
|
|
|
15
|
-
import base64
|
|
16
|
-
import io
|
|
17
|
-
from PIL import Image as PILImage
|
|
18
|
-
|
|
19
21
|
|
|
20
22
|
def calculate_target_params(content_length):
|
|
21
23
|
"""Calculate initial quality and size based on input file size"""
|
|
@@ -144,7 +146,7 @@ def optimize_image(img, content_length, max_output_bytes=350000):
|
|
|
144
146
|
|
|
145
147
|
|
|
146
148
|
@mcp.tool(description="Read a file's raw content by path or permalink")
|
|
147
|
-
async def read_content(path: str) -> dict:
|
|
149
|
+
async def read_content(path: str, project: Optional[str] = None) -> dict:
|
|
148
150
|
"""Read a file's raw content by path or permalink.
|
|
149
151
|
|
|
150
152
|
This tool provides direct access to file content in the knowledge base,
|
|
@@ -158,6 +160,7 @@ async def read_content(path: str) -> dict:
|
|
|
158
160
|
- A regular file path (docs/example.md)
|
|
159
161
|
- A memory URL (memory://docs/example)
|
|
160
162
|
- A permalink (docs/example)
|
|
163
|
+
project: Optional project name to read from. If not provided, uses current active project.
|
|
161
164
|
|
|
162
165
|
Returns:
|
|
163
166
|
A dictionary with the file content and metadata:
|
|
@@ -175,11 +178,17 @@ async def read_content(path: str) -> dict:
|
|
|
175
178
|
|
|
176
179
|
# Read using memory URL
|
|
177
180
|
content = await read_file("memory://docs/architecture")
|
|
181
|
+
|
|
182
|
+
# Read from specific project
|
|
183
|
+
content = await read_content("docs/example.md", project="work-project")
|
|
178
184
|
"""
|
|
179
185
|
logger.info("Reading file", path=path)
|
|
180
186
|
|
|
187
|
+
active_project = get_active_project(project)
|
|
188
|
+
project_url = active_project.project_url
|
|
189
|
+
|
|
181
190
|
url = memory_url_path(path)
|
|
182
|
-
response = await call_get(client, f"/resource/{url}")
|
|
191
|
+
response = await call_get(client, f"{project_url}/resource/{url}")
|
|
183
192
|
content_type = response.headers.get("content-type", "application/octet-stream")
|
|
184
193
|
content_length = int(response.headers.get("content-length", 0))
|
|
185
194
|
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
"""Read note tool for Basic Memory MCP server."""
|
|
2
2
|
|
|
3
3
|
from textwrap import dedent
|
|
4
|
+
from typing import Optional
|
|
4
5
|
|
|
5
6
|
from loguru import logger
|
|
6
7
|
|
|
@@ -8,13 +9,16 @@ from basic_memory.mcp.async_client import client
|
|
|
8
9
|
from basic_memory.mcp.server import mcp
|
|
9
10
|
from basic_memory.mcp.tools.search import search_notes
|
|
10
11
|
from basic_memory.mcp.tools.utils import call_get
|
|
12
|
+
from basic_memory.mcp.project_session import get_active_project
|
|
11
13
|
from basic_memory.schemas.memory import memory_url_path
|
|
12
14
|
|
|
13
15
|
|
|
14
16
|
@mcp.tool(
|
|
15
17
|
description="Read a markdown note by title or permalink.",
|
|
16
18
|
)
|
|
17
|
-
async def read_note(
|
|
19
|
+
async def read_note(
|
|
20
|
+
identifier: str, page: int = 1, page_size: int = 10, project: Optional[str] = None
|
|
21
|
+
) -> str:
|
|
18
22
|
"""Read a markdown note from the knowledge base.
|
|
19
23
|
|
|
20
24
|
This tool finds and retrieves a note by its title, permalink, or content search,
|
|
@@ -26,6 +30,7 @@ async def read_note(identifier: str, page: int = 1, page_size: int = 10) -> str:
|
|
|
26
30
|
Can be a full memory:// URL, a permalink, a title, or search text
|
|
27
31
|
page: Page number for paginated results (default: 1)
|
|
28
32
|
page_size: Number of items per page (default: 10)
|
|
33
|
+
project: Optional project name to read from. If not provided, uses current active project.
|
|
29
34
|
|
|
30
35
|
Returns:
|
|
31
36
|
The full markdown content of the note if found, or helpful guidance if not found.
|
|
@@ -42,10 +47,17 @@ async def read_note(identifier: str, page: int = 1, page_size: int = 10) -> str:
|
|
|
42
47
|
|
|
43
48
|
# Read with pagination
|
|
44
49
|
read_note("Project Updates", page=2, page_size=5)
|
|
50
|
+
|
|
51
|
+
# Read from specific project
|
|
52
|
+
read_note("Meeting Notes", project="work-project")
|
|
45
53
|
"""
|
|
54
|
+
|
|
55
|
+
active_project = get_active_project(project)
|
|
56
|
+
project_url = active_project.project_url
|
|
57
|
+
|
|
46
58
|
# Get the file via REST API - first try direct permalink lookup
|
|
47
59
|
entity_path = memory_url_path(identifier)
|
|
48
|
-
path = f"/resource/{entity_path}"
|
|
60
|
+
path = f"{project_url}/resource/{entity_path}"
|
|
49
61
|
logger.info(f"Attempting to read note from URL: {path}")
|
|
50
62
|
|
|
51
63
|
try:
|
|
@@ -62,14 +74,14 @@ async def read_note(identifier: str, page: int = 1, page_size: int = 10) -> str:
|
|
|
62
74
|
|
|
63
75
|
# Fallback 1: Try title search via API
|
|
64
76
|
logger.info(f"Search title for: {identifier}")
|
|
65
|
-
title_results = await search_notes(query=identifier, search_type="title")
|
|
77
|
+
title_results = await search_notes(query=identifier, search_type="title", project=project)
|
|
66
78
|
|
|
67
79
|
if title_results and title_results.results:
|
|
68
80
|
result = title_results.results[0] # Get the first/best match
|
|
69
81
|
if result.permalink:
|
|
70
82
|
try:
|
|
71
83
|
# Try to fetch the content using the found permalink
|
|
72
|
-
path = f"/resource/{result.permalink}"
|
|
84
|
+
path = f"{project_url}/resource/{result.permalink}"
|
|
73
85
|
response = await call_get(
|
|
74
86
|
client, path, params={"page": page, "page_size": page_size}
|
|
75
87
|
)
|
|
@@ -86,7 +98,7 @@ async def read_note(identifier: str, page: int = 1, page_size: int = 10) -> str:
|
|
|
86
98
|
|
|
87
99
|
# Fallback 2: Text search as a last resort
|
|
88
100
|
logger.info(f"Title search failed, trying text search for: {identifier}")
|
|
89
|
-
text_results = await search_notes(query=identifier, search_type="text")
|
|
101
|
+
text_results = await search_notes(query=identifier, search_type="text", project=project)
|
|
90
102
|
|
|
91
103
|
# We didn't find a direct match, construct a helpful error message
|
|
92
104
|
if not text_results or not text_results.results:
|
|
@@ -1,12 +1,13 @@
|
|
|
1
1
|
"""Recent activity tool for Basic Memory MCP server."""
|
|
2
2
|
|
|
3
|
-
from typing import List, Union
|
|
3
|
+
from typing import List, Union, Optional
|
|
4
4
|
|
|
5
5
|
from loguru import logger
|
|
6
6
|
|
|
7
7
|
from basic_memory.mcp.async_client import client
|
|
8
8
|
from basic_memory.mcp.server import mcp
|
|
9
9
|
from basic_memory.mcp.tools.utils import call_get
|
|
10
|
+
from basic_memory.mcp.project_session import get_active_project
|
|
10
11
|
from basic_memory.schemas.base import TimeFrame
|
|
11
12
|
from basic_memory.schemas.memory import GraphContext
|
|
12
13
|
from basic_memory.schemas.search import SearchItemType
|
|
@@ -31,6 +32,7 @@ async def recent_activity(
|
|
|
31
32
|
page: int = 1,
|
|
32
33
|
page_size: int = 10,
|
|
33
34
|
max_related: int = 10,
|
|
35
|
+
project: Optional[str] = None,
|
|
34
36
|
) -> GraphContext:
|
|
35
37
|
"""Get recent activity across the knowledge base.
|
|
36
38
|
|
|
@@ -51,6 +53,7 @@ async def recent_activity(
|
|
|
51
53
|
page: Page number of results to return (default: 1)
|
|
52
54
|
page_size: Number of results to return per page (default: 10)
|
|
53
55
|
max_related: Maximum number of related results to return (default: 10)
|
|
56
|
+
project: Optional project name to get activity from. If not provided, uses current active project.
|
|
54
57
|
|
|
55
58
|
Returns:
|
|
56
59
|
GraphContext containing:
|
|
@@ -74,6 +77,9 @@ async def recent_activity(
|
|
|
74
77
|
# Look back further with more context
|
|
75
78
|
recent_activity(type="entity", depth=2, timeframe="2 weeks ago")
|
|
76
79
|
|
|
80
|
+
# Get activity from specific project
|
|
81
|
+
recent_activity(type="entity", project="work-project")
|
|
82
|
+
|
|
77
83
|
Notes:
|
|
78
84
|
- Higher depth values (>3) may impact performance with large result sets
|
|
79
85
|
- For focused queries, consider using build_context with a specific URI
|
|
@@ -114,9 +120,12 @@ async def recent_activity(
|
|
|
114
120
|
# Add validated types to params
|
|
115
121
|
params["type"] = [t.value for t in validated_types] # pyright: ignore
|
|
116
122
|
|
|
123
|
+
active_project = get_active_project(project)
|
|
124
|
+
project_url = active_project.project_url
|
|
125
|
+
|
|
117
126
|
response = await call_get(
|
|
118
127
|
client,
|
|
119
|
-
"/memory/recent",
|
|
128
|
+
f"{project_url}/memory/recent",
|
|
120
129
|
params=params,
|
|
121
130
|
)
|
|
122
131
|
return GraphContext.model_validate(response.json())
|
basic_memory/mcp/tools/search.py
CHANGED
|
@@ -7,6 +7,7 @@ from loguru import logger
|
|
|
7
7
|
from basic_memory.mcp.async_client import client
|
|
8
8
|
from basic_memory.mcp.server import mcp
|
|
9
9
|
from basic_memory.mcp.tools.utils import call_post
|
|
10
|
+
from basic_memory.mcp.project_session import get_active_project
|
|
10
11
|
from basic_memory.schemas.search import SearchItemType, SearchQuery, SearchResponse
|
|
11
12
|
|
|
12
13
|
|
|
@@ -21,6 +22,7 @@ async def search_notes(
|
|
|
21
22
|
types: Optional[List[str]] = None,
|
|
22
23
|
entity_types: Optional[List[str]] = None,
|
|
23
24
|
after_date: Optional[str] = None,
|
|
25
|
+
project: Optional[str] = None,
|
|
24
26
|
) -> SearchResponse:
|
|
25
27
|
"""Search across all content in the knowledge base.
|
|
26
28
|
|
|
@@ -36,6 +38,7 @@ async def search_notes(
|
|
|
36
38
|
types: Optional list of note types to search (e.g., ["note", "person"])
|
|
37
39
|
entity_types: Optional list of entity types to filter by (e.g., ["entity", "observation"])
|
|
38
40
|
after_date: Optional date filter for recent content (e.g., "1 week", "2d")
|
|
41
|
+
project: Optional project name to search in. If not provided, uses current active project.
|
|
39
42
|
|
|
40
43
|
Returns:
|
|
41
44
|
SearchResponse with results and pagination info
|
|
@@ -79,6 +82,9 @@ async def search_notes(
|
|
|
79
82
|
query="docs/meeting-*",
|
|
80
83
|
search_type="permalink"
|
|
81
84
|
)
|
|
85
|
+
|
|
86
|
+
# Search in specific project
|
|
87
|
+
results = await search_notes("meeting notes", project="work-project")
|
|
82
88
|
"""
|
|
83
89
|
# Create a SearchQuery object based on the parameters
|
|
84
90
|
search_query = SearchQuery()
|
|
@@ -103,10 +109,13 @@ async def search_notes(
|
|
|
103
109
|
if after_date:
|
|
104
110
|
search_query.after_date = after_date
|
|
105
111
|
|
|
112
|
+
active_project = get_active_project(project)
|
|
113
|
+
project_url = active_project.project_url
|
|
114
|
+
|
|
106
115
|
logger.info(f"Searching for {search_query}")
|
|
107
116
|
response = await call_post(
|
|
108
117
|
client,
|
|
109
|
-
"/search/",
|
|
118
|
+
f"{project_url}/search/",
|
|
110
119
|
json=search_query.model_dump(),
|
|
111
120
|
params={"page": page, "page_size": page_size},
|
|
112
121
|
)
|