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.
- basic_memory/__init__.py +1 -1
- basic_memory/alembic/versions/a1b2c3d4e5f6_fix_project_foreign_keys.py +49 -0
- basic_memory/api/app.py +10 -4
- basic_memory/api/routers/knowledge_router.py +25 -8
- basic_memory/api/routers/project_router.py +99 -4
- basic_memory/api/routers/resource_router.py +3 -3
- basic_memory/cli/app.py +9 -28
- basic_memory/cli/auth.py +277 -0
- basic_memory/cli/commands/cloud/__init__.py +5 -0
- basic_memory/cli/commands/cloud/api_client.py +112 -0
- basic_memory/cli/commands/cloud/bisync_commands.py +818 -0
- basic_memory/cli/commands/cloud/core_commands.py +288 -0
- basic_memory/cli/commands/cloud/mount_commands.py +295 -0
- basic_memory/cli/commands/cloud/rclone_config.py +288 -0
- basic_memory/cli/commands/cloud/rclone_installer.py +198 -0
- basic_memory/cli/commands/command_utils.py +60 -0
- basic_memory/cli/commands/import_memory_json.py +0 -4
- basic_memory/cli/commands/mcp.py +16 -4
- basic_memory/cli/commands/project.py +141 -145
- basic_memory/cli/commands/status.py +34 -22
- basic_memory/cli/commands/sync.py +45 -228
- basic_memory/cli/commands/tool.py +87 -16
- basic_memory/cli/main.py +1 -0
- basic_memory/config.py +96 -20
- basic_memory/db.py +104 -3
- basic_memory/deps.py +20 -3
- basic_memory/file_utils.py +89 -0
- basic_memory/ignore_utils.py +295 -0
- basic_memory/importers/chatgpt_importer.py +1 -1
- basic_memory/importers/utils.py +2 -2
- basic_memory/markdown/entity_parser.py +2 -2
- basic_memory/markdown/markdown_processor.py +2 -2
- basic_memory/markdown/plugins.py +39 -21
- basic_memory/markdown/utils.py +1 -1
- basic_memory/mcp/async_client.py +22 -10
- basic_memory/mcp/project_context.py +141 -0
- basic_memory/mcp/prompts/ai_assistant_guide.py +49 -4
- basic_memory/mcp/prompts/continue_conversation.py +1 -1
- basic_memory/mcp/prompts/recent_activity.py +116 -32
- basic_memory/mcp/prompts/search.py +1 -1
- basic_memory/mcp/prompts/utils.py +11 -4
- basic_memory/mcp/resources/ai_assistant_guide.md +179 -41
- basic_memory/mcp/resources/project_info.py +20 -6
- basic_memory/mcp/server.py +0 -37
- basic_memory/mcp/tools/__init__.py +5 -6
- basic_memory/mcp/tools/build_context.py +39 -19
- basic_memory/mcp/tools/canvas.py +19 -8
- basic_memory/mcp/tools/chatgpt_tools.py +178 -0
- basic_memory/mcp/tools/delete_note.py +67 -34
- basic_memory/mcp/tools/edit_note.py +55 -39
- basic_memory/mcp/tools/headers.py +44 -0
- basic_memory/mcp/tools/list_directory.py +18 -8
- basic_memory/mcp/tools/move_note.py +119 -41
- basic_memory/mcp/tools/project_management.py +77 -229
- basic_memory/mcp/tools/read_content.py +28 -12
- basic_memory/mcp/tools/read_note.py +97 -57
- basic_memory/mcp/tools/recent_activity.py +441 -42
- basic_memory/mcp/tools/search.py +82 -70
- basic_memory/mcp/tools/sync_status.py +5 -4
- basic_memory/mcp/tools/utils.py +19 -0
- basic_memory/mcp/tools/view_note.py +31 -6
- basic_memory/mcp/tools/write_note.py +65 -14
- basic_memory/models/knowledge.py +19 -2
- basic_memory/models/project.py +6 -2
- basic_memory/repository/entity_repository.py +31 -84
- basic_memory/repository/project_repository.py +1 -1
- basic_memory/repository/relation_repository.py +13 -0
- basic_memory/repository/repository.py +2 -2
- basic_memory/repository/search_repository.py +9 -3
- basic_memory/schemas/__init__.py +6 -0
- basic_memory/schemas/base.py +70 -12
- basic_memory/schemas/cloud.py +46 -0
- basic_memory/schemas/memory.py +99 -18
- basic_memory/schemas/project_info.py +9 -10
- basic_memory/schemas/sync_report.py +48 -0
- basic_memory/services/context_service.py +35 -11
- basic_memory/services/directory_service.py +7 -0
- basic_memory/services/entity_service.py +82 -52
- basic_memory/services/initialization.py +30 -11
- basic_memory/services/project_service.py +23 -33
- basic_memory/sync/sync_service.py +148 -24
- basic_memory/sync/watch_service.py +128 -44
- basic_memory/utils.py +181 -109
- {basic_memory-0.14.3.dist-info → basic_memory-0.15.0.dist-info}/METADATA +26 -96
- basic_memory-0.15.0.dist-info/RECORD +147 -0
- basic_memory/mcp/project_session.py +0 -120
- basic_memory-0.14.3.dist-info/RECORD +0 -132
- {basic_memory-0.14.3.dist-info → basic_memory-0.15.0.dist-info}/WHEEL +0 -0
- {basic_memory-0.14.3.dist-info → basic_memory-0.15.0.dist-info}/entry_points.txt +0 -0
- {basic_memory-0.14.3.dist-info → basic_memory-0.15.0.dist-info}/licenses/LICENSE +0 -0
|
@@ -4,240 +4,75 @@ These tools allow users to switch between projects, list available projects,
|
|
|
4
4
|
and manage project context during conversations.
|
|
5
5
|
"""
|
|
6
6
|
|
|
7
|
-
|
|
8
|
-
from typing import Optional
|
|
9
|
-
|
|
7
|
+
import os
|
|
10
8
|
from fastmcp import Context
|
|
11
|
-
from loguru import logger
|
|
12
9
|
|
|
13
10
|
from basic_memory.mcp.async_client import client
|
|
14
|
-
from basic_memory.mcp.project_session import session, add_project_metadata
|
|
15
11
|
from basic_memory.mcp.server import mcp
|
|
16
|
-
from basic_memory.mcp.tools.utils import call_get,
|
|
17
|
-
from basic_memory.schemas import
|
|
18
|
-
|
|
12
|
+
from basic_memory.mcp.tools.utils import call_get, call_post, call_delete
|
|
13
|
+
from basic_memory.schemas.project_info import (
|
|
14
|
+
ProjectList,
|
|
15
|
+
ProjectStatusResponse,
|
|
16
|
+
ProjectInfoRequest,
|
|
17
|
+
)
|
|
19
18
|
from basic_memory.utils import generate_permalink
|
|
20
19
|
|
|
21
20
|
|
|
22
21
|
@mcp.tool("list_memory_projects")
|
|
23
|
-
async def list_memory_projects(
|
|
24
|
-
ctx: Context | None = None, _compatibility: Optional[str] = None
|
|
25
|
-
) -> str:
|
|
22
|
+
async def list_memory_projects(context: Context | None = None) -> str:
|
|
26
23
|
"""List all available projects with their status.
|
|
27
24
|
|
|
28
|
-
Shows all Basic Memory projects that are available
|
|
29
|
-
|
|
25
|
+
Shows all Basic Memory projects that are available for MCP operations.
|
|
26
|
+
Use this tool to discover projects when you need to know which project to use.
|
|
27
|
+
|
|
28
|
+
Use this tool:
|
|
29
|
+
- At conversation start when project is unknown
|
|
30
|
+
- When user asks about available projects
|
|
31
|
+
- Before any operation requiring a project
|
|
32
|
+
|
|
33
|
+
After calling:
|
|
34
|
+
- Ask user which project to use
|
|
35
|
+
- Remember their choice for the session
|
|
30
36
|
|
|
31
37
|
Returns:
|
|
32
|
-
Formatted list of projects with
|
|
38
|
+
Formatted list of projects with session management guidance
|
|
33
39
|
|
|
34
40
|
Example:
|
|
35
41
|
list_memory_projects()
|
|
36
42
|
"""
|
|
37
|
-
if
|
|
38
|
-
await
|
|
43
|
+
if context: # pragma: no cover
|
|
44
|
+
await context.info("Listing all available projects")
|
|
45
|
+
|
|
46
|
+
# Check if server is constrained to a specific project
|
|
47
|
+
constrained_project = os.environ.get("BASIC_MEMORY_MCP_PROJECT")
|
|
39
48
|
|
|
40
49
|
# Get projects from API
|
|
41
50
|
response = await call_get(client, "/projects/projects")
|
|
42
51
|
project_list = ProjectList.model_validate(response.json())
|
|
43
52
|
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
53
|
+
if constrained_project:
|
|
54
|
+
result = f"Project: {constrained_project}\n\n"
|
|
55
|
+
result += "Note: This MCP server is constrained to a single project.\n"
|
|
56
|
+
result += "All operations will automatically use this project."
|
|
57
|
+
else:
|
|
58
|
+
# Show all projects with session guidance
|
|
59
|
+
result = "Available projects:\n"
|
|
47
60
|
|
|
48
|
-
|
|
49
|
-
indicators = []
|
|
50
|
-
if project.name == current:
|
|
51
|
-
indicators.append("current")
|
|
52
|
-
if project.is_default:
|
|
53
|
-
indicators.append("default")
|
|
54
|
-
|
|
55
|
-
if indicators:
|
|
56
|
-
result += f"• {project.name} ({', '.join(indicators)})\n"
|
|
57
|
-
else:
|
|
61
|
+
for project in project_list.projects:
|
|
58
62
|
result += f"• {project.name}\n"
|
|
59
63
|
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
"""Switch to a different project context.
|
|
66
|
-
|
|
67
|
-
Changes the active project context for all subsequent tool calls.
|
|
68
|
-
Shows a project summary after switching successfully.
|
|
69
|
-
|
|
70
|
-
Args:
|
|
71
|
-
project_name: Name of the project to switch to
|
|
72
|
-
|
|
73
|
-
Returns:
|
|
74
|
-
Confirmation message with project summary
|
|
75
|
-
|
|
76
|
-
Example:
|
|
77
|
-
switch_project("work-notes")
|
|
78
|
-
switch_project("personal-journal")
|
|
79
|
-
"""
|
|
80
|
-
if ctx: # pragma: no cover
|
|
81
|
-
await ctx.info(f"Switching to project: {project_name}")
|
|
82
|
-
|
|
83
|
-
project_permalink = generate_permalink(project_name)
|
|
84
|
-
current_project = session.get_current_project()
|
|
85
|
-
try:
|
|
86
|
-
# Validate project exists by getting project list
|
|
87
|
-
response = await call_get(client, "/projects/projects")
|
|
88
|
-
project_list = ProjectList.model_validate(response.json())
|
|
89
|
-
|
|
90
|
-
# Find the project by name (case-insensitive) or permalink
|
|
91
|
-
target_project = None
|
|
92
|
-
for p in project_list.projects:
|
|
93
|
-
# Match by permalink (handles case-insensitive input)
|
|
94
|
-
if p.permalink == project_permalink:
|
|
95
|
-
target_project = p
|
|
96
|
-
break
|
|
97
|
-
# Also match by name comparison (case-insensitive)
|
|
98
|
-
if p.name.lower() == project_name.lower():
|
|
99
|
-
target_project = p
|
|
100
|
-
break
|
|
101
|
-
|
|
102
|
-
if not target_project:
|
|
103
|
-
available_projects = [p.name for p in project_list.projects]
|
|
104
|
-
return f"Error: Project '{project_name}' not found. Available projects: {', '.join(available_projects)}"
|
|
105
|
-
|
|
106
|
-
# Switch to the project using the canonical name from database
|
|
107
|
-
canonical_name = target_project.name
|
|
108
|
-
session.set_current_project(canonical_name)
|
|
109
|
-
current_project = session.get_current_project()
|
|
110
|
-
|
|
111
|
-
# Get project info to show summary
|
|
112
|
-
try:
|
|
113
|
-
current_project_permalink = generate_permalink(canonical_name)
|
|
114
|
-
response = await call_get(
|
|
115
|
-
client,
|
|
116
|
-
f"/{current_project_permalink}/project/info",
|
|
117
|
-
params={"project_name": canonical_name},
|
|
118
|
-
)
|
|
119
|
-
project_info = ProjectInfoResponse.model_validate(response.json())
|
|
120
|
-
|
|
121
|
-
result = f"✓ Switched to {canonical_name} project\n\n"
|
|
122
|
-
result += "Project Summary:\n"
|
|
123
|
-
result += f"• {project_info.statistics.total_entities} entities\n"
|
|
124
|
-
result += f"• {project_info.statistics.total_observations} observations\n"
|
|
125
|
-
result += f"• {project_info.statistics.total_relations} relations\n"
|
|
126
|
-
|
|
127
|
-
except Exception as e:
|
|
128
|
-
# If we can't get project info, still confirm the switch
|
|
129
|
-
logger.warning(f"Could not get project info for {canonical_name}: {e}")
|
|
130
|
-
result = f"✓ Switched to {canonical_name} project\n\n"
|
|
131
|
-
result += "Project summary unavailable.\n"
|
|
132
|
-
|
|
133
|
-
return add_project_metadata(result, canonical_name)
|
|
134
|
-
|
|
135
|
-
except Exception as e:
|
|
136
|
-
logger.error(f"Error switching to project {project_name}: {e}")
|
|
137
|
-
# Revert to previous project on error
|
|
138
|
-
session.set_current_project(current_project)
|
|
139
|
-
|
|
140
|
-
# Return user-friendly error message instead of raising exception
|
|
141
|
-
return dedent(f"""
|
|
142
|
-
# Project Switch Failed
|
|
143
|
-
|
|
144
|
-
Could not switch to project '{project_name}': {str(e)}
|
|
145
|
-
|
|
146
|
-
## Current project: {current_project}
|
|
147
|
-
Your session remains on the previous project.
|
|
148
|
-
|
|
149
|
-
## Troubleshooting:
|
|
150
|
-
1. **Check available projects**: Use `list_memory_projects()` to see valid project names
|
|
151
|
-
2. **Verify spelling**: Ensure the project name is spelled correctly
|
|
152
|
-
3. **Check permissions**: Verify you have access to the requested project
|
|
153
|
-
4. **Try again**: The error might be temporary
|
|
154
|
-
|
|
155
|
-
## Available options:
|
|
156
|
-
- See all projects: `list_memory_projects()`
|
|
157
|
-
- Stay on current project: `get_current_project()`
|
|
158
|
-
- Try different project: `switch_project("correct-project-name")`
|
|
159
|
-
|
|
160
|
-
If the project should exist but isn't listed, send a message to support@basicmachines.co.
|
|
161
|
-
""").strip()
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
@mcp.tool()
|
|
165
|
-
async def get_current_project(
|
|
166
|
-
ctx: Context | None = None, _compatibility: Optional[str] = None
|
|
167
|
-
) -> str:
|
|
168
|
-
"""Show the currently active project and basic stats.
|
|
169
|
-
|
|
170
|
-
Displays which project is currently active and provides basic information
|
|
171
|
-
about it.
|
|
172
|
-
|
|
173
|
-
Returns:
|
|
174
|
-
Current project name and basic statistics
|
|
175
|
-
|
|
176
|
-
Example:
|
|
177
|
-
get_current_project()
|
|
178
|
-
"""
|
|
179
|
-
if ctx: # pragma: no cover
|
|
180
|
-
await ctx.info("Getting current project information")
|
|
181
|
-
|
|
182
|
-
current_project = session.get_current_project()
|
|
183
|
-
result = f"Current project: {current_project}\n\n"
|
|
184
|
-
|
|
185
|
-
# get project stats (use permalink in URL path)
|
|
186
|
-
current_project_permalink = generate_permalink(current_project)
|
|
187
|
-
response = await call_get(
|
|
188
|
-
client,
|
|
189
|
-
f"/{current_project_permalink}/project/info",
|
|
190
|
-
params={"project_name": current_project},
|
|
191
|
-
)
|
|
192
|
-
project_info = ProjectInfoResponse.model_validate(response.json())
|
|
193
|
-
|
|
194
|
-
result += f"• {project_info.statistics.total_entities} entities\n"
|
|
195
|
-
result += f"• {project_info.statistics.total_observations} observations\n"
|
|
196
|
-
result += f"• {project_info.statistics.total_relations} relations\n"
|
|
197
|
-
|
|
198
|
-
default_project = session.get_default_project()
|
|
199
|
-
if current_project != default_project:
|
|
200
|
-
result += f"• Default project: {default_project}\n"
|
|
64
|
+
result += "\n" + "─" * 40 + "\n"
|
|
65
|
+
result += "Next: Ask which project to use for this session.\n"
|
|
66
|
+
result += "Example: 'Which project should I use for this task?'\n\n"
|
|
67
|
+
result += "Session reminder: Track the selected project for all subsequent operations in this conversation.\n"
|
|
68
|
+
result += "The user can say 'switch to [project]' to change projects."
|
|
201
69
|
|
|
202
|
-
return
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
@mcp.tool()
|
|
206
|
-
async def set_default_project(project_name: str, ctx: Context | None = None) -> str:
|
|
207
|
-
"""Set default project in config. Requires restart to take effect.
|
|
208
|
-
|
|
209
|
-
Updates the configuration to use a different default project. This change
|
|
210
|
-
only takes effect after restarting the Basic Memory server.
|
|
211
|
-
|
|
212
|
-
Args:
|
|
213
|
-
project_name: Name of the project to set as default
|
|
214
|
-
|
|
215
|
-
Returns:
|
|
216
|
-
Confirmation message about config update
|
|
217
|
-
|
|
218
|
-
Example:
|
|
219
|
-
set_default_project("work-notes")
|
|
220
|
-
"""
|
|
221
|
-
if ctx: # pragma: no cover
|
|
222
|
-
await ctx.info(f"Setting default project to: {project_name}")
|
|
223
|
-
|
|
224
|
-
# Call API to set default project
|
|
225
|
-
response = await call_put(client, f"/projects/{project_name}/default")
|
|
226
|
-
status_response = ProjectStatusResponse.model_validate(response.json())
|
|
227
|
-
|
|
228
|
-
result = f"✓ {status_response.message}\n\n"
|
|
229
|
-
result += "Restart Basic Memory for this change to take effect:\n"
|
|
230
|
-
result += "basic-memory mcp\n"
|
|
231
|
-
|
|
232
|
-
if status_response.old_project:
|
|
233
|
-
result += f"\nPrevious default: {status_response.old_project.name}\n"
|
|
234
|
-
|
|
235
|
-
return add_project_metadata(result, session.get_current_project())
|
|
70
|
+
return result
|
|
236
71
|
|
|
237
72
|
|
|
238
73
|
@mcp.tool("create_memory_project")
|
|
239
74
|
async def create_memory_project(
|
|
240
|
-
project_name: str, project_path: str, set_default: bool = False,
|
|
75
|
+
project_name: str, project_path: str, set_default: bool = False, context: Context | None = None
|
|
241
76
|
) -> str:
|
|
242
77
|
"""Create a new Basic Memory project.
|
|
243
78
|
|
|
@@ -256,8 +91,13 @@ async def create_memory_project(
|
|
|
256
91
|
create_memory_project("my-research", "~/Documents/research")
|
|
257
92
|
create_memory_project("work-notes", "/home/user/work", set_default=True)
|
|
258
93
|
"""
|
|
259
|
-
if
|
|
260
|
-
|
|
94
|
+
# Check if server is constrained to a specific project
|
|
95
|
+
constrained_project = os.environ.get("BASIC_MEMORY_MCP_PROJECT")
|
|
96
|
+
if constrained_project:
|
|
97
|
+
return f'# Error\n\nProject creation disabled - MCP server is constrained to project \'{constrained_project}\'.\nUse the CLI to create projects: `basic-memory project add "{project_name}" "{project_path}"`'
|
|
98
|
+
|
|
99
|
+
if context: # pragma: no cover
|
|
100
|
+
await context.info(f"Creating project: {project_name} at {project_path}")
|
|
261
101
|
|
|
262
102
|
# Create the project request
|
|
263
103
|
project_request = ProjectInfoRequest(
|
|
@@ -278,17 +118,14 @@ async def create_memory_project(
|
|
|
278
118
|
if set_default:
|
|
279
119
|
result += "• Set as default project\n"
|
|
280
120
|
|
|
281
|
-
result += "\nProject is now available for use.\n"
|
|
282
|
-
|
|
283
|
-
# If project was set as default, update session
|
|
284
|
-
if set_default:
|
|
285
|
-
session.set_current_project(project_name)
|
|
121
|
+
result += "\nProject is now available for use in tool calls.\n"
|
|
122
|
+
result += f"Use '{project_name}' as the project parameter in MCP tool calls.\n"
|
|
286
123
|
|
|
287
|
-
return
|
|
124
|
+
return result
|
|
288
125
|
|
|
289
126
|
|
|
290
127
|
@mcp.tool()
|
|
291
|
-
async def delete_project(project_name: str,
|
|
128
|
+
async def delete_project(project_name: str, context: Context | None = None) -> str:
|
|
292
129
|
"""Delete a Basic Memory project.
|
|
293
130
|
|
|
294
131
|
Removes a project from the configuration and database. This does NOT delete
|
|
@@ -308,31 +145,42 @@ async def delete_project(project_name: str, ctx: Context | None = None) -> str:
|
|
|
308
145
|
This action cannot be undone. The project will need to be re-added
|
|
309
146
|
to access its content through Basic Memory again.
|
|
310
147
|
"""
|
|
311
|
-
if
|
|
312
|
-
|
|
148
|
+
# Check if server is constrained to a specific project
|
|
149
|
+
constrained_project = os.environ.get("BASIC_MEMORY_MCP_PROJECT")
|
|
150
|
+
if constrained_project:
|
|
151
|
+
return f"# Error\n\nProject deletion disabled - MCP server is constrained to project '{constrained_project}'.\nUse the CLI to delete projects: `basic-memory project remove \"{project_name}\"`"
|
|
313
152
|
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
# Check if trying to delete current project
|
|
317
|
-
if project_name == current_project:
|
|
318
|
-
raise ValueError(
|
|
319
|
-
f"Cannot delete the currently active project '{project_name}'. Switch to a different project first."
|
|
320
|
-
)
|
|
153
|
+
if context: # pragma: no cover
|
|
154
|
+
await context.info(f"Deleting project: {project_name}")
|
|
321
155
|
|
|
322
156
|
# Get project info before deletion to validate it exists
|
|
323
157
|
response = await call_get(client, "/projects/projects")
|
|
324
158
|
project_list = ProjectList.model_validate(response.json())
|
|
325
159
|
|
|
326
|
-
#
|
|
327
|
-
|
|
328
|
-
|
|
160
|
+
# Find the project by name (case-insensitive) or permalink - same logic as switch_project
|
|
161
|
+
project_permalink = generate_permalink(project_name)
|
|
162
|
+
target_project = None
|
|
163
|
+
for p in project_list.projects:
|
|
164
|
+
# Match by permalink (handles case-insensitive input)
|
|
165
|
+
if p.permalink == project_permalink:
|
|
166
|
+
target_project = p
|
|
167
|
+
break
|
|
168
|
+
# Also match by name comparison (case-insensitive)
|
|
169
|
+
if p.name.lower() == project_name.lower():
|
|
170
|
+
target_project = p
|
|
171
|
+
break
|
|
172
|
+
|
|
173
|
+
if not target_project:
|
|
329
174
|
available_projects = [p.name for p in project_list.projects]
|
|
330
175
|
raise ValueError(
|
|
331
176
|
f"Project '{project_name}' not found. Available projects: {', '.join(available_projects)}"
|
|
332
177
|
)
|
|
333
178
|
|
|
334
|
-
# Call API to delete project
|
|
335
|
-
|
|
179
|
+
# Call API to delete project using URL encoding for special characters
|
|
180
|
+
from urllib.parse import quote
|
|
181
|
+
|
|
182
|
+
encoded_name = quote(target_project.name, safe="")
|
|
183
|
+
response = await call_delete(client, f"/projects/{encoded_name}")
|
|
336
184
|
status_response = ProjectStatusResponse.model_validate(response.json())
|
|
337
185
|
|
|
338
186
|
result = f"✓ {status_response.message}\n\n"
|
|
@@ -346,4 +194,4 @@ async def delete_project(project_name: str, ctx: Context | None = None) -> str:
|
|
|
346
194
|
result += "Files remain on disk but project is no longer tracked by Basic Memory.\n"
|
|
347
195
|
result += "Re-add the project to access its content again.\n"
|
|
348
196
|
|
|
349
|
-
return
|
|
197
|
+
return result
|
|
@@ -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
8
|
import base64
|
|
10
9
|
import io
|
|
11
10
|
|
|
11
|
+
from typing import Optional
|
|
12
|
+
|
|
12
13
|
from loguru import logger
|
|
13
14
|
from PIL import Image as PILImage
|
|
15
|
+
from fastmcp import Context
|
|
14
16
|
|
|
17
|
+
from basic_memory.mcp.project_context import get_active_project
|
|
15
18
|
from basic_memory.mcp.server import mcp
|
|
16
19
|
from basic_memory.mcp.async_client import client
|
|
17
20
|
from basic_memory.mcp.tools.utils import call_get
|
|
18
|
-
from basic_memory.mcp.project_session import get_active_project
|
|
19
21
|
from basic_memory.schemas.memory import memory_url_path
|
|
20
22
|
from basic_memory.utils import validate_project_path
|
|
21
23
|
|
|
@@ -147,11 +149,16 @@ def optimize_image(img, content_length, max_output_bytes=350000):
|
|
|
147
149
|
|
|
148
150
|
|
|
149
151
|
@mcp.tool(description="Read a file's raw content by path or permalink")
|
|
150
|
-
async def read_content(
|
|
152
|
+
async def read_content(
|
|
153
|
+
path: str, project: Optional[str] = None, context: Context | None = None
|
|
154
|
+
) -> dict:
|
|
151
155
|
"""Read a file's raw content by path or permalink.
|
|
152
156
|
|
|
153
157
|
This tool provides direct access to file content in the knowledge base,
|
|
154
|
-
handling different file types appropriately
|
|
158
|
+
handling different file types appropriately. Uses stateless architecture -
|
|
159
|
+
project parameter optional with server resolution.
|
|
160
|
+
|
|
161
|
+
Supported file types:
|
|
155
162
|
- Text files (markdown, code, etc.) are returned as plain text
|
|
156
163
|
- Images are automatically resized/optimized for display
|
|
157
164
|
- Other binary files are returned as base64 if below size limits
|
|
@@ -161,7 +168,9 @@ async def read_content(path: str, project: Optional[str] = None) -> dict:
|
|
|
161
168
|
- A regular file path (docs/example.md)
|
|
162
169
|
- A memory URL (memory://docs/example)
|
|
163
170
|
- A permalink (docs/example)
|
|
164
|
-
project:
|
|
171
|
+
project: Project name to read from. Optional - server will resolve using hierarchy.
|
|
172
|
+
If unknown, use list_memory_projects() to discover available projects.
|
|
173
|
+
context: Optional FastMCP context for performance caching.
|
|
165
174
|
|
|
166
175
|
Returns:
|
|
167
176
|
A dictionary with the file content and metadata:
|
|
@@ -172,20 +181,27 @@ async def read_content(path: str, project: Optional[str] = None) -> dict:
|
|
|
172
181
|
|
|
173
182
|
Examples:
|
|
174
183
|
# Read a markdown file
|
|
175
|
-
result = await
|
|
184
|
+
result = await read_content("docs/project-specs.md")
|
|
176
185
|
|
|
177
186
|
# Read an image
|
|
178
|
-
image_data = await
|
|
187
|
+
image_data = await read_content("assets/diagram.png")
|
|
179
188
|
|
|
180
189
|
# Read using memory URL
|
|
181
|
-
content = await
|
|
190
|
+
content = await read_content("memory://docs/architecture")
|
|
191
|
+
|
|
192
|
+
# Read configuration file
|
|
193
|
+
config = await read_content("config/settings.json")
|
|
194
|
+
|
|
195
|
+
# Explicit project specification
|
|
196
|
+
result = await read_content("docs/project-specs.md", project="my-project")
|
|
182
197
|
|
|
183
|
-
|
|
184
|
-
|
|
198
|
+
Raises:
|
|
199
|
+
HTTPError: If project doesn't exist or is inaccessible
|
|
200
|
+
SecurityError: If path attempts path traversal
|
|
185
201
|
"""
|
|
186
|
-
logger.info("Reading file", path=path)
|
|
202
|
+
logger.info("Reading file", path=path, project=project)
|
|
187
203
|
|
|
188
|
-
active_project = get_active_project(project)
|
|
204
|
+
active_project = await get_active_project(client, project, context)
|
|
189
205
|
project_url = active_project.project_url
|
|
190
206
|
|
|
191
207
|
url = memory_url_path(path)
|