basic-memory 0.7.0__py3-none-any.whl → 0.9.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/alembic.ini +119 -0
- basic_memory/alembic/env.py +23 -1
- basic_memory/alembic/migrations.py +4 -9
- basic_memory/alembic/versions/502b60eaa905_remove_required_from_entity_permalink.py +51 -0
- basic_memory/alembic/versions/b3c3938bacdb_relation_to_name_unique_index.py +44 -0
- basic_memory/alembic/versions/cc7172b46608_update_search_index_schema.py +106 -0
- basic_memory/api/app.py +9 -10
- basic_memory/api/routers/__init__.py +2 -1
- basic_memory/api/routers/knowledge_router.py +31 -5
- basic_memory/api/routers/memory_router.py +18 -17
- basic_memory/api/routers/project_info_router.py +275 -0
- basic_memory/api/routers/resource_router.py +105 -4
- basic_memory/api/routers/search_router.py +22 -4
- basic_memory/cli/app.py +54 -5
- basic_memory/cli/commands/__init__.py +15 -2
- basic_memory/cli/commands/db.py +9 -13
- basic_memory/cli/commands/import_chatgpt.py +26 -30
- basic_memory/cli/commands/import_claude_conversations.py +27 -29
- basic_memory/cli/commands/import_claude_projects.py +29 -31
- basic_memory/cli/commands/import_memory_json.py +26 -28
- basic_memory/cli/commands/mcp.py +7 -1
- basic_memory/cli/commands/project.py +119 -0
- basic_memory/cli/commands/project_info.py +167 -0
- basic_memory/cli/commands/status.py +14 -28
- basic_memory/cli/commands/sync.py +63 -22
- basic_memory/cli/commands/tool.py +253 -0
- basic_memory/cli/main.py +39 -1
- basic_memory/config.py +166 -4
- basic_memory/db.py +19 -4
- basic_memory/deps.py +10 -3
- basic_memory/file_utils.py +37 -19
- basic_memory/markdown/entity_parser.py +3 -3
- basic_memory/markdown/utils.py +5 -0
- basic_memory/mcp/async_client.py +1 -1
- basic_memory/mcp/main.py +24 -0
- basic_memory/mcp/prompts/__init__.py +19 -0
- basic_memory/mcp/prompts/ai_assistant_guide.py +26 -0
- basic_memory/mcp/prompts/continue_conversation.py +111 -0
- basic_memory/mcp/prompts/recent_activity.py +88 -0
- basic_memory/mcp/prompts/search.py +182 -0
- basic_memory/mcp/prompts/utils.py +155 -0
- basic_memory/mcp/server.py +2 -6
- basic_memory/mcp/tools/__init__.py +12 -21
- basic_memory/mcp/tools/build_context.py +85 -0
- basic_memory/mcp/tools/canvas.py +97 -0
- basic_memory/mcp/tools/delete_note.py +28 -0
- basic_memory/mcp/tools/project_info.py +51 -0
- basic_memory/mcp/tools/read_content.py +229 -0
- basic_memory/mcp/tools/read_note.py +190 -0
- basic_memory/mcp/tools/recent_activity.py +100 -0
- basic_memory/mcp/tools/search.py +56 -17
- basic_memory/mcp/tools/utils.py +245 -16
- basic_memory/mcp/tools/write_note.py +124 -0
- basic_memory/models/knowledge.py +27 -11
- basic_memory/models/search.py +2 -1
- basic_memory/repository/entity_repository.py +3 -2
- basic_memory/repository/project_info_repository.py +9 -0
- basic_memory/repository/repository.py +24 -7
- basic_memory/repository/search_repository.py +47 -14
- basic_memory/schemas/__init__.py +10 -9
- basic_memory/schemas/base.py +4 -1
- basic_memory/schemas/memory.py +14 -4
- basic_memory/schemas/project_info.py +96 -0
- basic_memory/schemas/search.py +29 -33
- basic_memory/services/context_service.py +3 -3
- basic_memory/services/entity_service.py +26 -13
- basic_memory/services/file_service.py +145 -26
- basic_memory/services/link_resolver.py +9 -46
- basic_memory/services/search_service.py +95 -22
- basic_memory/sync/__init__.py +3 -2
- basic_memory/sync/sync_service.py +523 -117
- basic_memory/sync/watch_service.py +258 -132
- basic_memory/utils.py +51 -36
- basic_memory-0.9.0.dist-info/METADATA +736 -0
- basic_memory-0.9.0.dist-info/RECORD +99 -0
- basic_memory/alembic/README +0 -1
- basic_memory/cli/commands/tools.py +0 -157
- basic_memory/mcp/tools/knowledge.py +0 -68
- basic_memory/mcp/tools/memory.py +0 -170
- basic_memory/mcp/tools/notes.py +0 -202
- basic_memory/schemas/discovery.py +0 -28
- basic_memory/sync/file_change_scanner.py +0 -158
- basic_memory/sync/utils.py +0 -31
- basic_memory-0.7.0.dist-info/METADATA +0 -378
- basic_memory-0.7.0.dist-info/RECORD +0 -82
- {basic_memory-0.7.0.dist-info → basic_memory-0.9.0.dist-info}/WHEEL +0 -0
- {basic_memory-0.7.0.dist-info → basic_memory-0.9.0.dist-info}/entry_points.txt +0 -0
- {basic_memory-0.7.0.dist-info → basic_memory-0.9.0.dist-info}/licenses/LICENSE +0 -0
|
@@ -0,0 +1,182 @@
|
|
|
1
|
+
"""Search prompts for Basic Memory MCP server.
|
|
2
|
+
|
|
3
|
+
These prompts help users search and explore their knowledge base.
|
|
4
|
+
"""
|
|
5
|
+
|
|
6
|
+
from textwrap import dedent
|
|
7
|
+
from typing import Annotated, Optional
|
|
8
|
+
|
|
9
|
+
from loguru import logger
|
|
10
|
+
from pydantic import Field
|
|
11
|
+
|
|
12
|
+
from basic_memory.mcp.server import mcp
|
|
13
|
+
from basic_memory.mcp.tools.search import search as search_tool
|
|
14
|
+
from basic_memory.schemas.base import TimeFrame
|
|
15
|
+
from basic_memory.schemas.search import SearchQuery, SearchResponse
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
@mcp.prompt(
|
|
19
|
+
name="Search Knowledge Base",
|
|
20
|
+
description="Search across all content in basic-memory",
|
|
21
|
+
)
|
|
22
|
+
async def search_prompt(
|
|
23
|
+
query: str,
|
|
24
|
+
timeframe: Annotated[
|
|
25
|
+
Optional[TimeFrame],
|
|
26
|
+
Field(description="How far back to search (e.g. '1d', '1 week')"),
|
|
27
|
+
] = None,
|
|
28
|
+
) -> str:
|
|
29
|
+
"""Search across all content in basic-memory.
|
|
30
|
+
|
|
31
|
+
This prompt helps search for content in the knowledge base and
|
|
32
|
+
provides helpful context about the results.
|
|
33
|
+
|
|
34
|
+
Args:
|
|
35
|
+
query: The search text to look for
|
|
36
|
+
timeframe: Optional timeframe to limit results (e.g. '1d', '1 week')
|
|
37
|
+
|
|
38
|
+
Returns:
|
|
39
|
+
Formatted search results with context
|
|
40
|
+
"""
|
|
41
|
+
logger.info(f"Searching knowledge base, query: {query}, timeframe: {timeframe}")
|
|
42
|
+
|
|
43
|
+
search_results = await search_tool(SearchQuery(text=query, after_date=timeframe))
|
|
44
|
+
return format_search_results(query, search_results, timeframe)
|
|
45
|
+
|
|
46
|
+
|
|
47
|
+
def format_search_results(
|
|
48
|
+
query: str, results: SearchResponse, timeframe: Optional[TimeFrame] = None
|
|
49
|
+
) -> str:
|
|
50
|
+
"""Format search results into a helpful summary.
|
|
51
|
+
|
|
52
|
+
Args:
|
|
53
|
+
query: The search query
|
|
54
|
+
results: Search results object
|
|
55
|
+
timeframe: How far back results were searched
|
|
56
|
+
|
|
57
|
+
Returns:
|
|
58
|
+
Formatted search results summary
|
|
59
|
+
"""
|
|
60
|
+
if not results.results:
|
|
61
|
+
return dedent(f"""
|
|
62
|
+
# Search Results for: "{query}"
|
|
63
|
+
|
|
64
|
+
I couldn't find any results for this query.
|
|
65
|
+
|
|
66
|
+
## Opportunity to Capture Knowledge!
|
|
67
|
+
|
|
68
|
+
This is an excellent opportunity to create new knowledge on this topic. Consider:
|
|
69
|
+
|
|
70
|
+
```python
|
|
71
|
+
await write_note(
|
|
72
|
+
title="{query.capitalize()}",
|
|
73
|
+
content=f'''
|
|
74
|
+
# {query.capitalize()}
|
|
75
|
+
|
|
76
|
+
## Overview
|
|
77
|
+
[Summary of what we've discussed about {query}]
|
|
78
|
+
|
|
79
|
+
## Observations
|
|
80
|
+
- [category] [First observation about {query}]
|
|
81
|
+
- [category] [Second observation about {query}]
|
|
82
|
+
|
|
83
|
+
## Relations
|
|
84
|
+
- relates_to [[Other Relevant Topic]]
|
|
85
|
+
'''
|
|
86
|
+
)
|
|
87
|
+
```
|
|
88
|
+
|
|
89
|
+
## Other Suggestions
|
|
90
|
+
- Try a different search term
|
|
91
|
+
- Broaden your search criteria
|
|
92
|
+
- Check recent activity with `recent_activity(timeframe="1w")`
|
|
93
|
+
""")
|
|
94
|
+
|
|
95
|
+
# Start building our summary with header
|
|
96
|
+
time_info = f" (after {timeframe})" if timeframe else ""
|
|
97
|
+
summary = dedent(f"""
|
|
98
|
+
# Search Results for: "{query}"{time_info}
|
|
99
|
+
|
|
100
|
+
This is a memory search session.
|
|
101
|
+
Please use the available basic-memory tools to gather relevant context before responding.
|
|
102
|
+
I found {len(results.results)} results that match your query.
|
|
103
|
+
|
|
104
|
+
Here are the most relevant results:
|
|
105
|
+
""")
|
|
106
|
+
|
|
107
|
+
# Add each search result
|
|
108
|
+
for i, result in enumerate(results.results[:5]): # Limit to top 5 results
|
|
109
|
+
summary += dedent(f"""
|
|
110
|
+
## {i + 1}. {result.title}
|
|
111
|
+
- **Type**: {result.type.value}
|
|
112
|
+
""")
|
|
113
|
+
|
|
114
|
+
# Add creation date if available in metadata
|
|
115
|
+
if result.metadata and "created_at" in result.metadata:
|
|
116
|
+
created_at = result.metadata["created_at"]
|
|
117
|
+
if hasattr(created_at, "strftime"):
|
|
118
|
+
summary += (
|
|
119
|
+
f"- **Created**: {created_at.strftime('%Y-%m-%d %H:%M')}\n" # pragma: no cover
|
|
120
|
+
)
|
|
121
|
+
elif isinstance(created_at, str):
|
|
122
|
+
summary += f"- **Created**: {created_at}\n"
|
|
123
|
+
|
|
124
|
+
# Add score and excerpt
|
|
125
|
+
summary += f"- **Relevance Score**: {result.score:.2f}\n"
|
|
126
|
+
|
|
127
|
+
# Add excerpt if available in metadata
|
|
128
|
+
if result.content:
|
|
129
|
+
summary += f"- **Excerpt**:\n{result.content}\n"
|
|
130
|
+
|
|
131
|
+
# Add permalink for retrieving content
|
|
132
|
+
if result.permalink:
|
|
133
|
+
summary += dedent(f"""
|
|
134
|
+
You can view this content with: `read_note("{result.permalink}")`
|
|
135
|
+
Or explore its context with: `build_context("memory://{result.permalink}")`
|
|
136
|
+
""")
|
|
137
|
+
else:
|
|
138
|
+
summary += dedent(f"""
|
|
139
|
+
You can view this file with: `read_file("{result.file_path}")`
|
|
140
|
+
""") # pragma: no cover
|
|
141
|
+
|
|
142
|
+
# Add next steps with strong write encouragement
|
|
143
|
+
summary += dedent(f"""
|
|
144
|
+
## Next Steps
|
|
145
|
+
|
|
146
|
+
You can:
|
|
147
|
+
- Refine your search: `search("{query} AND additional_term")`
|
|
148
|
+
- Exclude terms: `search("{query} NOT exclude_term")`
|
|
149
|
+
- View more results: `search("{query}", after_date=None)`
|
|
150
|
+
- Check recent activity: `recent_activity()`
|
|
151
|
+
|
|
152
|
+
## Synthesize and Capture Knowledge
|
|
153
|
+
|
|
154
|
+
Consider creating a new note that synthesizes what you've learned:
|
|
155
|
+
|
|
156
|
+
```python
|
|
157
|
+
await write_note(
|
|
158
|
+
title="Synthesis of {query.capitalize()} Information",
|
|
159
|
+
content='''
|
|
160
|
+
# Synthesis of {query.capitalize()} Information
|
|
161
|
+
|
|
162
|
+
## Overview
|
|
163
|
+
[Synthesis of the search results and your conversation]
|
|
164
|
+
|
|
165
|
+
## Key Insights
|
|
166
|
+
[Summary of main points learned from these results]
|
|
167
|
+
|
|
168
|
+
## Observations
|
|
169
|
+
- [insight] [Important observation from search results]
|
|
170
|
+
- [connection] [How this connects to other topics]
|
|
171
|
+
|
|
172
|
+
## Relations
|
|
173
|
+
- relates_to [[{results.results[0].title if results.results else "Related Topic"}]]
|
|
174
|
+
- extends [[Another Relevant Topic]]
|
|
175
|
+
'''
|
|
176
|
+
)
|
|
177
|
+
```
|
|
178
|
+
|
|
179
|
+
Remember that capturing synthesized knowledge is one of the most valuable features of Basic Memory.
|
|
180
|
+
""")
|
|
181
|
+
|
|
182
|
+
return summary
|
|
@@ -0,0 +1,155 @@
|
|
|
1
|
+
"""Utility functions for formatting prompt responses.
|
|
2
|
+
|
|
3
|
+
These utilities help format data from various tools into consistent,
|
|
4
|
+
user-friendly markdown summaries.
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
from dataclasses import dataclass
|
|
8
|
+
from textwrap import dedent
|
|
9
|
+
from typing import List
|
|
10
|
+
|
|
11
|
+
from basic_memory.schemas.base import TimeFrame
|
|
12
|
+
from basic_memory.schemas.memory import (
|
|
13
|
+
normalize_memory_url,
|
|
14
|
+
EntitySummary,
|
|
15
|
+
RelationSummary,
|
|
16
|
+
ObservationSummary,
|
|
17
|
+
)
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
@dataclass
|
|
21
|
+
class PromptContextItem:
|
|
22
|
+
primary_results: List[EntitySummary]
|
|
23
|
+
related_results: List[EntitySummary | RelationSummary | ObservationSummary]
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
@dataclass
|
|
27
|
+
class PromptContext:
|
|
28
|
+
timeframe: TimeFrame
|
|
29
|
+
topic: str
|
|
30
|
+
results: List[PromptContextItem]
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
def format_prompt_context(context: PromptContext) -> str:
|
|
34
|
+
"""Format continuation context into a helpful summary.
|
|
35
|
+
Returns:
|
|
36
|
+
Formatted continuation summary
|
|
37
|
+
"""
|
|
38
|
+
if not context.results:
|
|
39
|
+
return dedent(f"""
|
|
40
|
+
# Continuing conversation on: {context.topic}
|
|
41
|
+
|
|
42
|
+
This is a memory retrieval session.
|
|
43
|
+
The supplied query did not return any information specifically on this topic.
|
|
44
|
+
|
|
45
|
+
## Opportunity to Capture New Knowledge!
|
|
46
|
+
|
|
47
|
+
This is an excellent chance to start documenting this topic:
|
|
48
|
+
|
|
49
|
+
```python
|
|
50
|
+
await write_note(
|
|
51
|
+
title="{context.topic}",
|
|
52
|
+
content=f'''
|
|
53
|
+
# {context.topic}
|
|
54
|
+
|
|
55
|
+
## Overview
|
|
56
|
+
[Summary of what we know about {context.topic}]
|
|
57
|
+
|
|
58
|
+
## Key Points
|
|
59
|
+
[Main aspects or components of {context.topic}]
|
|
60
|
+
|
|
61
|
+
## Observations
|
|
62
|
+
- [category] [First important observation about {context.topic}]
|
|
63
|
+
- [category] [Second observation about {context.topic}]
|
|
64
|
+
|
|
65
|
+
## Relations
|
|
66
|
+
- relates_to [[Related Topic]]
|
|
67
|
+
- part_of [[Broader Context]]
|
|
68
|
+
'''
|
|
69
|
+
)
|
|
70
|
+
```
|
|
71
|
+
|
|
72
|
+
## Other Options
|
|
73
|
+
|
|
74
|
+
Please use the available basic-memory tools to gather relevant context before responding.
|
|
75
|
+
You can also:
|
|
76
|
+
- Try a different search term
|
|
77
|
+
- Check recent activity with `recent_activity(timeframe="1w")`
|
|
78
|
+
""")
|
|
79
|
+
|
|
80
|
+
# Start building our summary with header - add knowledge capture emphasis
|
|
81
|
+
summary = dedent(f"""
|
|
82
|
+
# Continuing conversation on: {context.topic}
|
|
83
|
+
|
|
84
|
+
This is a memory retrieval session.
|
|
85
|
+
|
|
86
|
+
Please use the available basic-memory tools to gather relevant context before responding.
|
|
87
|
+
Start by executing one of the suggested commands below to retrieve content.
|
|
88
|
+
|
|
89
|
+
Here's what I found from previous conversations:
|
|
90
|
+
|
|
91
|
+
> **Knowledge Capture Recommendation:** As you continue this conversation, actively look for opportunities to record new information, decisions, or insights that emerge. Use `write_note()` to document important context.
|
|
92
|
+
""")
|
|
93
|
+
|
|
94
|
+
# Track what we've added to avoid duplicates
|
|
95
|
+
added_permalinks = set()
|
|
96
|
+
sections = []
|
|
97
|
+
|
|
98
|
+
# Process each context
|
|
99
|
+
for context in context.results: # pyright: ignore
|
|
100
|
+
for primary in context.primary_results: # pyright: ignore
|
|
101
|
+
if primary.permalink not in added_permalinks:
|
|
102
|
+
primary_permalink = primary.permalink
|
|
103
|
+
|
|
104
|
+
added_permalinks.add(primary_permalink)
|
|
105
|
+
|
|
106
|
+
memory_url = normalize_memory_url(primary_permalink)
|
|
107
|
+
section = dedent(f"""
|
|
108
|
+
--- {memory_url}
|
|
109
|
+
|
|
110
|
+
## {primary.title}
|
|
111
|
+
- **Type**: {primary.type}
|
|
112
|
+
""")
|
|
113
|
+
|
|
114
|
+
# Add creation date
|
|
115
|
+
section += f"- **Created**: {primary.created_at.strftime('%Y-%m-%d %H:%M')}\n"
|
|
116
|
+
|
|
117
|
+
# Add content snippet
|
|
118
|
+
if hasattr(primary, "content") and primary.content: # pyright: ignore
|
|
119
|
+
content = primary.content or "" # pyright: ignore
|
|
120
|
+
if content:
|
|
121
|
+
section += f"\n**Excerpt**:\n{content}\n"
|
|
122
|
+
|
|
123
|
+
section += dedent(f"""
|
|
124
|
+
|
|
125
|
+
You can read this document with: `read_note("{primary_permalink}")`
|
|
126
|
+
""")
|
|
127
|
+
sections.append(section)
|
|
128
|
+
|
|
129
|
+
if context.related_results: # pyright: ignore
|
|
130
|
+
section += dedent( # pyright: ignore
|
|
131
|
+
"""
|
|
132
|
+
## Related Context
|
|
133
|
+
"""
|
|
134
|
+
)
|
|
135
|
+
|
|
136
|
+
for related in context.related_results: # pyright: ignore
|
|
137
|
+
section_content = dedent(f"""
|
|
138
|
+
- type: **{related.type}**
|
|
139
|
+
- title: {related.title}
|
|
140
|
+
""")
|
|
141
|
+
if related.permalink:
|
|
142
|
+
section_content += (
|
|
143
|
+
f'You can view this document with: `read_note("{related.permalink}")`'
|
|
144
|
+
)
|
|
145
|
+
else:
|
|
146
|
+
section_content += (
|
|
147
|
+
f'You can view this file with: `read_file("{related.file_path}")`'
|
|
148
|
+
)
|
|
149
|
+
|
|
150
|
+
section += section_content
|
|
151
|
+
sections.append(section)
|
|
152
|
+
|
|
153
|
+
# Add all sections
|
|
154
|
+
summary += "\n".join(sections)
|
|
155
|
+
return summary
|
basic_memory/mcp/server.py
CHANGED
|
@@ -1,15 +1,11 @@
|
|
|
1
1
|
"""Enhanced FastMCP server instance for Basic Memory."""
|
|
2
2
|
|
|
3
3
|
from mcp.server.fastmcp import FastMCP
|
|
4
|
-
|
|
5
|
-
from basic_memory.utils import setup_logging
|
|
4
|
+
from mcp.server.fastmcp.utilities.logging import configure_logging
|
|
6
5
|
|
|
7
6
|
# mcp console logging
|
|
8
|
-
|
|
9
|
-
|
|
7
|
+
configure_logging(level="INFO")
|
|
10
8
|
|
|
11
|
-
# start our out file logging
|
|
12
|
-
setup_logging(log_file=".basic-memory/basic-memory.log")
|
|
13
9
|
|
|
14
10
|
# Create the shared server instance
|
|
15
11
|
mcp = FastMCP("Basic Memory")
|
|
@@ -6,31 +6,22 @@ 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.
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
from basic_memory.mcp.tools.
|
|
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.write_note import write_note
|
|
13
15
|
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,
|
|
19
|
-
)
|
|
16
|
+
from basic_memory.mcp.tools.canvas import canvas
|
|
20
17
|
|
|
21
18
|
__all__ = [
|
|
22
|
-
# Knowledge graph tools
|
|
23
|
-
"delete_entities",
|
|
24
|
-
"get_entity",
|
|
25
|
-
"get_entities",
|
|
26
|
-
# Search tools
|
|
27
|
-
"search",
|
|
28
|
-
# memory tools
|
|
29
19
|
"build_context",
|
|
30
|
-
"
|
|
31
|
-
|
|
20
|
+
"canvas",
|
|
21
|
+
"delete_note",
|
|
22
|
+
"read_content",
|
|
32
23
|
"read_note",
|
|
24
|
+
"recent_activity",
|
|
25
|
+
"search",
|
|
33
26
|
"write_note",
|
|
34
|
-
# file edit
|
|
35
|
-
# "ai_edit",
|
|
36
27
|
]
|
|
@@ -0,0 +1,85 @@
|
|
|
1
|
+
"""Build context tool for Basic Memory MCP server."""
|
|
2
|
+
|
|
3
|
+
from typing import Optional
|
|
4
|
+
|
|
5
|
+
from loguru import logger
|
|
6
|
+
|
|
7
|
+
from basic_memory.mcp.async_client import client
|
|
8
|
+
from basic_memory.mcp.server import mcp
|
|
9
|
+
from basic_memory.mcp.tools.utils import call_get
|
|
10
|
+
from basic_memory.schemas.base import TimeFrame
|
|
11
|
+
from basic_memory.schemas.memory import (
|
|
12
|
+
GraphContext,
|
|
13
|
+
MemoryUrl,
|
|
14
|
+
memory_url_path,
|
|
15
|
+
normalize_memory_url,
|
|
16
|
+
)
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
@mcp.tool(
|
|
20
|
+
description="""Build context from a memory:// URI to continue conversations naturally.
|
|
21
|
+
|
|
22
|
+
Use this to follow up on previous discussions or explore related topics.
|
|
23
|
+
Timeframes support natural language like:
|
|
24
|
+
- "2 days ago"
|
|
25
|
+
- "last week"
|
|
26
|
+
- "today"
|
|
27
|
+
- "3 months ago"
|
|
28
|
+
Or standard formats like "7d", "24h"
|
|
29
|
+
""",
|
|
30
|
+
)
|
|
31
|
+
async def build_context(
|
|
32
|
+
url: MemoryUrl,
|
|
33
|
+
depth: Optional[int] = 1,
|
|
34
|
+
timeframe: Optional[TimeFrame] = "7d",
|
|
35
|
+
page: int = 1,
|
|
36
|
+
page_size: int = 10,
|
|
37
|
+
max_related: int = 10,
|
|
38
|
+
) -> GraphContext:
|
|
39
|
+
"""Get context needed to continue a discussion.
|
|
40
|
+
|
|
41
|
+
This tool enables natural continuation of discussions by loading relevant context
|
|
42
|
+
from memory:// URIs. It uses pattern matching to find relevant content and builds
|
|
43
|
+
a rich context graph of related information.
|
|
44
|
+
|
|
45
|
+
Args:
|
|
46
|
+
url: memory:// URI pointing to discussion content (e.g. memory://specs/search)
|
|
47
|
+
depth: How many relation hops to traverse (1-3 recommended for performance)
|
|
48
|
+
timeframe: How far back to look. Supports natural language like "2 days ago", "last week"
|
|
49
|
+
page: Page number of results to return (default: 1)
|
|
50
|
+
page_size: Number of results to return per page (default: 10)
|
|
51
|
+
max_related: Maximum number of related results to return (default: 10)
|
|
52
|
+
|
|
53
|
+
Returns:
|
|
54
|
+
GraphContext containing:
|
|
55
|
+
- primary_results: Content matching the memory:// URI
|
|
56
|
+
- related_results: Connected content via relations
|
|
57
|
+
- metadata: Context building details
|
|
58
|
+
|
|
59
|
+
Examples:
|
|
60
|
+
# Continue a specific discussion
|
|
61
|
+
build_context("memory://specs/search")
|
|
62
|
+
|
|
63
|
+
# Get deeper context about a component
|
|
64
|
+
build_context("memory://components/memory-service", depth=2)
|
|
65
|
+
|
|
66
|
+
# Look at recent changes to a specification
|
|
67
|
+
build_context("memory://specs/document-format", timeframe="today")
|
|
68
|
+
|
|
69
|
+
# Research the history of a feature
|
|
70
|
+
build_context("memory://features/knowledge-graph", timeframe="3 months ago")
|
|
71
|
+
"""
|
|
72
|
+
logger.info(f"Building context from {url}")
|
|
73
|
+
url = normalize_memory_url(url)
|
|
74
|
+
response = await call_get(
|
|
75
|
+
client,
|
|
76
|
+
f"/memory/{memory_url_path(url)}",
|
|
77
|
+
params={
|
|
78
|
+
"depth": depth,
|
|
79
|
+
"timeframe": timeframe,
|
|
80
|
+
"page": page,
|
|
81
|
+
"page_size": page_size,
|
|
82
|
+
"max_related": max_related,
|
|
83
|
+
},
|
|
84
|
+
)
|
|
85
|
+
return GraphContext.model_validate(response.json())
|
|
@@ -0,0 +1,97 @@
|
|
|
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
|
|
8
|
+
|
|
9
|
+
from loguru import logger
|
|
10
|
+
|
|
11
|
+
from basic_memory.mcp.async_client import client
|
|
12
|
+
from basic_memory.mcp.server import mcp
|
|
13
|
+
from basic_memory.mcp.tools.utils import call_put
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
@mcp.tool(
|
|
17
|
+
description="Create an Obsidian canvas file to visualize concepts and connections.",
|
|
18
|
+
)
|
|
19
|
+
async def canvas(
|
|
20
|
+
nodes: List[Dict[str, Any]],
|
|
21
|
+
edges: List[Dict[str, Any]],
|
|
22
|
+
title: str,
|
|
23
|
+
folder: str,
|
|
24
|
+
) -> str:
|
|
25
|
+
"""Create an Obsidian canvas file with the provided nodes and edges.
|
|
26
|
+
|
|
27
|
+
This tool creates a .canvas file compatible with Obsidian's Canvas feature,
|
|
28
|
+
allowing visualization of relationships between concepts or documents.
|
|
29
|
+
|
|
30
|
+
For the full JSON Canvas 1.0 specification, see the 'spec://canvas' resource.
|
|
31
|
+
|
|
32
|
+
Args:
|
|
33
|
+
nodes: List of node objects following JSON Canvas 1.0 spec
|
|
34
|
+
edges: List of edge objects following JSON Canvas 1.0 spec
|
|
35
|
+
title: The title of the canvas (will be saved as title.canvas)
|
|
36
|
+
folder: The folder where the file should be saved
|
|
37
|
+
|
|
38
|
+
Returns:
|
|
39
|
+
A summary of the created canvas file
|
|
40
|
+
|
|
41
|
+
Important Notes:
|
|
42
|
+
- When referencing files, use the exact file path as shown in Obsidian
|
|
43
|
+
Example: "folder/Document Name.md" (not permalink format)
|
|
44
|
+
- For file nodes, the "file" attribute must reference an existing file
|
|
45
|
+
- Nodes require id, type, x, y, width, height properties
|
|
46
|
+
- Edges require id, fromNode, toNode properties
|
|
47
|
+
- Position nodes in a logical layout (x,y coordinates in pixels)
|
|
48
|
+
- Use color attributes ("1"-"6" or hex) for visual organization
|
|
49
|
+
|
|
50
|
+
Basic Structure:
|
|
51
|
+
```json
|
|
52
|
+
{
|
|
53
|
+
"nodes": [
|
|
54
|
+
{
|
|
55
|
+
"id": "node1",
|
|
56
|
+
"type": "file", // Options: "file", "text", "link", "group"
|
|
57
|
+
"file": "folder/Document.md",
|
|
58
|
+
"x": 0,
|
|
59
|
+
"y": 0,
|
|
60
|
+
"width": 400,
|
|
61
|
+
"height": 300
|
|
62
|
+
}
|
|
63
|
+
],
|
|
64
|
+
"edges": [
|
|
65
|
+
{
|
|
66
|
+
"id": "edge1",
|
|
67
|
+
"fromNode": "node1",
|
|
68
|
+
"toNode": "node2",
|
|
69
|
+
"label": "connects to"
|
|
70
|
+
}
|
|
71
|
+
]
|
|
72
|
+
}
|
|
73
|
+
```
|
|
74
|
+
"""
|
|
75
|
+
# Ensure path has .canvas extension
|
|
76
|
+
file_title = title if title.endswith(".canvas") else f"{title}.canvas"
|
|
77
|
+
file_path = f"{folder}/{file_title}"
|
|
78
|
+
|
|
79
|
+
# Create canvas data structure
|
|
80
|
+
canvas_data = {"nodes": nodes, "edges": edges}
|
|
81
|
+
|
|
82
|
+
# Convert to JSON
|
|
83
|
+
canvas_json = json.dumps(canvas_data, indent=2)
|
|
84
|
+
|
|
85
|
+
# Write the file using the resource API
|
|
86
|
+
logger.info(f"Creating canvas file: {file_path}")
|
|
87
|
+
response = await call_put(client, f"/resource/{file_path}", json=canvas_json)
|
|
88
|
+
|
|
89
|
+
# Parse response
|
|
90
|
+
result = response.json()
|
|
91
|
+
logger.debug(result)
|
|
92
|
+
|
|
93
|
+
# Build summary
|
|
94
|
+
action = "Created" if response.status_code == 201 else "Updated"
|
|
95
|
+
summary = [f"# {action}: {file_path}", "\nThe canvas is ready to open in Obsidian."]
|
|
96
|
+
|
|
97
|
+
return "\n".join(summary)
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
from basic_memory.mcp.tools.utils import call_delete
|
|
2
|
+
|
|
3
|
+
|
|
4
|
+
from basic_memory.mcp.server import mcp
|
|
5
|
+
from basic_memory.mcp.async_client import client
|
|
6
|
+
from basic_memory.schemas import DeleteEntitiesResponse
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
@mcp.tool(description="Delete a note by title or permalink")
|
|
10
|
+
async def delete_note(identifier: str) -> bool:
|
|
11
|
+
"""Delete a note from the knowledge base.
|
|
12
|
+
|
|
13
|
+
Args:
|
|
14
|
+
identifier: Note title or permalink
|
|
15
|
+
|
|
16
|
+
Returns:
|
|
17
|
+
True if note was deleted, False otherwise
|
|
18
|
+
|
|
19
|
+
Examples:
|
|
20
|
+
# Delete by title
|
|
21
|
+
delete_note("Meeting Notes: Project Planning")
|
|
22
|
+
|
|
23
|
+
# Delete by permalink
|
|
24
|
+
delete_note("notes/project-planning")
|
|
25
|
+
"""
|
|
26
|
+
response = await call_delete(client, f"/knowledge/entities/{identifier}")
|
|
27
|
+
result = DeleteEntitiesResponse.model_validate(response.json())
|
|
28
|
+
return result.deleted
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
"""Project info tool for Basic Memory MCP server."""
|
|
2
|
+
|
|
3
|
+
from loguru import logger
|
|
4
|
+
|
|
5
|
+
from basic_memory.mcp.async_client import client
|
|
6
|
+
from basic_memory.mcp.server import mcp
|
|
7
|
+
from basic_memory.mcp.tools.utils import call_get
|
|
8
|
+
from basic_memory.schemas import ProjectInfoResponse
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
@mcp.tool(
|
|
12
|
+
description="Get information and statistics about the current Basic Memory project.",
|
|
13
|
+
)
|
|
14
|
+
async def project_info() -> ProjectInfoResponse:
|
|
15
|
+
"""Get comprehensive information about the current Basic Memory project.
|
|
16
|
+
|
|
17
|
+
This tool provides detailed statistics and status information about your
|
|
18
|
+
Basic Memory project, including:
|
|
19
|
+
|
|
20
|
+
- Project configuration
|
|
21
|
+
- Entity, observation, and relation counts
|
|
22
|
+
- Graph metrics (most connected entities, isolated entities)
|
|
23
|
+
- Recent activity and growth over time
|
|
24
|
+
- System status (database, watch service, version)
|
|
25
|
+
|
|
26
|
+
Use this tool to:
|
|
27
|
+
- Verify your Basic Memory installation is working correctly
|
|
28
|
+
- Get insights into your knowledge base structure
|
|
29
|
+
- Monitor growth and activity over time
|
|
30
|
+
- Identify potential issues like unresolved relations
|
|
31
|
+
|
|
32
|
+
Returns:
|
|
33
|
+
Detailed project information and statistics
|
|
34
|
+
|
|
35
|
+
Examples:
|
|
36
|
+
# Get information about the current project
|
|
37
|
+
info = await project_info()
|
|
38
|
+
|
|
39
|
+
# Check entity counts
|
|
40
|
+
print(f"Total entities: {info.statistics.total_entities}")
|
|
41
|
+
|
|
42
|
+
# Check system status
|
|
43
|
+
print(f"Basic Memory version: {info.system.version}")
|
|
44
|
+
"""
|
|
45
|
+
logger.info("Getting project info")
|
|
46
|
+
|
|
47
|
+
# Call the API endpoint
|
|
48
|
+
response = await call_get(client, "/stats/project-info")
|
|
49
|
+
|
|
50
|
+
# Convert response to ProjectInfoResponse
|
|
51
|
+
return ProjectInfoResponse.model_validate(response.json())
|