basic-memory 0.16.1__py3-none-any.whl → 0.17.4__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/env.py +112 -26
- basic_memory/alembic/versions/314f1ea54dc4_add_postgres_full_text_search_support_.py +131 -0
- basic_memory/alembic/versions/5fe1ab1ccebe_add_projects_table.py +15 -3
- basic_memory/alembic/versions/647e7a75e2cd_project_constraint_fix.py +44 -36
- basic_memory/alembic/versions/6830751f5fb6_merge_multiple_heads.py +24 -0
- basic_memory/alembic/versions/a2b3c4d5e6f7_add_search_index_entity_cascade.py +56 -0
- basic_memory/alembic/versions/cc7172b46608_update_search_index_schema.py +13 -0
- basic_memory/alembic/versions/f8a9b2c3d4e5_add_pg_trgm_for_fuzzy_link_resolution.py +239 -0
- basic_memory/alembic/versions/g9a0b3c4d5e6_add_external_id_to_project_and_entity.py +173 -0
- basic_memory/api/app.py +45 -24
- basic_memory/api/container.py +133 -0
- basic_memory/api/routers/knowledge_router.py +17 -5
- basic_memory/api/routers/project_router.py +68 -14
- basic_memory/api/routers/resource_router.py +37 -27
- basic_memory/api/routers/utils.py +53 -14
- basic_memory/api/v2/__init__.py +35 -0
- basic_memory/api/v2/routers/__init__.py +21 -0
- basic_memory/api/v2/routers/directory_router.py +93 -0
- basic_memory/api/v2/routers/importer_router.py +181 -0
- basic_memory/api/v2/routers/knowledge_router.py +427 -0
- basic_memory/api/v2/routers/memory_router.py +130 -0
- basic_memory/api/v2/routers/project_router.py +359 -0
- basic_memory/api/v2/routers/prompt_router.py +269 -0
- basic_memory/api/v2/routers/resource_router.py +286 -0
- basic_memory/api/v2/routers/search_router.py +73 -0
- basic_memory/cli/app.py +43 -7
- basic_memory/cli/auth.py +27 -4
- basic_memory/cli/commands/__init__.py +3 -1
- basic_memory/cli/commands/cloud/api_client.py +20 -5
- basic_memory/cli/commands/cloud/cloud_utils.py +13 -6
- basic_memory/cli/commands/cloud/rclone_commands.py +110 -14
- basic_memory/cli/commands/cloud/rclone_installer.py +18 -4
- basic_memory/cli/commands/cloud/upload.py +10 -3
- basic_memory/cli/commands/command_utils.py +52 -4
- basic_memory/cli/commands/db.py +78 -19
- basic_memory/cli/commands/format.py +198 -0
- basic_memory/cli/commands/import_chatgpt.py +12 -8
- basic_memory/cli/commands/import_claude_conversations.py +12 -8
- basic_memory/cli/commands/import_claude_projects.py +12 -8
- basic_memory/cli/commands/import_memory_json.py +12 -8
- basic_memory/cli/commands/mcp.py +8 -26
- basic_memory/cli/commands/project.py +22 -9
- basic_memory/cli/commands/status.py +3 -2
- basic_memory/cli/commands/telemetry.py +81 -0
- basic_memory/cli/container.py +84 -0
- basic_memory/cli/main.py +7 -0
- basic_memory/config.py +177 -77
- basic_memory/db.py +183 -77
- basic_memory/deps/__init__.py +293 -0
- basic_memory/deps/config.py +26 -0
- basic_memory/deps/db.py +56 -0
- basic_memory/deps/importers.py +200 -0
- basic_memory/deps/projects.py +238 -0
- basic_memory/deps/repositories.py +179 -0
- basic_memory/deps/services.py +480 -0
- basic_memory/deps.py +14 -409
- basic_memory/file_utils.py +212 -3
- basic_memory/ignore_utils.py +5 -5
- basic_memory/importers/base.py +40 -19
- basic_memory/importers/chatgpt_importer.py +17 -4
- basic_memory/importers/claude_conversations_importer.py +27 -12
- basic_memory/importers/claude_projects_importer.py +50 -14
- basic_memory/importers/memory_json_importer.py +36 -16
- basic_memory/importers/utils.py +5 -2
- basic_memory/markdown/entity_parser.py +62 -23
- basic_memory/markdown/markdown_processor.py +67 -4
- basic_memory/markdown/plugins.py +4 -2
- basic_memory/markdown/utils.py +10 -1
- basic_memory/mcp/async_client.py +1 -0
- basic_memory/mcp/clients/__init__.py +28 -0
- basic_memory/mcp/clients/directory.py +70 -0
- basic_memory/mcp/clients/knowledge.py +176 -0
- basic_memory/mcp/clients/memory.py +120 -0
- basic_memory/mcp/clients/project.py +89 -0
- basic_memory/mcp/clients/resource.py +71 -0
- basic_memory/mcp/clients/search.py +65 -0
- basic_memory/mcp/container.py +110 -0
- basic_memory/mcp/project_context.py +47 -33
- basic_memory/mcp/prompts/ai_assistant_guide.py +2 -2
- basic_memory/mcp/prompts/recent_activity.py +2 -2
- basic_memory/mcp/prompts/utils.py +3 -3
- basic_memory/mcp/server.py +58 -0
- basic_memory/mcp/tools/build_context.py +14 -14
- basic_memory/mcp/tools/canvas.py +34 -12
- basic_memory/mcp/tools/chatgpt_tools.py +4 -1
- basic_memory/mcp/tools/delete_note.py +31 -7
- basic_memory/mcp/tools/edit_note.py +14 -9
- basic_memory/mcp/tools/list_directory.py +7 -17
- basic_memory/mcp/tools/move_note.py +35 -31
- basic_memory/mcp/tools/project_management.py +29 -25
- basic_memory/mcp/tools/read_content.py +13 -3
- basic_memory/mcp/tools/read_note.py +24 -14
- basic_memory/mcp/tools/recent_activity.py +32 -38
- basic_memory/mcp/tools/search.py +17 -10
- basic_memory/mcp/tools/utils.py +28 -0
- basic_memory/mcp/tools/view_note.py +2 -1
- basic_memory/mcp/tools/write_note.py +37 -14
- basic_memory/models/knowledge.py +15 -2
- basic_memory/models/project.py +7 -1
- basic_memory/models/search.py +58 -2
- basic_memory/project_resolver.py +222 -0
- basic_memory/repository/entity_repository.py +210 -3
- basic_memory/repository/observation_repository.py +1 -0
- basic_memory/repository/postgres_search_repository.py +451 -0
- basic_memory/repository/project_repository.py +38 -1
- basic_memory/repository/relation_repository.py +58 -2
- basic_memory/repository/repository.py +1 -0
- basic_memory/repository/search_index_row.py +95 -0
- basic_memory/repository/search_repository.py +77 -615
- basic_memory/repository/search_repository_base.py +241 -0
- basic_memory/repository/sqlite_search_repository.py +437 -0
- basic_memory/runtime.py +61 -0
- basic_memory/schemas/base.py +36 -6
- basic_memory/schemas/directory.py +2 -1
- basic_memory/schemas/memory.py +9 -2
- basic_memory/schemas/project_info.py +2 -0
- basic_memory/schemas/response.py +84 -27
- basic_memory/schemas/search.py +5 -0
- basic_memory/schemas/sync_report.py +1 -1
- basic_memory/schemas/v2/__init__.py +27 -0
- basic_memory/schemas/v2/entity.py +133 -0
- basic_memory/schemas/v2/resource.py +47 -0
- basic_memory/services/context_service.py +219 -43
- basic_memory/services/directory_service.py +26 -11
- basic_memory/services/entity_service.py +68 -33
- basic_memory/services/file_service.py +131 -16
- basic_memory/services/initialization.py +51 -26
- basic_memory/services/link_resolver.py +1 -0
- basic_memory/services/project_service.py +68 -43
- basic_memory/services/search_service.py +75 -16
- basic_memory/sync/__init__.py +2 -1
- basic_memory/sync/coordinator.py +160 -0
- basic_memory/sync/sync_service.py +135 -115
- basic_memory/sync/watch_service.py +32 -12
- basic_memory/telemetry.py +249 -0
- basic_memory/utils.py +96 -75
- {basic_memory-0.16.1.dist-info → basic_memory-0.17.4.dist-info}/METADATA +129 -5
- basic_memory-0.17.4.dist-info/RECORD +193 -0
- {basic_memory-0.16.1.dist-info → basic_memory-0.17.4.dist-info}/WHEEL +1 -1
- basic_memory-0.16.1.dist-info/RECORD +0 -148
- {basic_memory-0.16.1.dist-info → basic_memory-0.17.4.dist-info}/entry_points.txt +0 -0
- {basic_memory-0.16.1.dist-info → basic_memory-0.17.4.dist-info}/licenses/LICENSE +0 -0
|
@@ -0,0 +1,89 @@
|
|
|
1
|
+
"""Typed client for project API operations.
|
|
2
|
+
|
|
3
|
+
Encapsulates project-level endpoints.
|
|
4
|
+
"""
|
|
5
|
+
|
|
6
|
+
from typing import Any
|
|
7
|
+
|
|
8
|
+
from httpx import AsyncClient
|
|
9
|
+
|
|
10
|
+
from basic_memory.mcp.tools.utils import call_get, call_post, call_delete
|
|
11
|
+
from basic_memory.schemas.project_info import ProjectList, ProjectStatusResponse
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
class ProjectClient:
|
|
15
|
+
"""Typed client for project management operations.
|
|
16
|
+
|
|
17
|
+
Centralizes:
|
|
18
|
+
- API path construction for project endpoints
|
|
19
|
+
- Response validation via Pydantic models
|
|
20
|
+
- Consistent error handling through call_* utilities
|
|
21
|
+
|
|
22
|
+
Note: This client does not require a project_id since it operates
|
|
23
|
+
across projects.
|
|
24
|
+
|
|
25
|
+
Usage:
|
|
26
|
+
async with get_client() as http_client:
|
|
27
|
+
client = ProjectClient(http_client)
|
|
28
|
+
projects = await client.list_projects()
|
|
29
|
+
"""
|
|
30
|
+
|
|
31
|
+
def __init__(self, http_client: AsyncClient):
|
|
32
|
+
"""Initialize the project client.
|
|
33
|
+
|
|
34
|
+
Args:
|
|
35
|
+
http_client: HTTPX AsyncClient for making requests
|
|
36
|
+
"""
|
|
37
|
+
self.http_client = http_client
|
|
38
|
+
|
|
39
|
+
async def list_projects(self) -> ProjectList:
|
|
40
|
+
"""List all available projects.
|
|
41
|
+
|
|
42
|
+
Returns:
|
|
43
|
+
ProjectList with all projects and default project name
|
|
44
|
+
|
|
45
|
+
Raises:
|
|
46
|
+
ToolError: If the request fails
|
|
47
|
+
"""
|
|
48
|
+
response = await call_get(
|
|
49
|
+
self.http_client,
|
|
50
|
+
"/projects/projects",
|
|
51
|
+
)
|
|
52
|
+
return ProjectList.model_validate(response.json())
|
|
53
|
+
|
|
54
|
+
async def create_project(self, project_data: dict[str, Any]) -> ProjectStatusResponse:
|
|
55
|
+
"""Create a new project.
|
|
56
|
+
|
|
57
|
+
Args:
|
|
58
|
+
project_data: Project creation data (name, path, set_default)
|
|
59
|
+
|
|
60
|
+
Returns:
|
|
61
|
+
ProjectStatusResponse with creation result
|
|
62
|
+
|
|
63
|
+
Raises:
|
|
64
|
+
ToolError: If the request fails
|
|
65
|
+
"""
|
|
66
|
+
response = await call_post(
|
|
67
|
+
self.http_client,
|
|
68
|
+
"/projects/projects",
|
|
69
|
+
json=project_data,
|
|
70
|
+
)
|
|
71
|
+
return ProjectStatusResponse.model_validate(response.json())
|
|
72
|
+
|
|
73
|
+
async def delete_project(self, project_external_id: str) -> ProjectStatusResponse:
|
|
74
|
+
"""Delete a project by its external ID.
|
|
75
|
+
|
|
76
|
+
Args:
|
|
77
|
+
project_external_id: Project external ID (UUID)
|
|
78
|
+
|
|
79
|
+
Returns:
|
|
80
|
+
ProjectStatusResponse with deletion result
|
|
81
|
+
|
|
82
|
+
Raises:
|
|
83
|
+
ToolError: If the request fails
|
|
84
|
+
"""
|
|
85
|
+
response = await call_delete(
|
|
86
|
+
self.http_client,
|
|
87
|
+
f"/v2/projects/{project_external_id}",
|
|
88
|
+
)
|
|
89
|
+
return ProjectStatusResponse.model_validate(response.json())
|
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
"""Typed client for resource API operations.
|
|
2
|
+
|
|
3
|
+
Encapsulates all /v2/projects/{project_id}/resource/* endpoints.
|
|
4
|
+
"""
|
|
5
|
+
|
|
6
|
+
from typing import Optional
|
|
7
|
+
|
|
8
|
+
from httpx import AsyncClient, Response
|
|
9
|
+
|
|
10
|
+
from basic_memory.mcp.tools.utils import call_get
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
class ResourceClient:
|
|
14
|
+
"""Typed client for resource operations.
|
|
15
|
+
|
|
16
|
+
Centralizes:
|
|
17
|
+
- API path construction for /v2/projects/{project_id}/resource/*
|
|
18
|
+
- Consistent error handling through call_* utilities
|
|
19
|
+
|
|
20
|
+
Note: This client returns raw Response objects for resources since they
|
|
21
|
+
may be text, images, or other binary content that needs special handling.
|
|
22
|
+
|
|
23
|
+
Usage:
|
|
24
|
+
async with get_client() as http_client:
|
|
25
|
+
client = ResourceClient(http_client, project_id)
|
|
26
|
+
response = await client.read(entity_id)
|
|
27
|
+
text = response.text
|
|
28
|
+
"""
|
|
29
|
+
|
|
30
|
+
def __init__(self, http_client: AsyncClient, project_id: str):
|
|
31
|
+
"""Initialize the resource client.
|
|
32
|
+
|
|
33
|
+
Args:
|
|
34
|
+
http_client: HTTPX AsyncClient for making requests
|
|
35
|
+
project_id: Project external_id (UUID) for API calls
|
|
36
|
+
"""
|
|
37
|
+
self.http_client = http_client
|
|
38
|
+
self.project_id = project_id
|
|
39
|
+
self._base_path = f"/v2/projects/{project_id}/resource"
|
|
40
|
+
|
|
41
|
+
async def read(
|
|
42
|
+
self,
|
|
43
|
+
entity_id: str,
|
|
44
|
+
*,
|
|
45
|
+
page: Optional[int] = None,
|
|
46
|
+
page_size: Optional[int] = None,
|
|
47
|
+
) -> Response:
|
|
48
|
+
"""Read a resource by entity ID.
|
|
49
|
+
|
|
50
|
+
Args:
|
|
51
|
+
entity_id: Entity external_id (UUID)
|
|
52
|
+
page: Optional page number for paginated content
|
|
53
|
+
page_size: Optional page size for paginated content
|
|
54
|
+
|
|
55
|
+
Returns:
|
|
56
|
+
Raw HTTP Response (caller handles text/binary content)
|
|
57
|
+
|
|
58
|
+
Raises:
|
|
59
|
+
ToolError: If the resource is not found or request fails
|
|
60
|
+
"""
|
|
61
|
+
params: dict = {}
|
|
62
|
+
if page is not None:
|
|
63
|
+
params["page"] = page
|
|
64
|
+
if page_size is not None:
|
|
65
|
+
params["page_size"] = page_size
|
|
66
|
+
|
|
67
|
+
return await call_get(
|
|
68
|
+
self.http_client,
|
|
69
|
+
f"{self._base_path}/{entity_id}",
|
|
70
|
+
params=params if params else None,
|
|
71
|
+
)
|
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
"""Typed client for search API operations.
|
|
2
|
+
|
|
3
|
+
Encapsulates all /v2/projects/{project_id}/search/* endpoints.
|
|
4
|
+
"""
|
|
5
|
+
|
|
6
|
+
from typing import Any
|
|
7
|
+
|
|
8
|
+
from httpx import AsyncClient
|
|
9
|
+
|
|
10
|
+
from basic_memory.mcp.tools.utils import call_post
|
|
11
|
+
from basic_memory.schemas.search import SearchResponse
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
class SearchClient:
|
|
15
|
+
"""Typed client for search operations.
|
|
16
|
+
|
|
17
|
+
Centralizes:
|
|
18
|
+
- API path construction for /v2/projects/{project_id}/search/*
|
|
19
|
+
- Response validation via Pydantic models
|
|
20
|
+
- Consistent error handling through call_* utilities
|
|
21
|
+
|
|
22
|
+
Usage:
|
|
23
|
+
async with get_client() as http_client:
|
|
24
|
+
client = SearchClient(http_client, project_id)
|
|
25
|
+
results = await client.search(search_query.model_dump())
|
|
26
|
+
"""
|
|
27
|
+
|
|
28
|
+
def __init__(self, http_client: AsyncClient, project_id: str):
|
|
29
|
+
"""Initialize the search client.
|
|
30
|
+
|
|
31
|
+
Args:
|
|
32
|
+
http_client: HTTPX AsyncClient for making requests
|
|
33
|
+
project_id: Project external_id (UUID) for API calls
|
|
34
|
+
"""
|
|
35
|
+
self.http_client = http_client
|
|
36
|
+
self.project_id = project_id
|
|
37
|
+
self._base_path = f"/v2/projects/{project_id}/search"
|
|
38
|
+
|
|
39
|
+
async def search(
|
|
40
|
+
self,
|
|
41
|
+
query: dict[str, Any],
|
|
42
|
+
*,
|
|
43
|
+
page: int = 1,
|
|
44
|
+
page_size: int = 10,
|
|
45
|
+
) -> SearchResponse:
|
|
46
|
+
"""Search across all content in the knowledge base.
|
|
47
|
+
|
|
48
|
+
Args:
|
|
49
|
+
query: Search query dict (from SearchQuery.model_dump())
|
|
50
|
+
page: Page number (1-indexed)
|
|
51
|
+
page_size: Results per page
|
|
52
|
+
|
|
53
|
+
Returns:
|
|
54
|
+
SearchResponse with results and pagination
|
|
55
|
+
|
|
56
|
+
Raises:
|
|
57
|
+
ToolError: If the request fails
|
|
58
|
+
"""
|
|
59
|
+
response = await call_post(
|
|
60
|
+
self.http_client,
|
|
61
|
+
f"{self._base_path}/",
|
|
62
|
+
json=query,
|
|
63
|
+
params={"page": page, "page_size": page_size},
|
|
64
|
+
)
|
|
65
|
+
return SearchResponse.model_validate(response.json())
|
|
@@ -0,0 +1,110 @@
|
|
|
1
|
+
"""MCP composition root for Basic Memory.
|
|
2
|
+
|
|
3
|
+
This container owns reading ConfigManager and environment variables for the
|
|
4
|
+
MCP server entrypoint. Downstream modules receive config/dependencies explicitly
|
|
5
|
+
rather than reading globals.
|
|
6
|
+
|
|
7
|
+
Design principles:
|
|
8
|
+
- Only this module reads ConfigManager directly
|
|
9
|
+
- Runtime mode (cloud/local/test) is resolved here
|
|
10
|
+
- File sync decisions are centralized here
|
|
11
|
+
"""
|
|
12
|
+
|
|
13
|
+
from dataclasses import dataclass
|
|
14
|
+
from typing import TYPE_CHECKING
|
|
15
|
+
|
|
16
|
+
from basic_memory.config import BasicMemoryConfig, ConfigManager
|
|
17
|
+
from basic_memory.runtime import RuntimeMode, resolve_runtime_mode
|
|
18
|
+
|
|
19
|
+
if TYPE_CHECKING: # pragma: no cover
|
|
20
|
+
from basic_memory.sync import SyncCoordinator
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
@dataclass
|
|
24
|
+
class McpContainer:
|
|
25
|
+
"""Composition root for the MCP server entrypoint.
|
|
26
|
+
|
|
27
|
+
Holds resolved configuration and runtime context.
|
|
28
|
+
Created once at server startup, then used to wire dependencies.
|
|
29
|
+
"""
|
|
30
|
+
|
|
31
|
+
config: BasicMemoryConfig
|
|
32
|
+
mode: RuntimeMode
|
|
33
|
+
|
|
34
|
+
@classmethod
|
|
35
|
+
def create(cls) -> "McpContainer":
|
|
36
|
+
"""Create container by reading ConfigManager.
|
|
37
|
+
|
|
38
|
+
This is the single point where MCP reads global config.
|
|
39
|
+
"""
|
|
40
|
+
config = ConfigManager().config
|
|
41
|
+
mode = resolve_runtime_mode(
|
|
42
|
+
cloud_mode_enabled=config.cloud_mode_enabled,
|
|
43
|
+
is_test_env=config.is_test_env,
|
|
44
|
+
)
|
|
45
|
+
return cls(config=config, mode=mode)
|
|
46
|
+
|
|
47
|
+
# --- Runtime Mode Properties ---
|
|
48
|
+
|
|
49
|
+
@property
|
|
50
|
+
def should_sync_files(self) -> bool:
|
|
51
|
+
"""Whether local file sync should be started.
|
|
52
|
+
|
|
53
|
+
Sync is enabled when:
|
|
54
|
+
- sync_changes is True in config
|
|
55
|
+
- Not in test mode (tests manage their own sync)
|
|
56
|
+
- Not in cloud mode (cloud handles sync differently)
|
|
57
|
+
"""
|
|
58
|
+
return (
|
|
59
|
+
self.config.sync_changes and not self.mode.is_test and not self.mode.is_cloud
|
|
60
|
+
)
|
|
61
|
+
|
|
62
|
+
@property
|
|
63
|
+
def sync_skip_reason(self) -> str | None:
|
|
64
|
+
"""Reason why sync is skipped, or None if sync should run.
|
|
65
|
+
|
|
66
|
+
Useful for logging why sync was disabled.
|
|
67
|
+
"""
|
|
68
|
+
if self.mode.is_test:
|
|
69
|
+
return "Test environment detected"
|
|
70
|
+
if self.mode.is_cloud:
|
|
71
|
+
return "Cloud mode enabled"
|
|
72
|
+
if not self.config.sync_changes:
|
|
73
|
+
return "Sync changes disabled"
|
|
74
|
+
return None
|
|
75
|
+
|
|
76
|
+
def create_sync_coordinator(self) -> "SyncCoordinator":
|
|
77
|
+
"""Create a SyncCoordinator with this container's settings.
|
|
78
|
+
|
|
79
|
+
Returns:
|
|
80
|
+
SyncCoordinator configured for this runtime environment
|
|
81
|
+
"""
|
|
82
|
+
# Deferred import to avoid circular dependency
|
|
83
|
+
from basic_memory.sync import SyncCoordinator
|
|
84
|
+
|
|
85
|
+
return SyncCoordinator(
|
|
86
|
+
config=self.config,
|
|
87
|
+
should_sync=self.should_sync_files,
|
|
88
|
+
skip_reason=self.sync_skip_reason,
|
|
89
|
+
)
|
|
90
|
+
|
|
91
|
+
|
|
92
|
+
# Module-level container instance (set by lifespan)
|
|
93
|
+
_container: McpContainer | None = None
|
|
94
|
+
|
|
95
|
+
|
|
96
|
+
def get_container() -> McpContainer:
|
|
97
|
+
"""Get the current MCP container.
|
|
98
|
+
|
|
99
|
+
Raises:
|
|
100
|
+
RuntimeError: If container hasn't been initialized
|
|
101
|
+
"""
|
|
102
|
+
if _container is None:
|
|
103
|
+
raise RuntimeError("MCP container not initialized. Call set_container() first.")
|
|
104
|
+
return _container
|
|
105
|
+
|
|
106
|
+
|
|
107
|
+
def set_container(container: McpContainer) -> None:
|
|
108
|
+
"""Set the MCP container (called by lifespan)."""
|
|
109
|
+
global _container
|
|
110
|
+
_container = container
|
|
@@ -2,9 +2,12 @@
|
|
|
2
2
|
|
|
3
3
|
Provides project lookup utilities for MCP tools.
|
|
4
4
|
Handles project validation and context management in one place.
|
|
5
|
+
|
|
6
|
+
Note: This module uses ProjectResolver for unified project resolution.
|
|
7
|
+
The resolve_project_parameter function is a thin wrapper for backwards
|
|
8
|
+
compatibility with existing MCP tools.
|
|
5
9
|
"""
|
|
6
10
|
|
|
7
|
-
import os
|
|
8
11
|
from typing import Optional, List
|
|
9
12
|
from httpx import AsyncClient
|
|
10
13
|
from httpx._types import (
|
|
@@ -14,16 +17,26 @@ from loguru import logger
|
|
|
14
17
|
from fastmcp import Context
|
|
15
18
|
|
|
16
19
|
from basic_memory.config import ConfigManager
|
|
17
|
-
from basic_memory.
|
|
20
|
+
from basic_memory.project_resolver import ProjectResolver
|
|
18
21
|
from basic_memory.schemas.project_info import ProjectItem, ProjectList
|
|
19
22
|
from basic_memory.utils import generate_permalink
|
|
20
23
|
|
|
21
24
|
|
|
22
|
-
async def resolve_project_parameter(
|
|
25
|
+
async def resolve_project_parameter(
|
|
26
|
+
project: Optional[str] = None,
|
|
27
|
+
allow_discovery: bool = False,
|
|
28
|
+
cloud_mode: Optional[bool] = None,
|
|
29
|
+
default_project_mode: Optional[bool] = None,
|
|
30
|
+
default_project: Optional[str] = None,
|
|
31
|
+
) -> Optional[str]:
|
|
23
32
|
"""Resolve project parameter using three-tier hierarchy.
|
|
24
33
|
|
|
25
|
-
|
|
26
|
-
|
|
34
|
+
This is a thin wrapper around ProjectResolver for backwards compatibility.
|
|
35
|
+
New code should consider using ProjectResolver directly for more detailed
|
|
36
|
+
resolution information.
|
|
37
|
+
|
|
38
|
+
if cloud_mode:
|
|
39
|
+
project is required (unless allow_discovery=True for tools that support discovery mode)
|
|
27
40
|
else:
|
|
28
41
|
Resolution order:
|
|
29
42
|
1. Single Project Mode (--project cli arg, or BASIC_MEMORY_MCP_PROJECT env var) - highest priority
|
|
@@ -32,41 +45,39 @@ async def resolve_project_parameter(project: Optional[str] = None) -> Optional[s
|
|
|
32
45
|
|
|
33
46
|
Args:
|
|
34
47
|
project: Optional explicit project parameter
|
|
48
|
+
allow_discovery: If True, allows returning None in cloud mode for discovery mode
|
|
49
|
+
(used by tools like recent_activity that can operate across all projects)
|
|
50
|
+
cloud_mode: Optional explicit cloud mode. If not provided, reads from ConfigManager.
|
|
51
|
+
default_project_mode: Optional explicit default project mode. If not provided, reads from ConfigManager.
|
|
52
|
+
default_project: Optional explicit default project. If not provided, reads from ConfigManager.
|
|
35
53
|
|
|
36
54
|
Returns:
|
|
37
55
|
Resolved project name or None if no resolution possible
|
|
38
56
|
"""
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
#
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
logger.debug(f"Using explicit project parameter: {project}")
|
|
58
|
-
return project
|
|
59
|
-
|
|
60
|
-
# Priority 3: Default project mode
|
|
61
|
-
if config.default_project_mode:
|
|
62
|
-
logger.debug(f"Using default project from config: {config.default_project}")
|
|
63
|
-
return config.default_project
|
|
64
|
-
|
|
65
|
-
# No resolution possible
|
|
66
|
-
return None
|
|
57
|
+
# Load config for any values not explicitly provided
|
|
58
|
+
if cloud_mode is None or default_project_mode is None or default_project is None:
|
|
59
|
+
config = ConfigManager().config
|
|
60
|
+
if cloud_mode is None:
|
|
61
|
+
cloud_mode = config.cloud_mode
|
|
62
|
+
if default_project_mode is None:
|
|
63
|
+
default_project_mode = config.default_project_mode
|
|
64
|
+
if default_project is None:
|
|
65
|
+
default_project = config.default_project
|
|
66
|
+
|
|
67
|
+
# Create resolver with configuration and resolve
|
|
68
|
+
resolver = ProjectResolver.from_env(
|
|
69
|
+
cloud_mode=cloud_mode,
|
|
70
|
+
default_project_mode=default_project_mode,
|
|
71
|
+
default_project=default_project,
|
|
72
|
+
)
|
|
73
|
+
result = resolver.resolve(project=project, allow_discovery=allow_discovery)
|
|
74
|
+
return result.project
|
|
67
75
|
|
|
68
76
|
|
|
69
77
|
async def get_project_names(client: AsyncClient, headers: HeaderTypes | None = None) -> List[str]:
|
|
78
|
+
# Deferred import to avoid circular dependency with tools
|
|
79
|
+
from basic_memory.mcp.tools.utils import call_get
|
|
80
|
+
|
|
70
81
|
response = await call_get(client, "/projects/projects", headers=headers)
|
|
71
82
|
project_list = ProjectList.model_validate(response.json())
|
|
72
83
|
return [project.name for project in project_list.projects]
|
|
@@ -92,6 +103,9 @@ async def get_active_project(
|
|
|
92
103
|
ValueError: If no project can be resolved
|
|
93
104
|
HTTPError: If project doesn't exist or is inaccessible
|
|
94
105
|
"""
|
|
106
|
+
# Deferred import to avoid circular dependency with tools
|
|
107
|
+
from basic_memory.mcp.tools.utils import call_get
|
|
108
|
+
|
|
95
109
|
resolved_project = await resolve_project_parameter(project)
|
|
96
110
|
if not resolved_project:
|
|
97
111
|
project_names = await get_project_names(client, headers)
|
|
@@ -32,7 +32,7 @@ def ai_assistant_guide() -> str:
|
|
|
32
32
|
|
|
33
33
|
# Add mode-specific header
|
|
34
34
|
mode_info = ""
|
|
35
|
-
if config.default_project_mode:
|
|
35
|
+
if config.default_project_mode: # pragma: no cover
|
|
36
36
|
mode_info = f"""
|
|
37
37
|
# 🎯 Default Project Mode Active
|
|
38
38
|
|
|
@@ -46,7 +46,7 @@ def ai_assistant_guide() -> str:
|
|
|
46
46
|
────────────────────────────────────────
|
|
47
47
|
|
|
48
48
|
"""
|
|
49
|
-
else:
|
|
49
|
+
else: # pragma: no cover
|
|
50
50
|
mode_info = """
|
|
51
51
|
# 🔧 Multi-Project Mode Active
|
|
52
52
|
|
|
@@ -64,7 +64,7 @@ async def recent_activity_prompt(
|
|
|
64
64
|
primary_results.append(item.primary_result)
|
|
65
65
|
# Add up to 1 related result per primary item
|
|
66
66
|
if item.related_results:
|
|
67
|
-
related_results.extend(item.related_results[:1])
|
|
67
|
+
related_results.extend(item.related_results[:1]) # pragma: no cover
|
|
68
68
|
|
|
69
69
|
# Limit total results for readability
|
|
70
70
|
primary_results = primary_results[:8]
|
|
@@ -78,7 +78,7 @@ async def recent_activity_prompt(
|
|
|
78
78
|
primary_results.append(item.primary_result)
|
|
79
79
|
# Add up to 2 related results per primary item
|
|
80
80
|
if item.related_results:
|
|
81
|
-
related_results.extend(item.related_results[:2])
|
|
81
|
+
related_results.extend(item.related_results[:2]) # pragma: no cover
|
|
82
82
|
|
|
83
83
|
# Set topic based on mode
|
|
84
84
|
if project:
|
|
@@ -123,9 +123,9 @@ def format_prompt_context(context: PromptContext) -> str:
|
|
|
123
123
|
|
|
124
124
|
# Add content snippet
|
|
125
125
|
if hasattr(primary, "content") and primary.content: # pyright: ignore
|
|
126
|
-
content = primary.content or "" # pyright: ignore
|
|
127
|
-
if content:
|
|
128
|
-
section += f"\n**Excerpt**:\n{content}\n"
|
|
126
|
+
content = primary.content or "" # pyright: ignore # pragma: no cover
|
|
127
|
+
if content: # pragma: no cover
|
|
128
|
+
section += f"\n**Excerpt**:\n{content}\n" # pragma: no cover
|
|
129
129
|
|
|
130
130
|
section += dedent(f"""
|
|
131
131
|
|
basic_memory/mcp/server.py
CHANGED
|
@@ -2,8 +2,66 @@
|
|
|
2
2
|
Basic Memory FastMCP server.
|
|
3
3
|
"""
|
|
4
4
|
|
|
5
|
+
from contextlib import asynccontextmanager
|
|
6
|
+
|
|
5
7
|
from fastmcp import FastMCP
|
|
8
|
+
from loguru import logger
|
|
9
|
+
|
|
10
|
+
from basic_memory import db
|
|
11
|
+
from basic_memory.mcp.container import McpContainer, set_container
|
|
12
|
+
from basic_memory.services.initialization import initialize_app
|
|
13
|
+
from basic_memory.telemetry import show_notice_if_needed, track_app_started
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
@asynccontextmanager
|
|
17
|
+
async def lifespan(app: FastMCP):
|
|
18
|
+
"""Lifecycle manager for the MCP server.
|
|
19
|
+
|
|
20
|
+
Handles:
|
|
21
|
+
- Database initialization and migrations
|
|
22
|
+
- Telemetry notice and tracking
|
|
23
|
+
- File sync via SyncCoordinator (if enabled and not in cloud mode)
|
|
24
|
+
- Proper cleanup on shutdown
|
|
25
|
+
"""
|
|
26
|
+
# --- Composition Root ---
|
|
27
|
+
# Create container and read config (single point of config access)
|
|
28
|
+
container = McpContainer.create()
|
|
29
|
+
set_container(container)
|
|
30
|
+
|
|
31
|
+
logger.info(f"Starting Basic Memory MCP server (mode={container.mode.name})")
|
|
32
|
+
|
|
33
|
+
# Show telemetry notice (first run only) and track startup
|
|
34
|
+
show_notice_if_needed()
|
|
35
|
+
track_app_started("mcp")
|
|
36
|
+
|
|
37
|
+
# Track if we created the engine (vs test fixtures providing it)
|
|
38
|
+
# This prevents disposing an engine provided by test fixtures when
|
|
39
|
+
# multiple Client connections are made in the same test
|
|
40
|
+
engine_was_none = db._engine is None
|
|
41
|
+
|
|
42
|
+
# Initialize app (runs migrations, reconciles projects)
|
|
43
|
+
await initialize_app(container.config)
|
|
44
|
+
|
|
45
|
+
# Create and start sync coordinator (lifecycle centralized in coordinator)
|
|
46
|
+
sync_coordinator = container.create_sync_coordinator()
|
|
47
|
+
await sync_coordinator.start()
|
|
48
|
+
|
|
49
|
+
try:
|
|
50
|
+
yield
|
|
51
|
+
finally:
|
|
52
|
+
# Shutdown - coordinator handles clean task cancellation
|
|
53
|
+
logger.info("Shutting down Basic Memory MCP server")
|
|
54
|
+
await sync_coordinator.stop()
|
|
55
|
+
|
|
56
|
+
# Only shutdown DB if we created it (not if test fixture provided it)
|
|
57
|
+
if engine_was_none:
|
|
58
|
+
await db.shutdown_db()
|
|
59
|
+
logger.info("Database connections closed")
|
|
60
|
+
else: # pragma: no cover
|
|
61
|
+
logger.debug("Skipping DB shutdown - engine provided externally")
|
|
62
|
+
|
|
6
63
|
|
|
7
64
|
mcp = FastMCP(
|
|
8
65
|
name="Basic Memory",
|
|
66
|
+
lifespan=lifespan,
|
|
9
67
|
)
|
|
@@ -8,7 +8,7 @@ from fastmcp import Context
|
|
|
8
8
|
from basic_memory.mcp.async_client import get_client
|
|
9
9
|
from basic_memory.mcp.project_context import get_active_project
|
|
10
10
|
from basic_memory.mcp.server import mcp
|
|
11
|
-
from basic_memory.
|
|
11
|
+
from basic_memory.telemetry import track_mcp_tool
|
|
12
12
|
from basic_memory.schemas.base import TimeFrame
|
|
13
13
|
from basic_memory.schemas.memory import (
|
|
14
14
|
GraphContext,
|
|
@@ -87,6 +87,7 @@ async def build_context(
|
|
|
87
87
|
Raises:
|
|
88
88
|
ToolError: If project doesn't exist or depth parameter is invalid
|
|
89
89
|
"""
|
|
90
|
+
track_mcp_tool("build_context")
|
|
90
91
|
logger.info(f"Building context from {url} in project {project}")
|
|
91
92
|
|
|
92
93
|
# Convert string depth to integer if needed
|
|
@@ -104,17 +105,16 @@ async def build_context(
|
|
|
104
105
|
# Get the active project using the new stateless approach
|
|
105
106
|
active_project = await get_active_project(client, project, context)
|
|
106
107
|
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
108
|
+
# Import here to avoid circular import
|
|
109
|
+
from basic_memory.mcp.clients import MemoryClient
|
|
110
|
+
|
|
111
|
+
# Use typed MemoryClient for API calls
|
|
112
|
+
memory_client = MemoryClient(client, active_project.external_id)
|
|
113
|
+
return await memory_client.build_context(
|
|
114
|
+
memory_url_path(url),
|
|
115
|
+
depth=depth or 1,
|
|
116
|
+
timeframe=timeframe,
|
|
117
|
+
page=page,
|
|
118
|
+
page_size=page_size,
|
|
119
|
+
max_related=max_related,
|
|
119
120
|
)
|
|
120
|
-
return GraphContext.model_validate(response.json())
|