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

Files changed (195) hide show
  1. basic_memory/__init__.py +5 -1
  2. basic_memory/alembic/alembic.ini +119 -0
  3. basic_memory/alembic/env.py +130 -20
  4. basic_memory/alembic/migrations.py +4 -9
  5. basic_memory/alembic/versions/314f1ea54dc4_add_postgres_full_text_search_support_.py +131 -0
  6. basic_memory/alembic/versions/502b60eaa905_remove_required_from_entity_permalink.py +51 -0
  7. basic_memory/alembic/versions/5fe1ab1ccebe_add_projects_table.py +120 -0
  8. basic_memory/alembic/versions/647e7a75e2cd_project_constraint_fix.py +112 -0
  9. basic_memory/alembic/versions/6830751f5fb6_merge_multiple_heads.py +24 -0
  10. basic_memory/alembic/versions/9d9c1cb7d8f5_add_mtime_and_size_columns_to_entity_.py +49 -0
  11. basic_memory/alembic/versions/a1b2c3d4e5f6_fix_project_foreign_keys.py +49 -0
  12. basic_memory/alembic/versions/a2b3c4d5e6f7_add_search_index_entity_cascade.py +56 -0
  13. basic_memory/alembic/versions/b3c3938bacdb_relation_to_name_unique_index.py +44 -0
  14. basic_memory/alembic/versions/cc7172b46608_update_search_index_schema.py +113 -0
  15. basic_memory/alembic/versions/e7e1f4367280_add_scan_watermark_tracking_to_project.py +37 -0
  16. basic_memory/alembic/versions/f8a9b2c3d4e5_add_pg_trgm_for_fuzzy_link_resolution.py +239 -0
  17. basic_memory/alembic/versions/g9a0b3c4d5e6_add_external_id_to_project_and_entity.py +173 -0
  18. basic_memory/api/app.py +87 -20
  19. basic_memory/api/container.py +133 -0
  20. basic_memory/api/routers/__init__.py +4 -1
  21. basic_memory/api/routers/directory_router.py +84 -0
  22. basic_memory/api/routers/importer_router.py +152 -0
  23. basic_memory/api/routers/knowledge_router.py +180 -23
  24. basic_memory/api/routers/management_router.py +80 -0
  25. basic_memory/api/routers/memory_router.py +9 -64
  26. basic_memory/api/routers/project_router.py +460 -0
  27. basic_memory/api/routers/prompt_router.py +260 -0
  28. basic_memory/api/routers/resource_router.py +136 -11
  29. basic_memory/api/routers/search_router.py +5 -5
  30. basic_memory/api/routers/utils.py +169 -0
  31. basic_memory/api/template_loader.py +292 -0
  32. basic_memory/api/v2/__init__.py +35 -0
  33. basic_memory/api/v2/routers/__init__.py +21 -0
  34. basic_memory/api/v2/routers/directory_router.py +93 -0
  35. basic_memory/api/v2/routers/importer_router.py +181 -0
  36. basic_memory/api/v2/routers/knowledge_router.py +427 -0
  37. basic_memory/api/v2/routers/memory_router.py +130 -0
  38. basic_memory/api/v2/routers/project_router.py +359 -0
  39. basic_memory/api/v2/routers/prompt_router.py +269 -0
  40. basic_memory/api/v2/routers/resource_router.py +286 -0
  41. basic_memory/api/v2/routers/search_router.py +73 -0
  42. basic_memory/cli/app.py +80 -10
  43. basic_memory/cli/auth.py +300 -0
  44. basic_memory/cli/commands/__init__.py +15 -2
  45. basic_memory/cli/commands/cloud/__init__.py +6 -0
  46. basic_memory/cli/commands/cloud/api_client.py +127 -0
  47. basic_memory/cli/commands/cloud/bisync_commands.py +110 -0
  48. basic_memory/cli/commands/cloud/cloud_utils.py +108 -0
  49. basic_memory/cli/commands/cloud/core_commands.py +195 -0
  50. basic_memory/cli/commands/cloud/rclone_commands.py +397 -0
  51. basic_memory/cli/commands/cloud/rclone_config.py +110 -0
  52. basic_memory/cli/commands/cloud/rclone_installer.py +263 -0
  53. basic_memory/cli/commands/cloud/upload.py +240 -0
  54. basic_memory/cli/commands/cloud/upload_command.py +124 -0
  55. basic_memory/cli/commands/command_utils.py +99 -0
  56. basic_memory/cli/commands/db.py +87 -12
  57. basic_memory/cli/commands/format.py +198 -0
  58. basic_memory/cli/commands/import_chatgpt.py +47 -223
  59. basic_memory/cli/commands/import_claude_conversations.py +48 -171
  60. basic_memory/cli/commands/import_claude_projects.py +53 -160
  61. basic_memory/cli/commands/import_memory_json.py +55 -111
  62. basic_memory/cli/commands/mcp.py +67 -11
  63. basic_memory/cli/commands/project.py +889 -0
  64. basic_memory/cli/commands/status.py +52 -34
  65. basic_memory/cli/commands/telemetry.py +81 -0
  66. basic_memory/cli/commands/tool.py +341 -0
  67. basic_memory/cli/container.py +84 -0
  68. basic_memory/cli/main.py +14 -6
  69. basic_memory/config.py +580 -26
  70. basic_memory/db.py +285 -28
  71. basic_memory/deps/__init__.py +293 -0
  72. basic_memory/deps/config.py +26 -0
  73. basic_memory/deps/db.py +56 -0
  74. basic_memory/deps/importers.py +200 -0
  75. basic_memory/deps/projects.py +238 -0
  76. basic_memory/deps/repositories.py +179 -0
  77. basic_memory/deps/services.py +480 -0
  78. basic_memory/deps.py +16 -185
  79. basic_memory/file_utils.py +318 -54
  80. basic_memory/ignore_utils.py +297 -0
  81. basic_memory/importers/__init__.py +27 -0
  82. basic_memory/importers/base.py +100 -0
  83. basic_memory/importers/chatgpt_importer.py +245 -0
  84. basic_memory/importers/claude_conversations_importer.py +192 -0
  85. basic_memory/importers/claude_projects_importer.py +184 -0
  86. basic_memory/importers/memory_json_importer.py +128 -0
  87. basic_memory/importers/utils.py +61 -0
  88. basic_memory/markdown/entity_parser.py +182 -23
  89. basic_memory/markdown/markdown_processor.py +70 -7
  90. basic_memory/markdown/plugins.py +43 -23
  91. basic_memory/markdown/schemas.py +1 -1
  92. basic_memory/markdown/utils.py +38 -14
  93. basic_memory/mcp/async_client.py +135 -4
  94. basic_memory/mcp/clients/__init__.py +28 -0
  95. basic_memory/mcp/clients/directory.py +70 -0
  96. basic_memory/mcp/clients/knowledge.py +176 -0
  97. basic_memory/mcp/clients/memory.py +120 -0
  98. basic_memory/mcp/clients/project.py +89 -0
  99. basic_memory/mcp/clients/resource.py +71 -0
  100. basic_memory/mcp/clients/search.py +65 -0
  101. basic_memory/mcp/container.py +110 -0
  102. basic_memory/mcp/project_context.py +155 -0
  103. basic_memory/mcp/prompts/__init__.py +19 -0
  104. basic_memory/mcp/prompts/ai_assistant_guide.py +70 -0
  105. basic_memory/mcp/prompts/continue_conversation.py +62 -0
  106. basic_memory/mcp/prompts/recent_activity.py +188 -0
  107. basic_memory/mcp/prompts/search.py +57 -0
  108. basic_memory/mcp/prompts/utils.py +162 -0
  109. basic_memory/mcp/resources/ai_assistant_guide.md +283 -0
  110. basic_memory/mcp/resources/project_info.py +71 -0
  111. basic_memory/mcp/server.py +61 -9
  112. basic_memory/mcp/tools/__init__.py +33 -21
  113. basic_memory/mcp/tools/build_context.py +120 -0
  114. basic_memory/mcp/tools/canvas.py +152 -0
  115. basic_memory/mcp/tools/chatgpt_tools.py +190 -0
  116. basic_memory/mcp/tools/delete_note.py +249 -0
  117. basic_memory/mcp/tools/edit_note.py +325 -0
  118. basic_memory/mcp/tools/list_directory.py +157 -0
  119. basic_memory/mcp/tools/move_note.py +549 -0
  120. basic_memory/mcp/tools/project_management.py +204 -0
  121. basic_memory/mcp/tools/read_content.py +281 -0
  122. basic_memory/mcp/tools/read_note.py +265 -0
  123. basic_memory/mcp/tools/recent_activity.py +528 -0
  124. basic_memory/mcp/tools/search.py +377 -24
  125. basic_memory/mcp/tools/utils.py +402 -16
  126. basic_memory/mcp/tools/view_note.py +78 -0
  127. basic_memory/mcp/tools/write_note.py +230 -0
  128. basic_memory/models/__init__.py +3 -2
  129. basic_memory/models/knowledge.py +82 -17
  130. basic_memory/models/project.py +93 -0
  131. basic_memory/models/search.py +68 -8
  132. basic_memory/project_resolver.py +222 -0
  133. basic_memory/repository/__init__.py +2 -0
  134. basic_memory/repository/entity_repository.py +437 -8
  135. basic_memory/repository/observation_repository.py +36 -3
  136. basic_memory/repository/postgres_search_repository.py +451 -0
  137. basic_memory/repository/project_info_repository.py +10 -0
  138. basic_memory/repository/project_repository.py +140 -0
  139. basic_memory/repository/relation_repository.py +79 -4
  140. basic_memory/repository/repository.py +148 -29
  141. basic_memory/repository/search_index_row.py +95 -0
  142. basic_memory/repository/search_repository.py +79 -268
  143. basic_memory/repository/search_repository_base.py +241 -0
  144. basic_memory/repository/sqlite_search_repository.py +437 -0
  145. basic_memory/runtime.py +61 -0
  146. basic_memory/schemas/__init__.py +22 -9
  147. basic_memory/schemas/base.py +131 -12
  148. basic_memory/schemas/cloud.py +50 -0
  149. basic_memory/schemas/directory.py +31 -0
  150. basic_memory/schemas/importer.py +35 -0
  151. basic_memory/schemas/memory.py +194 -25
  152. basic_memory/schemas/project_info.py +213 -0
  153. basic_memory/schemas/prompt.py +90 -0
  154. basic_memory/schemas/request.py +56 -2
  155. basic_memory/schemas/response.py +85 -28
  156. basic_memory/schemas/search.py +36 -35
  157. basic_memory/schemas/sync_report.py +72 -0
  158. basic_memory/schemas/v2/__init__.py +27 -0
  159. basic_memory/schemas/v2/entity.py +133 -0
  160. basic_memory/schemas/v2/resource.py +47 -0
  161. basic_memory/services/__init__.py +2 -1
  162. basic_memory/services/context_service.py +451 -138
  163. basic_memory/services/directory_service.py +310 -0
  164. basic_memory/services/entity_service.py +636 -71
  165. basic_memory/services/exceptions.py +21 -0
  166. basic_memory/services/file_service.py +402 -33
  167. basic_memory/services/initialization.py +216 -0
  168. basic_memory/services/link_resolver.py +50 -56
  169. basic_memory/services/project_service.py +888 -0
  170. basic_memory/services/search_service.py +232 -37
  171. basic_memory/sync/__init__.py +4 -2
  172. basic_memory/sync/background_sync.py +26 -0
  173. basic_memory/sync/coordinator.py +160 -0
  174. basic_memory/sync/sync_service.py +1200 -109
  175. basic_memory/sync/watch_service.py +432 -135
  176. basic_memory/telemetry.py +249 -0
  177. basic_memory/templates/prompts/continue_conversation.hbs +110 -0
  178. basic_memory/templates/prompts/search.hbs +101 -0
  179. basic_memory/utils.py +407 -54
  180. basic_memory-0.17.4.dist-info/METADATA +617 -0
  181. basic_memory-0.17.4.dist-info/RECORD +193 -0
  182. {basic_memory-0.7.0.dist-info → basic_memory-0.17.4.dist-info}/WHEEL +1 -1
  183. {basic_memory-0.7.0.dist-info → basic_memory-0.17.4.dist-info}/entry_points.txt +1 -0
  184. basic_memory/alembic/README +0 -1
  185. basic_memory/cli/commands/sync.py +0 -206
  186. basic_memory/cli/commands/tools.py +0 -157
  187. basic_memory/mcp/tools/knowledge.py +0 -68
  188. basic_memory/mcp/tools/memory.py +0 -170
  189. basic_memory/mcp/tools/notes.py +0 -202
  190. basic_memory/schemas/discovery.py +0 -28
  191. basic_memory/sync/file_change_scanner.py +0 -158
  192. basic_memory/sync/utils.py +0 -31
  193. basic_memory-0.7.0.dist-info/METADATA +0 -378
  194. basic_memory-0.7.0.dist-info/RECORD +0 -82
  195. {basic_memory-0.7.0.dist-info → basic_memory-0.17.4.dist-info}/licenses/LICENSE +0 -0
@@ -6,31 +6,43 @@ all tools with the MCP server.
6
6
  """
7
7
 
8
8
  # Import tools to register them with MCP
9
- from basic_memory.mcp.tools.memory import build_context, recent_activity
10
-
11
- # from basic_memory.mcp.tools.ai_edit import ai_edit
12
- from basic_memory.mcp.tools.notes import read_note, write_note
13
- from basic_memory.mcp.tools.search import search
14
-
15
- from basic_memory.mcp.tools.knowledge import (
16
- delete_entities,
17
- get_entity,
18
- get_entities,
9
+ from basic_memory.mcp.tools.delete_note import delete_note
10
+ from basic_memory.mcp.tools.read_content import read_content
11
+ from basic_memory.mcp.tools.build_context import build_context
12
+ from basic_memory.mcp.tools.recent_activity import recent_activity
13
+ from basic_memory.mcp.tools.read_note import read_note
14
+ from basic_memory.mcp.tools.view_note import view_note
15
+ from basic_memory.mcp.tools.write_note import write_note
16
+ from basic_memory.mcp.tools.search import search_notes
17
+ from basic_memory.mcp.tools.canvas import canvas
18
+ from basic_memory.mcp.tools.list_directory import list_directory
19
+ from basic_memory.mcp.tools.edit_note import edit_note
20
+ from basic_memory.mcp.tools.move_note import move_note
21
+ from basic_memory.mcp.tools.project_management import (
22
+ list_memory_projects,
23
+ create_memory_project,
24
+ delete_project,
19
25
  )
20
26
 
27
+ # ChatGPT-compatible tools
28
+ from basic_memory.mcp.tools.chatgpt_tools import search, fetch
29
+
21
30
  __all__ = [
22
- # Knowledge graph tools
23
- "delete_entities",
24
- "get_entity",
25
- "get_entities",
26
- # Search tools
27
- "search",
28
- # memory tools
29
31
  "build_context",
30
- "recent_activity",
31
- # notes
32
+ "canvas",
33
+ "create_memory_project",
34
+ "delete_note",
35
+ "delete_project",
36
+ "edit_note",
37
+ "fetch",
38
+ "list_directory",
39
+ "list_memory_projects",
40
+ "move_note",
41
+ "read_content",
32
42
  "read_note",
43
+ "recent_activity",
44
+ "search",
45
+ "search_notes",
46
+ "view_note",
33
47
  "write_note",
34
- # file edit
35
- # "ai_edit",
36
48
  ]
@@ -0,0 +1,120 @@
1
+ """Build context tool for Basic Memory MCP server."""
2
+
3
+ from typing import Optional
4
+
5
+ from loguru import logger
6
+ from fastmcp import Context
7
+
8
+ from basic_memory.mcp.async_client import get_client
9
+ from basic_memory.mcp.project_context import get_active_project
10
+ from basic_memory.mcp.server import mcp
11
+ from basic_memory.telemetry import track_mcp_tool
12
+ from basic_memory.schemas.base import TimeFrame
13
+ from basic_memory.schemas.memory import (
14
+ GraphContext,
15
+ MemoryUrl,
16
+ memory_url_path,
17
+ )
18
+
19
+
20
+ @mcp.tool(
21
+ description="""Build context from a memory:// URI to continue conversations naturally.
22
+
23
+ Use this to follow up on previous discussions or explore related topics.
24
+
25
+ Memory URL Format:
26
+ - Use paths like "folder/note" or "memory://folder/note"
27
+ - Pattern matching: "folder/*" matches all notes in folder
28
+ - Valid characters: letters, numbers, hyphens, underscores, forward slashes
29
+ - Avoid: double slashes (//), angle brackets (<>), quotes, pipes (|)
30
+ - Examples: "specs/search", "projects/basic-memory", "notes/*"
31
+
32
+ Timeframes support natural language like:
33
+ - "2 days ago", "last week", "today", "3 months ago"
34
+ - Or standard formats like "7d", "24h"
35
+ """,
36
+ )
37
+ async def build_context(
38
+ url: MemoryUrl,
39
+ project: Optional[str] = None,
40
+ depth: str | int | None = 1,
41
+ timeframe: Optional[TimeFrame] = "7d",
42
+ page: int = 1,
43
+ page_size: int = 10,
44
+ max_related: int = 10,
45
+ context: Context | None = None,
46
+ ) -> GraphContext:
47
+ """Get context needed to continue a discussion within a specific project.
48
+
49
+ This tool enables natural continuation of discussions by loading relevant context
50
+ from memory:// URIs. It uses pattern matching to find relevant content and builds
51
+ a rich context graph of related information.
52
+
53
+ Project Resolution:
54
+ Server resolves projects in this order: Single Project Mode → project parameter → default project.
55
+ If project unknown, use list_memory_projects() or recent_activity() first.
56
+
57
+ Args:
58
+ project: Project name to build context from. Optional - server will resolve using hierarchy.
59
+ If unknown, use list_memory_projects() to discover available projects.
60
+ url: memory:// URI pointing to discussion content (e.g. memory://specs/search)
61
+ depth: How many relation hops to traverse (1-3 recommended for performance)
62
+ timeframe: How far back to look. Supports natural language like "2 days ago", "last week"
63
+ page: Page number of results to return (default: 1)
64
+ page_size: Number of results to return per page (default: 10)
65
+ max_related: Maximum number of related results to return (default: 10)
66
+ context: Optional FastMCP context for performance caching.
67
+
68
+ Returns:
69
+ GraphContext containing:
70
+ - primary_results: Content matching the memory:// URI
71
+ - related_results: Connected content via relations
72
+ - metadata: Context building details
73
+
74
+ Examples:
75
+ # Continue a specific discussion
76
+ build_context("my-project", "memory://specs/search")
77
+
78
+ # Get deeper context about a component
79
+ build_context("work-docs", "memory://components/memory-service", depth=2)
80
+
81
+ # Look at recent changes to a specification
82
+ build_context("research", "memory://specs/document-format", timeframe="today")
83
+
84
+ # Research the history of a feature
85
+ build_context("dev-notes", "memory://features/knowledge-graph", timeframe="3 months ago")
86
+
87
+ Raises:
88
+ ToolError: If project doesn't exist or depth parameter is invalid
89
+ """
90
+ track_mcp_tool("build_context")
91
+ logger.info(f"Building context from {url} in project {project}")
92
+
93
+ # Convert string depth to integer if needed
94
+ if isinstance(depth, str):
95
+ try:
96
+ depth = int(depth)
97
+ except ValueError:
98
+ from mcp.server.fastmcp.exceptions import ToolError
99
+
100
+ raise ToolError(f"Invalid depth parameter: '{depth}' is not a valid integer")
101
+
102
+ # URL is already validated and normalized by MemoryUrl type annotation
103
+
104
+ async with get_client() as client:
105
+ # Get the active project using the new stateless approach
106
+ active_project = await get_active_project(client, project, context)
107
+
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,
120
+ )
@@ -0,0 +1,152 @@
1
+ """Canvas creation tool for Basic Memory MCP server.
2
+
3
+ This tool creates Obsidian canvas files (.canvas) using the JSON Canvas 1.0 spec.
4
+ """
5
+
6
+ import json
7
+ from typing import Dict, List, Any, Optional
8
+
9
+ from loguru import logger
10
+ from fastmcp import Context
11
+
12
+ from basic_memory.mcp.async_client import get_client
13
+ from basic_memory.mcp.project_context import get_active_project
14
+ from basic_memory.mcp.server import mcp
15
+ from basic_memory.mcp.tools.utils import call_put, call_post, resolve_entity_id
16
+ from basic_memory.telemetry import track_mcp_tool
17
+
18
+
19
+ @mcp.tool(
20
+ description="Create an Obsidian canvas file to visualize concepts and connections.",
21
+ )
22
+ async def canvas(
23
+ nodes: List[Dict[str, Any]],
24
+ edges: List[Dict[str, Any]],
25
+ title: str,
26
+ folder: str,
27
+ project: Optional[str] = None,
28
+ context: Context | None = None,
29
+ ) -> str:
30
+ """Create an Obsidian canvas file with the provided nodes and edges.
31
+
32
+ This tool creates a .canvas file compatible with Obsidian's Canvas feature,
33
+ allowing visualization of relationships between concepts or documents.
34
+
35
+ Project Resolution:
36
+ Server resolves projects in this order: Single Project Mode → project parameter → default project.
37
+ If project unknown, use list_memory_projects() or recent_activity() first.
38
+
39
+ For the full JSON Canvas 1.0 specification, see the 'spec://canvas' resource.
40
+
41
+ Args:
42
+ project: Project name to create canvas in. Optional - server will resolve using hierarchy.
43
+ If unknown, use list_memory_projects() to discover available projects.
44
+ nodes: List of node objects following JSON Canvas 1.0 spec
45
+ edges: List of edge objects following JSON Canvas 1.0 spec
46
+ title: The title of the canvas (will be saved as title.canvas)
47
+ folder: Folder path relative to project root where the canvas should be saved.
48
+ Use forward slashes (/) as separators. Examples: "diagrams", "projects/2025", "visual/maps"
49
+ context: Optional FastMCP context for performance caching.
50
+
51
+ Returns:
52
+ A summary of the created canvas file
53
+
54
+ Important Notes:
55
+ - When referencing files, use the exact file path as shown in Obsidian
56
+ Example: "folder/Document Name.md" (not permalink format)
57
+ - For file nodes, the "file" attribute must reference an existing file
58
+ - Nodes require id, type, x, y, width, height properties
59
+ - Edges require id, fromNode, toNode properties
60
+ - Position nodes in a logical layout (x,y coordinates in pixels)
61
+ - Use color attributes ("1"-"6" or hex) for visual organization
62
+
63
+ Basic Structure:
64
+ ```json
65
+ {
66
+ "nodes": [
67
+ {
68
+ "id": "node1",
69
+ "type": "file", // Options: "file", "text", "link", "group"
70
+ "file": "folder/Document.md",
71
+ "x": 0,
72
+ "y": 0,
73
+ "width": 400,
74
+ "height": 300
75
+ }
76
+ ],
77
+ "edges": [
78
+ {
79
+ "id": "edge1",
80
+ "fromNode": "node1",
81
+ "toNode": "node2",
82
+ "label": "connects to"
83
+ }
84
+ ]
85
+ }
86
+ ```
87
+
88
+ Examples:
89
+ # Create canvas in project
90
+ canvas("my-project", nodes=[...], edges=[...], title="My Canvas", folder="diagrams")
91
+
92
+ # Create canvas in work project
93
+ canvas("work-project", nodes=[...], edges=[...], title="Process Flow", folder="visual/maps")
94
+
95
+ Raises:
96
+ ToolError: If project doesn't exist or folder path is invalid
97
+ """
98
+ track_mcp_tool("canvas")
99
+ async with get_client() as client:
100
+ active_project = await get_active_project(client, project, context)
101
+
102
+ # Ensure path has .canvas extension
103
+ file_title = title if title.endswith(".canvas") else f"{title}.canvas"
104
+ file_path = f"{folder}/{file_title}"
105
+
106
+ # Create canvas data structure
107
+ canvas_data = {"nodes": nodes, "edges": edges}
108
+
109
+ # Convert to JSON
110
+ canvas_json = json.dumps(canvas_data, indent=2)
111
+
112
+ # Try to create the canvas file first (optimistic create)
113
+ logger.info(f"Creating canvas file: {file_path} in project {project}")
114
+ try:
115
+ response = await call_post(
116
+ client,
117
+ f"/v2/projects/{active_project.external_id}/resource",
118
+ json={"file_path": file_path, "content": canvas_json},
119
+ )
120
+ action = "Created"
121
+ except Exception as e:
122
+ # If creation failed due to conflict (already exists), try to update
123
+ if (
124
+ "409" in str(e)
125
+ or "conflict" in str(e).lower()
126
+ or "already exists" in str(e).lower()
127
+ ):
128
+ logger.info(f"Canvas file exists, updating instead: {file_path}")
129
+ try:
130
+ entity_id = await resolve_entity_id(client, active_project.external_id, file_path)
131
+ # For update, send content in JSON body
132
+ response = await call_put(
133
+ client,
134
+ f"/v2/projects/{active_project.external_id}/resource/{entity_id}",
135
+ json={"content": canvas_json},
136
+ )
137
+ action = "Updated"
138
+ except Exception as update_error: # pragma: no cover
139
+ # Re-raise the original error if update also fails
140
+ raise e from update_error # pragma: no cover
141
+ else:
142
+ # Re-raise if it's not a conflict error
143
+ raise # pragma: no cover
144
+
145
+ # Parse response
146
+ result = response.json()
147
+ logger.debug(result)
148
+
149
+ # Build summary
150
+ summary = [f"# {action}: {file_path}", "\nThe canvas is ready to open in Obsidian."]
151
+
152
+ return "\n".join(summary)
@@ -0,0 +1,190 @@
1
+ """ChatGPT-compatible MCP tools for Basic Memory.
2
+
3
+ These adapters expose Basic Memory's search/fetch functionality using the exact
4
+ tool names and response structure OpenAI's MCP clients expect: each call returns
5
+ a list containing a single `{"type": "text", "text": "{...json...}"}` item.
6
+ """
7
+
8
+ import json
9
+ from typing import Any, Dict, List, Optional
10
+ from loguru import logger
11
+ from fastmcp import Context
12
+
13
+ from basic_memory.mcp.server import mcp
14
+ from basic_memory.mcp.tools.search import search_notes
15
+ from basic_memory.mcp.tools.read_note import read_note
16
+ from basic_memory.schemas.search import SearchResponse
17
+ from basic_memory.config import ConfigManager
18
+ from basic_memory.telemetry import track_mcp_tool
19
+
20
+
21
+ def _format_search_results_for_chatgpt(results: SearchResponse) -> List[Dict[str, Any]]:
22
+ """Format search results according to ChatGPT's expected schema.
23
+
24
+ Returns a list of result objects with id, title, and url fields.
25
+ """
26
+ formatted_results = []
27
+
28
+ for result in results.results:
29
+ formatted_result = {
30
+ "id": result.permalink or f"doc-{len(formatted_results)}",
31
+ "title": result.title if result.title and result.title.strip() else "Untitled",
32
+ "url": result.permalink or "",
33
+ }
34
+ formatted_results.append(formatted_result)
35
+
36
+ return formatted_results
37
+
38
+
39
+ def _format_document_for_chatgpt(
40
+ content: str, identifier: str, title: Optional[str] = None
41
+ ) -> Dict[str, Any]:
42
+ """Format document content according to ChatGPT's expected schema.
43
+
44
+ Returns a document object with id, title, text, url, and metadata fields.
45
+ """
46
+ # Extract title from markdown content if not provided
47
+ if not title and isinstance(content, str):
48
+ lines = content.split("\n")
49
+ if lines and lines[0].startswith("# "):
50
+ title = lines[0][2:].strip()
51
+ else:
52
+ title = identifier.split("/")[-1].replace("-", " ").title()
53
+
54
+ # Ensure title is never None
55
+ if not title:
56
+ title = "Untitled Document"
57
+
58
+ # Handle error cases
59
+ if isinstance(content, str) and content.lstrip().startswith("# Note Not Found"):
60
+ return {
61
+ "id": identifier,
62
+ "title": title or "Document Not Found",
63
+ "text": content,
64
+ "url": identifier,
65
+ "metadata": {"error": "Document not found"},
66
+ }
67
+
68
+ return {
69
+ "id": identifier,
70
+ "title": title or "Untitled Document",
71
+ "text": content,
72
+ "url": identifier,
73
+ "metadata": {"format": "markdown"},
74
+ }
75
+
76
+
77
+ @mcp.tool(description="Search for content across the knowledge base")
78
+ async def search(
79
+ query: str,
80
+ context: Context | None = None,
81
+ ) -> List[Dict[str, Any]]:
82
+ """ChatGPT/OpenAI MCP search adapter returning a single text content item.
83
+
84
+ Args:
85
+ query: Search query (full-text syntax supported by `search_notes`)
86
+ context: Optional FastMCP context passed through for auth/session data
87
+
88
+ Returns:
89
+ List with one dict: `{ "type": "text", "text": "{...JSON...}" }`
90
+ where the JSON body contains `results`, `total_count`, and echo of `query`.
91
+ """
92
+ track_mcp_tool("search")
93
+ logger.info(f"ChatGPT search request: query='{query}'")
94
+
95
+ try:
96
+ # ChatGPT tools don't expose project parameter, so use default project
97
+ config = ConfigManager().config
98
+ default_project = config.default_project
99
+
100
+ # Call underlying search_notes with sensible defaults for ChatGPT
101
+ results = await search_notes.fn(
102
+ query=query,
103
+ project=default_project, # Use default project for ChatGPT
104
+ page=1,
105
+ page_size=10, # Reasonable default for ChatGPT consumption
106
+ search_type="text", # Default to full-text search
107
+ context=context,
108
+ )
109
+
110
+ # Handle string error responses from search_notes
111
+ if isinstance(results, str):
112
+ logger.warning(f"Search failed with error: {results[:100]}...")
113
+ search_results = {
114
+ "results": [],
115
+ "error": "Search failed",
116
+ "error_details": results[:500], # Truncate long error messages
117
+ }
118
+ else:
119
+ # Format successful results for ChatGPT
120
+ formatted_results = _format_search_results_for_chatgpt(results)
121
+ search_results = {
122
+ "results": formatted_results,
123
+ "total_count": len(results.results), # Use actual count from results
124
+ "query": query,
125
+ }
126
+ logger.info(f"Search completed: {len(formatted_results)} results returned")
127
+
128
+ # Return in MCP content array format as required by OpenAI
129
+ return [{"type": "text", "text": json.dumps(search_results, ensure_ascii=False)}]
130
+
131
+ except Exception as e:
132
+ logger.error(f"ChatGPT search failed for query '{query}': {e}")
133
+ error_results = {
134
+ "results": [],
135
+ "error": "Internal search error",
136
+ "error_message": str(e)[:200],
137
+ }
138
+ return [{"type": "text", "text": json.dumps(error_results, ensure_ascii=False)}]
139
+
140
+
141
+ @mcp.tool(description="Fetch the full contents of a search result document")
142
+ async def fetch(
143
+ id: str,
144
+ context: Context | None = None,
145
+ ) -> List[Dict[str, Any]]:
146
+ """ChatGPT/OpenAI MCP fetch adapter returning a single text content item.
147
+
148
+ Args:
149
+ id: Document identifier (permalink, title, or memory URL)
150
+ context: Optional FastMCP context passed through for auth/session data
151
+
152
+ Returns:
153
+ List with one dict: `{ "type": "text", "text": "{...JSON...}" }`
154
+ where the JSON body includes `id`, `title`, `text`, `url`, and metadata.
155
+ """
156
+ track_mcp_tool("fetch")
157
+ logger.info(f"ChatGPT fetch request: id='{id}'")
158
+
159
+ try:
160
+ # ChatGPT tools don't expose project parameter, so use default project
161
+ config = ConfigManager().config
162
+ default_project = config.default_project
163
+
164
+ # Call underlying read_note function
165
+ content = await read_note.fn(
166
+ identifier=id,
167
+ project=default_project, # Use default project for ChatGPT
168
+ page=1,
169
+ page_size=10, # Default pagination
170
+ context=context,
171
+ )
172
+
173
+ # Format the document for ChatGPT
174
+ document = _format_document_for_chatgpt(content, id)
175
+
176
+ logger.info(f"Fetch completed: id='{id}', content_length={len(document.get('text', ''))}")
177
+
178
+ # Return in MCP content array format as required by OpenAI
179
+ return [{"type": "text", "text": json.dumps(document, ensure_ascii=False)}]
180
+
181
+ except Exception as e:
182
+ logger.error(f"ChatGPT fetch failed for id '{id}': {e}")
183
+ error_document = {
184
+ "id": id,
185
+ "title": "Fetch Error",
186
+ "text": f"Failed to fetch document: {str(e)[:200]}",
187
+ "url": id,
188
+ "metadata": {"error": "Fetch failed"},
189
+ }
190
+ return [{"type": "text", "text": json.dumps(error_document, ensure_ascii=False)}]