mem-brain-mcp 1.0.8__py3-none-any.whl → 1.1.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.
- mem_brain_mcp/server.py +71 -732
- {mem_brain_mcp-1.0.8.dist-info → mem_brain_mcp-1.1.0.dist-info}/METADATA +30 -5
- {mem_brain_mcp-1.0.8.dist-info → mem_brain_mcp-1.1.0.dist-info}/RECORD +5 -5
- {mem_brain_mcp-1.0.8.dist-info → mem_brain_mcp-1.1.0.dist-info}/WHEEL +0 -0
- {mem_brain_mcp-1.0.8.dist-info → mem_brain_mcp-1.1.0.dist-info}/entry_points.txt +0 -0
mem_brain_mcp/server.py
CHANGED
|
@@ -7,7 +7,6 @@ import httpx
|
|
|
7
7
|
from fastmcp import FastMCP
|
|
8
8
|
from fastmcp.server.context import request_ctx
|
|
9
9
|
from fastmcp.exceptions import ToolError
|
|
10
|
-
from fastmcp.prompts.prompt import PromptMessage, TextContent
|
|
11
10
|
from starlette.requests import Request
|
|
12
11
|
from starlette.responses import JSONResponse
|
|
13
12
|
|
|
@@ -26,7 +25,7 @@ AGENT_INSTRUCTIONS = """You are an intelligent assistant with a persistent, evol
|
|
|
26
25
|
**1. SEARCH FIRST & SMART** — Before answering personal questions, call `search_memories`.
|
|
27
26
|
- **Formulate specific, natural language queries**, NOT simple keywords.
|
|
28
27
|
- ❌ `query="maga"` (Weak)
|
|
29
|
-
- ✅ `query="Who is Maga and what is his relationship to me?"` (Strong
|
|
28
|
+
- ✅ `query="Who is Maga and what is his relationship to me?"` (Strong)
|
|
30
29
|
- **Use `keyword_filter` for Scoping**: Deterministically isolate context by project, session, or topic.
|
|
31
30
|
- ✅ `search_memories(query="...", keyword_filter="project-x")` (Matches memories tagged with project-x)
|
|
32
31
|
- ✅ `search_memories(query="...", keyword_filter="session-.*-2026")` (Regex match for 2026 sessions)
|
|
@@ -40,30 +39,17 @@ AGENT_INSTRUCTIONS = """You are an intelligent assistant with a persistent, evol
|
|
|
40
39
|
**3. PASSIVE STORAGE** — When user reveals preferences, store the **FACT** (not conversation).
|
|
41
40
|
- User: "I think I wanna try that sushi spot" → Store: "User interested in new sushi restaurant"
|
|
42
41
|
|
|
43
|
-
**4. KEEP IT CURRENT** — If user contradicts a past memory, use `update_memory`.
|
|
44
|
-
|
|
45
42
|
---
|
|
46
43
|
|
|
47
44
|
## 🛠️ TOOLS
|
|
48
45
|
|
|
49
|
-
### Core Operations
|
|
50
|
-
|
|
51
46
|
| Tool | When to Use |
|
|
52
47
|
|------|-------------|
|
|
53
48
|
| `search_memories(query, k=5)` | Before answering ANY personal question |
|
|
54
49
|
| `get_memories(memory_ids)` | Need full details for specific IDs |
|
|
55
50
|
| `add_memory(content, tags=[], category="")` | User reveals preference/goal/fact |
|
|
56
|
-
| `
|
|
51
|
+
| `get_stats()` | Check memory coverage and system health |
|
|
57
52
|
| `delete_memories(memory_id)` | Memory is wrong or user requests deletion |
|
|
58
|
-
| `unlink_memories(id1, id2)` | Connection no longer relevant |
|
|
59
|
-
| `get_stats()` | User asks "how much do you remember?" |
|
|
60
|
-
|
|
61
|
-
### Graph Intelligence (Advanced)
|
|
62
|
-
|
|
63
|
-
| Tool | Purpose | Example |
|
|
64
|
-
|------|---------|---------|
|
|
65
|
-
| `find_path(from_id, to_id)` | Explain connections | "How is coffee related to health?" → Shows: Coffee→Caffeine→Health |
|
|
66
|
-
| `get_neighborhood(memory_id, hops=2)` | Deep context | Get 2-hop radius around a memory |
|
|
67
53
|
|
|
68
54
|
---
|
|
69
55
|
|
|
@@ -78,10 +64,10 @@ AGENT_INSTRUCTIONS = """You are an intelligent assistant with a persistent, evol
|
|
|
78
64
|
- Type: `preference`, `constraint`, `goal`, `fact`, `event`
|
|
79
65
|
- Priority: `important`, `routine`, `temporary`
|
|
80
66
|
|
|
81
|
-
**
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
67
|
+
**System handles updates automatically:**
|
|
68
|
+
- When user changes preferences, just add the new memory
|
|
69
|
+
- The system automatically links related memories and manages updates
|
|
70
|
+
- No manual linking or unlinking needed
|
|
85
71
|
|
|
86
72
|
---
|
|
87
73
|
|
|
@@ -90,20 +76,11 @@ AGENT_INSTRUCTIONS = """You are an intelligent assistant with a persistent, evol
|
|
|
90
76
|
| Signal | Action |
|
|
91
77
|
|--------|--------|
|
|
92
78
|
| "I'm trying X", "exploring Y" | ADD new memory (temporary exploration) |
|
|
93
|
-
| "I no longer like X", "I switched to Y" |
|
|
79
|
+
| "I no longer like X", "I switched to Y" | ADD new memory (the system handles updates automatically) |
|
|
94
80
|
| Contradictory with equal weight | ADD with temporal context ("as of 2025") |
|
|
95
81
|
|
|
96
82
|
---
|
|
97
83
|
|
|
98
|
-
## ⚡ ARCHITECTURE (Brief)
|
|
99
|
-
|
|
100
|
-
- **Graph Structure**: Memories = nodes, links = edges
|
|
101
|
-
- **Search**: Semantic similarity (70%) + importance/connections (30%)
|
|
102
|
-
- **Auto-linking**: System creates links for narrative/causal connections
|
|
103
|
-
- **User isolation**: Separate database per user
|
|
104
|
-
|
|
105
|
-
---
|
|
106
|
-
|
|
107
84
|
## ✅ BEST PRACTICES
|
|
108
85
|
|
|
109
86
|
| DO | DON'T |
|
|
@@ -111,7 +88,6 @@ AGENT_INSTRUCTIONS = """You are an intelligent assistant with a persistent, evol
|
|
|
111
88
|
| Search before answering personal Q's | Guess without searching |
|
|
112
89
|
| Check `related_memories` field | Ignore graph connections |
|
|
113
90
|
| Store explicit facts | Store vague conversation |
|
|
114
|
-
| Update when info changes | Create duplicates |
|
|
115
91
|
| Synthesize across memories | Just list facts |
|
|
116
92
|
|
|
117
93
|
**Remember:** You're not a database. Connect the dots to provide thoughtful, personalized responses."""
|
|
@@ -170,68 +146,6 @@ mcp = FastMCP("Mem-Brain MCP")
|
|
|
170
146
|
api_client = APIClient()
|
|
171
147
|
|
|
172
148
|
|
|
173
|
-
async def _get_dynamic_context() -> str:
|
|
174
|
-
"""Fetch dynamic context (core identity + recent memories) from API."""
|
|
175
|
-
try:
|
|
176
|
-
# Get core identity
|
|
177
|
-
client = await _get_api_client()
|
|
178
|
-
identity_response = await client._request(
|
|
179
|
-
"POST", "/memories/search", json={"query": "user name location job identity", "k": 10}
|
|
180
|
-
)
|
|
181
|
-
|
|
182
|
-
identity_memories = []
|
|
183
|
-
for mem in identity_response.get("results", []):
|
|
184
|
-
tags = mem.get("tags", [])
|
|
185
|
-
if any(
|
|
186
|
-
tag in tags
|
|
187
|
-
for tag in {
|
|
188
|
-
"user_info",
|
|
189
|
-
"name",
|
|
190
|
-
"location",
|
|
191
|
-
"job",
|
|
192
|
-
"core_identity",
|
|
193
|
-
"identity",
|
|
194
|
-
"personal",
|
|
195
|
-
}
|
|
196
|
-
):
|
|
197
|
-
identity_memories.append(mem)
|
|
198
|
-
|
|
199
|
-
identity_section = ""
|
|
200
|
-
if identity_memories:
|
|
201
|
-
identity_section = "## 🧬 Core Identity\n"
|
|
202
|
-
for memory in identity_memories[:3]:
|
|
203
|
-
identity_section += f"- {memory['content']}\n"
|
|
204
|
-
|
|
205
|
-
# Get recent context
|
|
206
|
-
recent_response = await client._request(
|
|
207
|
-
"POST", "/memories/search", json={"query": "recent context", "k": 3}
|
|
208
|
-
)
|
|
209
|
-
|
|
210
|
-
recent_section = ""
|
|
211
|
-
if recent_response.get("results"):
|
|
212
|
-
recent_section = "## 🕐 Recent Context\n"
|
|
213
|
-
for memory in recent_response.get("results", [])[:3]:
|
|
214
|
-
content = memory["content"]
|
|
215
|
-
truncated = content[:100] + "..." if len(content) > 100 else content
|
|
216
|
-
recent_section += f"- {truncated}\n"
|
|
217
|
-
|
|
218
|
-
return f"""### 🧠 YOUR BRAIN (Current Working Context)
|
|
219
|
-
{identity_section if identity_section else "*No core identity established yet*"}
|
|
220
|
-
{recent_section if recent_section else "*No recent context*"}
|
|
221
|
-
|
|
222
|
-
---
|
|
223
|
-
|
|
224
|
-
"""
|
|
225
|
-
except Exception as e:
|
|
226
|
-
logger.warning(f"Could not fetch dynamic context: {e}")
|
|
227
|
-
return """### 🧠 YOUR BRAIN (Current Working Context)
|
|
228
|
-
*Context loading failed - API may be unavailable*
|
|
229
|
-
|
|
230
|
-
---
|
|
231
|
-
|
|
232
|
-
"""
|
|
233
|
-
|
|
234
|
-
|
|
235
149
|
# ============================================================================
|
|
236
150
|
# RESOURCES (Documentation that LLMs can read)
|
|
237
151
|
# ============================================================================
|
|
@@ -240,7 +154,7 @@ async def _get_dynamic_context() -> str:
|
|
|
240
154
|
@mcp.resource("mem-brain://docs/workflow-guide")
|
|
241
155
|
def workflow_guide() -> str:
|
|
242
156
|
"""Complete guide to the memory workflow: search strategies, pattern recognition, storage guidelines, and best practices."""
|
|
243
|
-
return """#
|
|
157
|
+
return """# Memory Workflow Guide
|
|
244
158
|
|
|
245
159
|
## 🎯 CORE DIRECTIVE
|
|
246
160
|
**Synthesize**, don't just retrieve. Connect user's request to their past preferences, habits, and constraints.
|
|
@@ -250,7 +164,7 @@ def workflow_guide() -> str:
|
|
|
250
164
|
**1. SEARCH FIRST & SMART** — Before answering personal questions, call `search_memories`.
|
|
251
165
|
- **Formulate specific, natural language queries**, NOT simple keywords.
|
|
252
166
|
- ❌ `query="maga"` (Weak)
|
|
253
|
-
- ✅ `query="Who is Maga and what is his relationship to me?"` (Strong
|
|
167
|
+
- ✅ `query="Who is Maga and what is his relationship to me?"` (Strong)
|
|
254
168
|
- Check `related_memories` field — these are auto-expanded graph neighbors.
|
|
255
169
|
- Synthesize: If "coffee" result links to "acid reflux", suggest cold brew.
|
|
256
170
|
|
|
@@ -261,8 +175,6 @@ def workflow_guide() -> str:
|
|
|
261
175
|
**3. PASSIVE STORAGE** — When user reveals preferences, store the **FACT** (not conversation).
|
|
262
176
|
- User: "I think I wanna try that sushi spot" → Store: "User interested in new sushi restaurant"
|
|
263
177
|
|
|
264
|
-
**4. KEEP IT CURRENT** — If user contradicts a past memory, use `update_memory`.
|
|
265
|
-
|
|
266
178
|
## ✅ BEST PRACTICES
|
|
267
179
|
|
|
268
180
|
| DO | DON'T |
|
|
@@ -270,59 +182,12 @@ def workflow_guide() -> str:
|
|
|
270
182
|
| Search before answering personal Q's | Guess without searching |
|
|
271
183
|
| Check `related_memories` field | Ignore graph connections |
|
|
272
184
|
| Store explicit facts | Store vague conversation |
|
|
273
|
-
| Update when info changes | Create duplicates |
|
|
274
185
|
| Synthesize across memories | Just list facts |
|
|
275
186
|
|
|
276
187
|
**Remember:** You're not a database. Connect the dots to provide thoughtful, personalized responses.
|
|
277
188
|
"""
|
|
278
189
|
|
|
279
190
|
|
|
280
|
-
@mcp.resource("mem-brain://docs/tool-reference")
|
|
281
|
-
def tool_reference() -> str:
|
|
282
|
-
"""Detailed reference for when and how to use each memory tool effectively."""
|
|
283
|
-
return """# Tool Usage Reference
|
|
284
|
-
|
|
285
|
-
## Core Operations
|
|
286
|
-
|
|
287
|
-
### `search_memories(query, k=5)`
|
|
288
|
-
**When to Use**: Before answering ANY personal question
|
|
289
|
-
**Critical**: Formulate specific, natural language queries, NOT simple keywords
|
|
290
|
-
- ✅ Good: "Who is Maga and what is their relationship to me?"
|
|
291
|
-
- ❌ Bad: "maga"
|
|
292
|
-
|
|
293
|
-
### `get_memories(memory_ids)`
|
|
294
|
-
**When to Use**: Need full details for specific IDs identified from search results
|
|
295
|
-
|
|
296
|
-
### `add_memory(content, tags=[], category="")`
|
|
297
|
-
**When to Use**: User reveals preference/goal/fact
|
|
298
|
-
**Storage Rule**: Store FACTS, not conversation
|
|
299
|
-
- ✅ "User prefers dark mode interfaces"
|
|
300
|
-
- ❌ "You said you like dark mode"
|
|
301
|
-
|
|
302
|
-
### `update_memory(memory_id, content=..., tags=...)`
|
|
303
|
-
**When to Use**: Information evolves or changes, user contradicts past memory
|
|
304
|
-
|
|
305
|
-
### `delete_memories(memory_id)`
|
|
306
|
-
**When to Use**: Memory is wrong or user explicitly requests deletion
|
|
307
|
-
|
|
308
|
-
### `unlink_memories(id1, id2)`
|
|
309
|
-
**When to Use**: Connection no longer relevant or accurate
|
|
310
|
-
|
|
311
|
-
### `get_stats()`
|
|
312
|
-
**When to Use**: User asks "how much do you remember?" or wants overview
|
|
313
|
-
|
|
314
|
-
## Graph Intelligence
|
|
315
|
-
|
|
316
|
-
### `find_path(from_id, to_id)`
|
|
317
|
-
**Purpose**: Explain connections between memories
|
|
318
|
-
**Example**: "How is coffee related to health?" → Shows path: Coffee→Caffeine→Health
|
|
319
|
-
|
|
320
|
-
### `get_neighborhood(memory_id, hops=2)`
|
|
321
|
-
**Purpose**: Get deep context around a memory
|
|
322
|
-
**Use Case**: Understanding relationships around important memories
|
|
323
|
-
"""
|
|
324
|
-
|
|
325
|
-
|
|
326
191
|
@mcp.resource("mem-brain://docs/storage-guidelines")
|
|
327
192
|
def storage_guidelines() -> str:
|
|
328
193
|
"""Best practices for storing facts, tagging patterns, and avoiding duplicates."""
|
|
@@ -339,79 +204,31 @@ def storage_guidelines() -> str:
|
|
|
339
204
|
**Types**: `preference`, `constraint`, `goal`, `fact`, `event`
|
|
340
205
|
**Priority**: `important`, `routine`, `temporary`
|
|
341
206
|
|
|
342
|
-
##
|
|
207
|
+
## System Handles Updates Automatically
|
|
343
208
|
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
209
|
+
- When user changes preferences, just add the new memory
|
|
210
|
+
- The system automatically links related memories and manages updates
|
|
211
|
+
- No manual linking or unlinking needed
|
|
347
212
|
|
|
348
213
|
## Changing Preferences
|
|
349
214
|
|
|
350
215
|
| Signal | Action |
|
|
351
216
|
|--------|--------|
|
|
352
217
|
| "I'm trying X", "exploring Y" | ADD new memory (temporary exploration) |
|
|
353
|
-
| "I no longer like X", "I switched to Y" |
|
|
218
|
+
| "I no longer like X", "I switched to Y" | ADD new memory (the system handles updates automatically) |
|
|
354
219
|
| Contradictory with equal weight | ADD with temporal context ("as of 2025") |
|
|
355
|
-
|
|
356
|
-
## Architecture
|
|
357
|
-
|
|
358
|
-
- **Graph Structure**: Memories = nodes, links = edges
|
|
359
|
-
- **Search**: Semantic similarity (70%) + importance/connections (30%)
|
|
360
|
-
- **Auto-linking**: System creates links for narrative/causal connections
|
|
361
|
-
- **User isolation**: Separate database per user
|
|
362
220
|
"""
|
|
363
221
|
|
|
364
222
|
|
|
365
|
-
# ============================================================================
|
|
366
|
-
# PROMPTS (Bootstrap Intelligence)
|
|
367
|
-
# ============================================================================
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
@mcp.prompt
|
|
371
|
-
async def setup_personal_memory() -> PromptMessage:
|
|
372
|
-
"""Initializes the assistant with the user's identity, recent context, and memory management rules. Run this once at the start of a session."""
|
|
373
|
-
context_section = await _get_dynamic_context()
|
|
374
|
-
|
|
375
|
-
full_instructions = f"""{context_section}{AGENT_INSTRUCTIONS}
|
|
376
|
-
|
|
377
|
-
**Note**: For detailed tool usage, see resource: `mem-brain://docs/tool-reference`
|
|
378
|
-
For storage guidelines, see resource: `mem-brain://docs/storage-guidelines`
|
|
379
|
-
"""
|
|
380
|
-
|
|
381
|
-
return PromptMessage(role="system", content=TextContent(type="text", text=full_instructions))
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
@mcp.prompt
|
|
385
|
-
async def refresh_context() -> PromptMessage:
|
|
386
|
-
"""Refreshes the assistant's context with updated core identity and recent memories. Use when context feels stale."""
|
|
387
|
-
context_section = await _get_dynamic_context()
|
|
388
|
-
|
|
389
|
-
return PromptMessage(
|
|
390
|
-
role="system",
|
|
391
|
-
content=TextContent(
|
|
392
|
-
type="text",
|
|
393
|
-
text=f"""{context_section}
|
|
394
|
-
|
|
395
|
-
**Context refreshed.** Continue using memory tools as before.
|
|
396
|
-
""",
|
|
397
|
-
),
|
|
398
|
-
)
|
|
399
|
-
|
|
400
|
-
|
|
401
223
|
# ============================================================================
|
|
402
224
|
# TOOLS (Operations)
|
|
403
225
|
# ============================================================================
|
|
404
226
|
|
|
405
227
|
|
|
406
228
|
@mcp.tool()
|
|
407
|
-
async def get_agent_instructions(
|
|
229
|
+
async def get_agent_instructions() -> str:
|
|
408
230
|
"""Get comprehensive system prompt and best practices for using the memory system effectively. This contains the intelligence for smart memory management, search strategies, and agent workflows."""
|
|
409
|
-
|
|
410
|
-
context_section = await _get_dynamic_context()
|
|
411
|
-
else:
|
|
412
|
-
context_section = ""
|
|
413
|
-
|
|
414
|
-
return context_section + AGENT_INSTRUCTIONS
|
|
231
|
+
return AGENT_INSTRUCTIONS
|
|
415
232
|
|
|
416
233
|
|
|
417
234
|
@mcp.tool()
|
|
@@ -792,187 +609,6 @@ async def get_memories(memory_ids: List[str]) -> str:
|
|
|
792
609
|
raise ToolError(f"Error getting memories: {str(e)}")
|
|
793
610
|
|
|
794
611
|
|
|
795
|
-
@mcp.tool()
|
|
796
|
-
async def update_memory(
|
|
797
|
-
memory_id: str, content: Optional[str] = None, tags: Optional[Union[List[str], str]] = None
|
|
798
|
-
) -> str:
|
|
799
|
-
"""Update an existing memory when information evolves or changes. Use this when user contradicts a past memory ('I no longer like X') or when details need updating.
|
|
800
|
-
|
|
801
|
-
Parameters:
|
|
802
|
-
memory_id (str, REQUIRED): The ID of the memory to update. Must be a non-empty string.
|
|
803
|
-
- Example: "480c1f76-bcdf-4491-8781-24510db992e3"
|
|
804
|
-
- Get memory IDs from search_memories() or get_memories() results
|
|
805
|
-
|
|
806
|
-
content (str, optional): New content for the memory.
|
|
807
|
-
- Can be None (to keep existing content) or a non-empty string
|
|
808
|
-
- If provided, must not be empty or whitespace-only
|
|
809
|
-
- Example: "User no longer likes TypeScript, prefers Python"
|
|
810
|
-
|
|
811
|
-
tags (list[str] or str, optional): New tags for the memory.
|
|
812
|
-
- Can be None (to keep existing tags), a list of strings, a comma-separated string, or a JSON array string
|
|
813
|
-
- If provided, replaces existing tags
|
|
814
|
-
- Example: ["coding", "python"]
|
|
815
|
-
- Example: "coding,python" (comma-separated)
|
|
816
|
-
- Example: '["coding", "python"]' (JSON string)
|
|
817
|
-
- Note: The system can auto-generate tags if you omit this parameter
|
|
818
|
-
|
|
819
|
-
Returns:
|
|
820
|
-
str: A formatted string with the updated memory details.
|
|
821
|
-
|
|
822
|
-
Common Errors and Solutions:
|
|
823
|
-
- Error: "Tool call arguments for mcp were invalid"
|
|
824
|
-
Solution: Ensure 'memory_id' parameter is provided as a string. Example: update_memory(memory_id="...")
|
|
825
|
-
|
|
826
|
-
- Error: "memory_id cannot be empty"
|
|
827
|
-
Solution: Provide a valid memory ID from search results. Example: update_memory(memory_id="480c1f76-...")
|
|
828
|
-
|
|
829
|
-
- Error: "At least one of 'content' or 'tags' must be provided"
|
|
830
|
-
Solution: Provide content or tags to update. Example: update_memory(memory_id="...", content="New content")
|
|
831
|
-
|
|
832
|
-
Examples:
|
|
833
|
-
# Update content only
|
|
834
|
-
update_memory(memory_id="480c1f76-...", content="User prefers Python over JavaScript")
|
|
835
|
-
|
|
836
|
-
# Update tags only
|
|
837
|
-
update_memory(memory_id="480c1f76-...", tags=["coding", "preferences"])
|
|
838
|
-
|
|
839
|
-
# Update both content and tags
|
|
840
|
-
update_memory(
|
|
841
|
-
memory_id="480c1f76-...",
|
|
842
|
-
content="User no longer likes TypeScript",
|
|
843
|
-
tags=["coding", "python"]
|
|
844
|
-
)
|
|
845
|
-
"""
|
|
846
|
-
# Validate parameters with detailed error messages
|
|
847
|
-
if memory_id is None:
|
|
848
|
-
raise ToolError(
|
|
849
|
-
"The 'memory_id' parameter is required but was not provided.\n"
|
|
850
|
-
"Get memory IDs from search_memories() or get_memories() results.\n"
|
|
851
|
-
'Example: update_memory(memory_id="480c1f76-bcdf-4491-8781-24510db992e3", content="New content")'
|
|
852
|
-
)
|
|
853
|
-
|
|
854
|
-
if not isinstance(memory_id, str):
|
|
855
|
-
raise ToolError(
|
|
856
|
-
f"The 'memory_id' parameter must be a string, but got {type(memory_id).__name__}.\n"
|
|
857
|
-
f"Received: {repr(memory_id)}\n"
|
|
858
|
-
'Example: update_memory(memory_id="480c1f76-...", content="New content")'
|
|
859
|
-
)
|
|
860
|
-
|
|
861
|
-
memory_id_str = memory_id.strip()
|
|
862
|
-
if not memory_id_str:
|
|
863
|
-
raise ToolError(
|
|
864
|
-
"The 'memory_id' parameter cannot be empty or whitespace-only.\n"
|
|
865
|
-
"Get memory IDs from search_memories() or get_memories() results.\n"
|
|
866
|
-
'Example: update_memory(memory_id="480c1f76-bcdf-4491-8781-24510db992e3", content="New content")'
|
|
867
|
-
)
|
|
868
|
-
|
|
869
|
-
# Validate that at least one update parameter is provided
|
|
870
|
-
if content is None and tags is None:
|
|
871
|
-
raise ToolError(
|
|
872
|
-
"At least one of 'content' or 'tags' must be provided to update the memory.\n"
|
|
873
|
-
'Example: update_memory(memory_id="...", content="New content")\n'
|
|
874
|
-
'Example: update_memory(memory_id="...", tags=["new", "tags"])'
|
|
875
|
-
)
|
|
876
|
-
|
|
877
|
-
# Validate content if provided
|
|
878
|
-
if content is not None:
|
|
879
|
-
if not isinstance(content, str):
|
|
880
|
-
raise ToolError(
|
|
881
|
-
f"The 'content' parameter must be a string or None, but got {type(content).__name__}.\n"
|
|
882
|
-
f"Received: {repr(content)}\n"
|
|
883
|
-
'Example: update_memory(memory_id="...", content="New content")'
|
|
884
|
-
)
|
|
885
|
-
content_str = str(content).strip()
|
|
886
|
-
if not content_str:
|
|
887
|
-
raise ToolError(
|
|
888
|
-
"The 'content' parameter cannot be empty or whitespace-only.\n"
|
|
889
|
-
"Provide a non-empty string or omit the parameter to keep existing content.\n"
|
|
890
|
-
'Example: update_memory(memory_id="...", content="New content")'
|
|
891
|
-
)
|
|
892
|
-
else:
|
|
893
|
-
content_str = None
|
|
894
|
-
|
|
895
|
-
# Validate tags if provided - handle various input formats
|
|
896
|
-
normalized_tags = None
|
|
897
|
-
if tags is not None:
|
|
898
|
-
if isinstance(tags, list):
|
|
899
|
-
# Validate list contents are strings
|
|
900
|
-
if tags:
|
|
901
|
-
invalid_items = [item for item in tags if not isinstance(item, str)]
|
|
902
|
-
if invalid_items:
|
|
903
|
-
raise ToolError(
|
|
904
|
-
f"The 'tags' parameter must be a list of strings, but found non-string items: {invalid_items}\n"
|
|
905
|
-
'Example: update_memory(memory_id="...", tags=["coding", "preferences"])\n'
|
|
906
|
-
'Example: update_memory(memory_id="...", tags=None) # or omit tags parameter'
|
|
907
|
-
)
|
|
908
|
-
normalized_tags = tags if tags else None # Empty list becomes None
|
|
909
|
-
elif isinstance(tags, str):
|
|
910
|
-
tags_str = tags.strip()
|
|
911
|
-
if not tags_str:
|
|
912
|
-
normalized_tags = None
|
|
913
|
-
else:
|
|
914
|
-
# Try to parse as JSON array first (e.g., '["tag1", "tag2"]')
|
|
915
|
-
try:
|
|
916
|
-
parsed = json.loads(tags_str)
|
|
917
|
-
if isinstance(parsed, list):
|
|
918
|
-
normalized_tags = [
|
|
919
|
-
str(item).strip() for item in parsed if str(item).strip()
|
|
920
|
-
]
|
|
921
|
-
else:
|
|
922
|
-
# If JSON but not a list, treat as single tag
|
|
923
|
-
normalized_tags = [tags_str]
|
|
924
|
-
except (json.JSONDecodeError, ValueError):
|
|
925
|
-
# Not JSON, try comma-separated string
|
|
926
|
-
if "," in tags_str:
|
|
927
|
-
normalized_tags = [
|
|
928
|
-
tag.strip() for tag in tags_str.split(",") if tag.strip()
|
|
929
|
-
]
|
|
930
|
-
else:
|
|
931
|
-
# Single tag string
|
|
932
|
-
normalized_tags = [tags_str]
|
|
933
|
-
else:
|
|
934
|
-
raise ToolError(
|
|
935
|
-
f"The 'tags' parameter must be a list of strings, a comma-separated string, or None, but got {type(tags).__name__}.\n"
|
|
936
|
-
f"Received: {repr(tags)}\n"
|
|
937
|
-
'Example: update_memory(memory_id="...", tags=["coding", "preferences"])\n'
|
|
938
|
-
'Example: update_memory(memory_id="...", tags="coding,preferences")\n'
|
|
939
|
-
'Example: update_memory(memory_id="...", tags=None) # or omit tags parameter'
|
|
940
|
-
)
|
|
941
|
-
|
|
942
|
-
try:
|
|
943
|
-
logger.info(
|
|
944
|
-
f"update_memory called - memory_id: {memory_id_str}, content length: {len(content_str) if content_str else 0}, tags: {normalized_tags}"
|
|
945
|
-
)
|
|
946
|
-
|
|
947
|
-
client = await _get_api_client()
|
|
948
|
-
result = await client.update_memory(memory_id_str, content_str, normalized_tags)
|
|
949
|
-
memory = result.get("memory")
|
|
950
|
-
if memory:
|
|
951
|
-
return f"Memory updated:\n{_format_memory(memory)}"
|
|
952
|
-
return f"Memory {memory_id_str} updated"
|
|
953
|
-
except httpx.HTTPStatusError as e:
|
|
954
|
-
error_detail = e.response.text if e.response else "Unknown error"
|
|
955
|
-
logger.error(f"API error: {e.response.status_code} - {error_detail}")
|
|
956
|
-
if e.response.status_code == 401:
|
|
957
|
-
raise ToolError(
|
|
958
|
-
"Authentication failed. Please login using the 'login' tool or configure your JWT token in the MCP client headers."
|
|
959
|
-
)
|
|
960
|
-
elif e.response.status_code == 400:
|
|
961
|
-
raise ToolError(
|
|
962
|
-
f'Invalid request: {error_detail}\nExample: update_memory(memory_id="...", content="New content")'
|
|
963
|
-
)
|
|
964
|
-
elif e.response.status_code == 404:
|
|
965
|
-
raise ToolError(
|
|
966
|
-
f"Memory not found: {memory_id_str}\nVerify the memory_id is correct by searching for it first."
|
|
967
|
-
)
|
|
968
|
-
raise ToolError(f"Failed to update memory: HTTP {e.response.status_code} - {error_detail}")
|
|
969
|
-
except ToolError:
|
|
970
|
-
raise
|
|
971
|
-
except Exception as e:
|
|
972
|
-
logger.error(f"Unexpected error in update_memory: {e}", exc_info=True)
|
|
973
|
-
raise ToolError(f"Error updating memory: {str(e)}")
|
|
974
|
-
|
|
975
|
-
|
|
976
612
|
@mcp.tool()
|
|
977
613
|
async def delete_memories(
|
|
978
614
|
memory_id: Optional[str] = None, tags: Optional[str] = None, category: Optional[str] = None
|
|
@@ -1107,355 +743,64 @@ async def delete_memories(
|
|
|
1107
743
|
|
|
1108
744
|
|
|
1109
745
|
@mcp.tool()
|
|
1110
|
-
async def
|
|
1111
|
-
"""
|
|
1112
|
-
|
|
1113
|
-
|
|
1114
|
-
|
|
1115
|
-
|
|
1116
|
-
|
|
1117
|
-
|
|
1118
|
-
|
|
1119
|
-
|
|
1120
|
-
|
|
1121
|
-
|
|
1122
|
-
|
|
1123
|
-
|
|
1124
|
-
|
|
1125
|
-
|
|
1126
|
-
|
|
1127
|
-
|
|
1128
|
-
|
|
1129
|
-
|
|
1130
|
-
|
|
1131
|
-
|
|
1132
|
-
|
|
1133
|
-
|
|
1134
|
-
unlink_memories(
|
|
1135
|
-
memory_id_1="480c1f76-bcdf-4491-8781-24510db992e3",
|
|
1136
|
-
memory_id_2="300d9716-a3a6-44d3-b0f4-b28002a65da8"
|
|
1137
|
-
)
|
|
746
|
+
async def get_stats() -> str:
|
|
747
|
+
"""Get comprehensive statistics about the memory system.
|
|
748
|
+
|
|
749
|
+
**Use this tool to:**
|
|
750
|
+
- Check how many memories you have stored
|
|
751
|
+
- See total link count and link density
|
|
752
|
+
- View top tags and memory distribution
|
|
753
|
+
- Understand your knowledge graph structure
|
|
754
|
+
|
|
755
|
+
**Why this matters:**
|
|
756
|
+
- Provides context about memory coverage
|
|
757
|
+
- Shows if the system is actively used
|
|
758
|
+
- Reveals patterns in what you store
|
|
759
|
+
- Helps identify gaps or over-representation
|
|
760
|
+
|
|
761
|
+
Returns formatted statistics including:
|
|
762
|
+
- Total memories count
|
|
763
|
+
- Total links count
|
|
764
|
+
- Average links per memory
|
|
765
|
+
- Top tags by frequency
|
|
766
|
+
- Memory categories/tags distribution
|
|
767
|
+
|
|
768
|
+
**Example:**
|
|
769
|
+
get_stats()
|
|
1138
770
|
"""
|
|
1139
|
-
# Validate parameters with detailed error messages
|
|
1140
|
-
if memory_id_1 is None:
|
|
1141
|
-
raise ToolError(
|
|
1142
|
-
"The 'memory_id_1' parameter is required but was not provided.\n"
|
|
1143
|
-
"Get memory IDs from search_memories() or get_memories() results.\n"
|
|
1144
|
-
'Example: unlink_memories(memory_id_1="480c1f76-...", memory_id_2="300d9716-...")'
|
|
1145
|
-
)
|
|
1146
|
-
|
|
1147
|
-
if not isinstance(memory_id_1, str):
|
|
1148
|
-
raise ToolError(
|
|
1149
|
-
f"The 'memory_id_1' parameter must be a string, but got {type(memory_id_1).__name__}.\n"
|
|
1150
|
-
f"Received: {repr(memory_id_1)}\n"
|
|
1151
|
-
'Example: unlink_memories(memory_id_1="480c1f76-...", memory_id_2="300d9716-...")'
|
|
1152
|
-
)
|
|
1153
|
-
|
|
1154
|
-
memory_id_1_str = memory_id_1.strip()
|
|
1155
|
-
if not memory_id_1_str:
|
|
1156
|
-
raise ToolError(
|
|
1157
|
-
"The 'memory_id_1' parameter cannot be empty or whitespace-only.\n"
|
|
1158
|
-
"Get memory IDs from search_memories() or get_memories() results.\n"
|
|
1159
|
-
'Example: unlink_memories(memory_id_1="480c1f76-bcdf-4491-8781-24510db992e3", memory_id_2="300d9716-...")'
|
|
1160
|
-
)
|
|
1161
|
-
|
|
1162
|
-
if memory_id_2 is None:
|
|
1163
|
-
raise ToolError(
|
|
1164
|
-
"The 'memory_id_2' parameter is required but was not provided.\n"
|
|
1165
|
-
"Get memory IDs from search_memories() or get_memories() results.\n"
|
|
1166
|
-
'Example: unlink_memories(memory_id_1="480c1f76-...", memory_id_2="300d9716-...")'
|
|
1167
|
-
)
|
|
1168
|
-
|
|
1169
|
-
if not isinstance(memory_id_2, str):
|
|
1170
|
-
raise ToolError(
|
|
1171
|
-
f"The 'memory_id_2' parameter must be a string, but got {type(memory_id_2).__name__}.\n"
|
|
1172
|
-
f"Received: {repr(memory_id_2)}\n"
|
|
1173
|
-
'Example: unlink_memories(memory_id_1="480c1f76-...", memory_id_2="300d9716-...")'
|
|
1174
|
-
)
|
|
1175
|
-
|
|
1176
|
-
memory_id_2_str = memory_id_2.strip()
|
|
1177
|
-
if not memory_id_2_str:
|
|
1178
|
-
raise ToolError(
|
|
1179
|
-
"The 'memory_id_2' parameter cannot be empty or whitespace-only.\n"
|
|
1180
|
-
"Get memory IDs from search_memories() or get_memories() results.\n"
|
|
1181
|
-
'Example: unlink_memories(memory_id_1="480c1f76-...", memory_id_2="300d9716-a3a6-44d3-b0f4-b28002a65da8")'
|
|
1182
|
-
)
|
|
1183
|
-
|
|
1184
|
-
# Ensure the IDs are different
|
|
1185
|
-
if memory_id_1_str == memory_id_2_str:
|
|
1186
|
-
raise ToolError(
|
|
1187
|
-
"memory_id_1 and memory_id_2 must be different.\n"
|
|
1188
|
-
"You cannot unlink a memory from itself.\n"
|
|
1189
|
-
'Example: unlink_memories(memory_id_1="480c1f76-...", memory_id_2="300d9716-...")'
|
|
1190
|
-
)
|
|
1191
|
-
|
|
1192
771
|
try:
|
|
1193
|
-
logger.info(
|
|
1194
|
-
f"unlink_memories called - memory_id_1: {memory_id_1_str}, memory_id_2: {memory_id_2_str}"
|
|
1195
|
-
)
|
|
772
|
+
logger.info("get_stats called - retrieving memory system statistics")
|
|
1196
773
|
client = await _get_api_client()
|
|
1197
|
-
result = await client.unlink_memories(memory_id_1_str, memory_id_2_str)
|
|
1198
|
-
return result.get("message", "Memories unlinked")
|
|
1199
|
-
except httpx.HTTPStatusError as e:
|
|
1200
|
-
error_detail = e.response.text if e.response else "Unknown error"
|
|
1201
|
-
logger.error(f"API error: {e.response.status_code} - {error_detail}")
|
|
1202
|
-
if e.response.status_code == 401:
|
|
1203
|
-
raise ToolError(
|
|
1204
|
-
"Authentication failed. Please login using the 'login' tool or configure your JWT token in the MCP client headers."
|
|
1205
|
-
)
|
|
1206
|
-
elif e.response.status_code == 404:
|
|
1207
|
-
raise ToolError(
|
|
1208
|
-
f"One or both memories not found.\nVerify the memory IDs are correct by searching for them first."
|
|
1209
|
-
)
|
|
1210
|
-
raise ToolError(
|
|
1211
|
-
f"Failed to unlink memories: HTTP {e.response.status_code} - {error_detail}"
|
|
1212
|
-
)
|
|
1213
|
-
except ToolError:
|
|
1214
|
-
raise
|
|
1215
|
-
except Exception as e:
|
|
1216
|
-
logger.error(f"Unexpected error in unlink_memories: {e}", exc_info=True)
|
|
1217
|
-
raise ToolError(f"Error unlinking memories: {str(e)}")
|
|
1218
|
-
|
|
1219
|
-
|
|
1220
|
-
@mcp.tool()
|
|
1221
|
-
async def get_stats(_placeholder: Optional[bool] = None) -> str:
|
|
1222
|
-
"""Get memory system statistics including total memories, links, and top tags. Use this when user asks 'how much do you remember?' or wants an overview of their memory system.
|
|
1223
|
-
|
|
1224
|
-
Parameters:
|
|
1225
|
-
_placeholder (bool, optional): Placeholder parameter for OpenCode compatibility. This parameter is ignored and can be omitted or set to any value. The function takes no actual parameters.
|
|
1226
|
-
- This is a workaround for MCP clients that incorrectly require a parameter for parameterless tools
|
|
1227
|
-
- Can be safely omitted or set to None/True/False
|
|
1228
|
-
- Example: get_stats() or get_stats(_placeholder=True)
|
|
1229
|
-
|
|
1230
|
-
Returns:
|
|
1231
|
-
str: Formatted statistics including total memories, links, and top tags.
|
|
1232
|
-
|
|
1233
|
-
Examples:
|
|
1234
|
-
# Get statistics (preferred - no parameters needed)
|
|
1235
|
-
get_stats()
|
|
1236
|
-
|
|
1237
|
-
# Get statistics (OpenCode workaround - parameter is ignored)
|
|
1238
|
-
get_stats(_placeholder=True)
|
|
1239
|
-
"""
|
|
1240
|
-
# _placeholder parameter is ignored - this is a workaround for OpenCode compatibility
|
|
1241
|
-
# The function actually takes no parameters, but some MCP clients incorrectly require one
|
|
1242
|
-
try:
|
|
1243
|
-
logger.info("get_stats called")
|
|
1244
|
-
logger.debug(f"get_stats called with _placeholder={_placeholder} (ignored)")
|
|
1245
|
-
client = await _get_api_client()
|
|
1246
|
-
logger.debug(f"API client initialized with base_url: {client.base_url}")
|
|
1247
774
|
result = await client.get_stats()
|
|
1248
|
-
logger.debug(
|
|
1249
|
-
f"get_stats result received: {list(result.keys()) if isinstance(result, dict) else 'N/A'}"
|
|
1250
|
-
)
|
|
1251
|
-
top_tags = ", ".join([f"{tag}({count})" for tag, count in result.get("top_tags", [])[:10]])
|
|
1252
|
-
return f"""Memory System Statistics:
|
|
1253
|
-
Total Memories: {result.get("total_memories", 0)}
|
|
1254
|
-
Total Links: {result.get("total_links", 0)}
|
|
1255
|
-
Average Links per Memory: {result.get("avg_links_per_memory", 0):.2f}
|
|
1256
|
-
Top Tags: {top_tags}"""
|
|
1257
|
-
except httpx.HTTPStatusError as e:
|
|
1258
|
-
error_detail = e.response.text if e.response else "Unknown error"
|
|
1259
|
-
logger.error(f"API error: {e.response.status_code} - {error_detail}")
|
|
1260
|
-
if e.response.status_code == 401:
|
|
1261
|
-
raise ToolError(
|
|
1262
|
-
"Authentication failed. Please login using the 'login' tool or configure your JWT token in the MCP client headers."
|
|
1263
|
-
)
|
|
1264
|
-
raise ToolError(f"Failed to get stats: HTTP {e.response.status_code} - {error_detail}")
|
|
1265
|
-
except ToolError:
|
|
1266
|
-
raise
|
|
1267
|
-
except Exception as e:
|
|
1268
|
-
logger.error(f"Unexpected error in get_stats: {e}", exc_info=True)
|
|
1269
|
-
raise ToolError(f"Error getting stats: {str(e)}")
|
|
1270
|
-
|
|
1271
|
-
|
|
1272
|
-
@mcp.tool()
|
|
1273
|
-
async def find_path(from_id: str, to_id: str) -> str:
|
|
1274
|
-
"""Find shortest path between two memories in the memory graph. Use this to explain connections between seemingly unrelated memories.
|
|
1275
|
-
|
|
1276
|
-
Parameters:
|
|
1277
|
-
from_id (str, REQUIRED): Source memory ID to start the path from.
|
|
1278
|
-
- Example: "480c1f76-bcdf-4491-8781-24510db992e3"
|
|
1279
|
-
- Get memory IDs from search_memories() or get_memories() results
|
|
1280
|
-
|
|
1281
|
-
to_id (str, REQUIRED): Target memory ID to find path to.
|
|
1282
|
-
- Example: "300d9716-a3a6-44d3-b0f4-b28002a65da8"
|
|
1283
|
-
- Get memory IDs from search_memories() or get_memories() results
|
|
1284
|
-
|
|
1285
|
-
Returns:
|
|
1286
|
-
str: The shortest path between the two memories, or a message if no path exists.
|
|
1287
|
-
|
|
1288
|
-
Common Errors and Solutions:
|
|
1289
|
-
- Error: "from_id cannot be empty"
|
|
1290
|
-
Solution: Provide a valid memory ID. Example: find_path(from_id="480c1f76-...", to_id="300d9716-...")
|
|
1291
|
-
|
|
1292
|
-
- Error: "to_id cannot be empty"
|
|
1293
|
-
Solution: Provide a valid memory ID. Example: find_path(from_id="480c1f76-...", to_id="300d9716-...")
|
|
1294
|
-
|
|
1295
|
-
Examples:
|
|
1296
|
-
# Find path between two memories
|
|
1297
|
-
find_path(
|
|
1298
|
-
from_id="480c1f76-bcdf-4491-8781-24510db992e3",
|
|
1299
|
-
to_id="300d9716-a3a6-44d3-b0f4-b28002a65da8"
|
|
1300
|
-
)
|
|
1301
|
-
"""
|
|
1302
|
-
# Validate parameters with detailed error messages
|
|
1303
|
-
if from_id is None:
|
|
1304
|
-
raise ToolError(
|
|
1305
|
-
"The 'from_id' parameter is required but was not provided.\n"
|
|
1306
|
-
"Get memory IDs from search_memories() or get_memories() results.\n"
|
|
1307
|
-
'Example: find_path(from_id="480c1f76-...", to_id="300d9716-...")'
|
|
1308
|
-
)
|
|
1309
|
-
|
|
1310
|
-
if not isinstance(from_id, str):
|
|
1311
|
-
raise ToolError(
|
|
1312
|
-
f"The 'from_id' parameter must be a string, but got {type(from_id).__name__}.\n"
|
|
1313
|
-
f"Received: {repr(from_id)}\n"
|
|
1314
|
-
'Example: find_path(from_id="480c1f76-...", to_id="300d9716-...")'
|
|
1315
|
-
)
|
|
1316
|
-
|
|
1317
|
-
from_id_str = from_id.strip()
|
|
1318
|
-
if not from_id_str:
|
|
1319
|
-
raise ToolError(
|
|
1320
|
-
"The 'from_id' parameter cannot be empty or whitespace-only.\n"
|
|
1321
|
-
"Get memory IDs from search_memories() or get_memories() results.\n"
|
|
1322
|
-
'Example: find_path(from_id="480c1f76-bcdf-4491-8781-24510db992e3", to_id="300d9716-...")'
|
|
1323
|
-
)
|
|
1324
|
-
|
|
1325
|
-
if to_id is None:
|
|
1326
|
-
raise ToolError(
|
|
1327
|
-
"The 'to_id' parameter is required but was not provided.\n"
|
|
1328
|
-
"Get memory IDs from search_memories() or get_memories() results.\n"
|
|
1329
|
-
'Example: find_path(from_id="480c1f76-...", to_id="300d9716-...")'
|
|
1330
|
-
)
|
|
1331
|
-
|
|
1332
|
-
if not isinstance(to_id, str):
|
|
1333
|
-
raise ToolError(
|
|
1334
|
-
f"The 'to_id' parameter must be a string, but got {type(to_id).__name__}.\n"
|
|
1335
|
-
f"Received: {repr(to_id)}\n"
|
|
1336
|
-
'Example: find_path(from_id="480c1f76-...", to_id="300d9716-...")'
|
|
1337
|
-
)
|
|
1338
775
|
|
|
1339
|
-
|
|
1340
|
-
|
|
1341
|
-
|
|
1342
|
-
|
|
1343
|
-
|
|
1344
|
-
|
|
1345
|
-
|
|
1346
|
-
|
|
1347
|
-
|
|
1348
|
-
|
|
1349
|
-
|
|
1350
|
-
|
|
1351
|
-
|
|
1352
|
-
|
|
1353
|
-
|
|
1354
|
-
|
|
1355
|
-
|
|
1356
|
-
|
|
1357
|
-
|
|
1358
|
-
|
|
1359
|
-
|
|
1360
|
-
|
|
1361
|
-
|
|
1362
|
-
|
|
1363
|
-
|
|
1364
|
-
|
|
1365
|
-
raise ToolError(
|
|
1366
|
-
f"One or both memories not found.\nVerify the memory IDs are correct by searching for them first."
|
|
1367
|
-
)
|
|
1368
|
-
raise ToolError(f"Failed to find path: HTTP {e.response.status_code} - {error_detail}")
|
|
1369
|
-
except ToolError:
|
|
1370
|
-
raise
|
|
1371
|
-
except Exception as e:
|
|
1372
|
-
logger.error(f"Unexpected error in find_path: {e}", exc_info=True)
|
|
1373
|
-
raise ToolError(f"Error finding path: {str(e)}")
|
|
1374
|
-
|
|
1375
|
-
|
|
1376
|
-
@mcp.tool()
|
|
1377
|
-
async def get_neighborhood(memory_id: str, hops: int = 2) -> str:
|
|
1378
|
-
"""Get all memories within N hops of a given memory. Use this for deep context and understanding relationships around important memories.
|
|
1379
|
-
|
|
1380
|
-
Parameters:
|
|
1381
|
-
memory_id (str, REQUIRED): Center memory ID to get neighborhood around.
|
|
1382
|
-
- Example: "480c1f76-bcdf-4491-8781-24510db992e3"
|
|
1383
|
-
- Get memory IDs from search_memories() or get_memories() results
|
|
1384
|
-
|
|
1385
|
-
hops (int, optional): Number of hops to traverse. Default is 2.
|
|
1386
|
-
- Must be between 1 and 5
|
|
1387
|
-
- 1 hop = direct connections only
|
|
1388
|
-
- 2 hops = direct connections + their connections
|
|
1389
|
-
- Example: 2 (default)
|
|
1390
|
-
- Example: 3
|
|
1391
|
-
|
|
1392
|
-
Returns:
|
|
1393
|
-
str: Formatted list of memories in the neighborhood with their hop distances.
|
|
1394
|
-
|
|
1395
|
-
Common Errors and Solutions:
|
|
1396
|
-
- Error: "memory_id cannot be empty"
|
|
1397
|
-
Solution: Provide a valid memory ID. Example: get_neighborhood(memory_id="480c1f76-...")
|
|
1398
|
-
|
|
1399
|
-
- Error: "hops must be between 1 and 5"
|
|
1400
|
-
Solution: Provide hops between 1 and 5. Example: get_neighborhood(memory_id="...", hops=3)
|
|
1401
|
-
|
|
1402
|
-
Examples:
|
|
1403
|
-
# Get neighborhood with default 2 hops
|
|
1404
|
-
get_neighborhood(memory_id="480c1f76-bcdf-4491-8781-24510db992e3")
|
|
1405
|
-
|
|
1406
|
-
# Get neighborhood with 3 hops
|
|
1407
|
-
get_neighborhood(memory_id="480c1f76-bcdf-4491-8781-24510db992e3", hops=3)
|
|
1408
|
-
|
|
1409
|
-
# Get direct connections only (1 hop)
|
|
1410
|
-
get_neighborhood(memory_id="480c1f76-bcdf-4491-8781-24510db992e3", hops=1)
|
|
1411
|
-
"""
|
|
1412
|
-
# Validate parameters with detailed error messages
|
|
1413
|
-
if memory_id is None:
|
|
1414
|
-
raise ToolError(
|
|
1415
|
-
"The 'memory_id' parameter is required but was not provided.\n"
|
|
1416
|
-
"Get memory IDs from search_memories() or get_memories() results.\n"
|
|
1417
|
-
'Example: get_neighborhood(memory_id="480c1f76-bcdf-4491-8781-24510db992e3")'
|
|
1418
|
-
)
|
|
1419
|
-
|
|
1420
|
-
if not isinstance(memory_id, str):
|
|
1421
|
-
raise ToolError(
|
|
1422
|
-
f"The 'memory_id' parameter must be a string, but got {type(memory_id).__name__}.\n"
|
|
1423
|
-
f"Received: {repr(memory_id)}\n"
|
|
1424
|
-
'Example: get_neighborhood(memory_id="480c1f76-bcdf-4491-8781-24510db992e3")'
|
|
1425
|
-
)
|
|
1426
|
-
|
|
1427
|
-
memory_id_str = memory_id.strip()
|
|
1428
|
-
if not memory_id_str:
|
|
1429
|
-
raise ToolError(
|
|
1430
|
-
"The 'memory_id' parameter cannot be empty or whitespace-only.\n"
|
|
1431
|
-
"Get memory IDs from search_memories() or get_memories() results.\n"
|
|
1432
|
-
'Example: get_neighborhood(memory_id="480c1f76-bcdf-4491-8781-24510db992e3")'
|
|
1433
|
-
)
|
|
1434
|
-
|
|
1435
|
-
if not isinstance(hops, int):
|
|
1436
|
-
raise ToolError(
|
|
1437
|
-
f"The 'hops' parameter must be an integer, but got {type(hops).__name__}.\n"
|
|
1438
|
-
f"Received: {repr(hops)}\n"
|
|
1439
|
-
'Example: get_neighborhood(memory_id="...", hops=2)'
|
|
1440
|
-
)
|
|
1441
|
-
|
|
1442
|
-
if not (1 <= hops <= 5):
|
|
1443
|
-
raise ToolError(
|
|
1444
|
-
f"The 'hops' parameter must be between 1 and 5, but got {hops}.\n"
|
|
1445
|
-
'Example: get_neighborhood(memory_id="...", hops=2)\n'
|
|
1446
|
-
'Example: get_neighborhood(memory_id="...", hops=3)'
|
|
1447
|
-
)
|
|
776
|
+
# Extract statistics
|
|
777
|
+
total_memories = result.get("total_memories", 0)
|
|
778
|
+
total_links = result.get("total_links", 0)
|
|
779
|
+
avg_links = result.get("avg_links_per_memory", 0)
|
|
780
|
+
top_tags = result.get("top_tags", [])
|
|
781
|
+
|
|
782
|
+
# Format response
|
|
783
|
+
lines = [
|
|
784
|
+
"📊 Memory System Statistics",
|
|
785
|
+
"",
|
|
786
|
+
f"Total Memories: {total_memories}",
|
|
787
|
+
f"Total Links: {total_links}",
|
|
788
|
+
f"Average Links per Memory: {avg_links:.2f}",
|
|
789
|
+
]
|
|
790
|
+
|
|
791
|
+
# Add top tags if available
|
|
792
|
+
if top_tags:
|
|
793
|
+
lines.append("")
|
|
794
|
+
lines.append("Top Tags:")
|
|
795
|
+
for tag_data in top_tags[:10]: # Show top 10
|
|
796
|
+
if isinstance(tag_data, dict):
|
|
797
|
+
tag_name = tag_data.get("tag", "unknown")
|
|
798
|
+
count = tag_data.get("count", 0)
|
|
799
|
+
lines.append(f" - {tag_name}: {count} memories")
|
|
800
|
+
else:
|
|
801
|
+
lines.append(f" - {tag_data}")
|
|
1448
802
|
|
|
1449
|
-
|
|
1450
|
-
logger.info(f"get_neighborhood called - memory_id: {memory_id_str}, hops: {hops}")
|
|
1451
|
-
client = await _get_api_client()
|
|
1452
|
-
result = await client.get_neighborhood(memory_id_str, hops)
|
|
1453
|
-
neighborhood_text = f"Neighborhood (hops={result.get('hops', 2)}, total={result.get('total_in_neighborhood', 0)}):\n"
|
|
1454
|
-
for mem in result.get("neighborhood", []):
|
|
1455
|
-
hop_dist = mem.get("hop_distance", 0)
|
|
1456
|
-
is_center = " (center)" if mem.get("is_center") else ""
|
|
1457
|
-
neighborhood_text += f" [{hop_dist}]{is_center} {mem.get('id', 'unknown')}: {mem.get('content', '')[:100]}\n"
|
|
1458
|
-
return neighborhood_text
|
|
803
|
+
return "\n".join(lines)
|
|
1459
804
|
except httpx.HTTPStatusError as e:
|
|
1460
805
|
error_detail = e.response.text if e.response else "Unknown error"
|
|
1461
806
|
logger.error(f"API error: {e.response.status_code} - {error_detail}")
|
|
@@ -1463,18 +808,12 @@ async def get_neighborhood(memory_id: str, hops: int = 2) -> str:
|
|
|
1463
808
|
raise ToolError(
|
|
1464
809
|
"Authentication failed. Please login using the 'login' tool or configure your JWT token in the MCP client headers."
|
|
1465
810
|
)
|
|
1466
|
-
|
|
1467
|
-
raise ToolError(
|
|
1468
|
-
f"Memory not found: {memory_id_str}\nVerify the memory_id is correct by searching for it first."
|
|
1469
|
-
)
|
|
1470
|
-
raise ToolError(
|
|
1471
|
-
f"Failed to get neighborhood: HTTP {e.response.status_code} - {error_detail}"
|
|
1472
|
-
)
|
|
811
|
+
raise ToolError(f"Failed to get statistics: HTTP {e.response.status_code} - {error_detail}")
|
|
1473
812
|
except ToolError:
|
|
1474
813
|
raise
|
|
1475
814
|
except Exception as e:
|
|
1476
|
-
logger.error(f"Unexpected error in
|
|
1477
|
-
raise ToolError(f"Error getting
|
|
815
|
+
logger.error(f"Unexpected error in get_stats: {e}", exc_info=True)
|
|
816
|
+
raise ToolError(f"Error getting statistics: {str(e)}")
|
|
1478
817
|
|
|
1479
818
|
|
|
1480
819
|
# ============================================================================
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: mem-brain-mcp
|
|
3
|
-
Version: 1.0
|
|
3
|
+
Version: 1.1.0
|
|
4
4
|
Summary: MCP Server for Mem-Brain API - Exposes memory operations as MCP tools
|
|
5
5
|
Keywords: ai,claude,cursor,llm,mcp,memory,model-context-protocol
|
|
6
6
|
Classifier: Development Status :: 4 - Beta
|
|
@@ -194,15 +194,40 @@ For detailed instructions, see [aws/DEPLOYMENT.md](./aws/DEPLOYMENT.md).
|
|
|
194
194
|
pytest
|
|
195
195
|
```
|
|
196
196
|
|
|
197
|
-
### Build and Publish
|
|
197
|
+
### Build and Publish to PyPI
|
|
198
|
+
|
|
199
|
+
**Prerequisites:** Ensure you have a PyPI account and your credentials are configured in `~/.pypirc`.
|
|
200
|
+
|
|
201
|
+
**Step-by-step deployment:**
|
|
202
|
+
|
|
198
203
|
```bash
|
|
199
|
-
#
|
|
200
|
-
|
|
204
|
+
# 1. Activate the virtual environment
|
|
205
|
+
cd mem-brain-mcp
|
|
206
|
+
source .venv/bin/activate
|
|
207
|
+
|
|
208
|
+
# 2. Update version in pyproject.toml
|
|
209
|
+
# Edit pyproject.toml and increment the version number (e.g., 1.0.8 -> 1.0.9)
|
|
210
|
+
|
|
211
|
+
# 3. Clean old builds
|
|
212
|
+
rm -rf dist/*
|
|
213
|
+
|
|
214
|
+
# 4. Build the package
|
|
215
|
+
python -m build
|
|
201
216
|
|
|
202
|
-
# Upload to PyPI
|
|
217
|
+
# 5. Upload to PyPI using twine
|
|
203
218
|
twine upload dist/*
|
|
204
219
|
```
|
|
205
220
|
|
|
221
|
+
**Quick deployment (one-liner after version bump):**
|
|
222
|
+
```bash
|
|
223
|
+
source .venv/bin/activate && rm -rf dist/* && python -m build && twine upload dist/*
|
|
224
|
+
```
|
|
225
|
+
|
|
226
|
+
**Verify deployment:**
|
|
227
|
+
```bash
|
|
228
|
+
pip3 index versions mem-brain-mcp
|
|
229
|
+
```
|
|
230
|
+
|
|
206
231
|
## License
|
|
207
232
|
|
|
208
233
|
Same as Mem-Brain API project.
|
|
@@ -2,8 +2,8 @@ mem_brain_mcp/__init__.py,sha256=4gTKntJjg7JiV6dGhDNy1yFh3TW2tCdME_GR-pnSXwk,89
|
|
|
2
2
|
mem_brain_mcp/__main__.py,sha256=H_mwoKm1FBmu4KzAcQcq-TXZqeNvlrAekAxB1s4F4hA,712
|
|
3
3
|
mem_brain_mcp/client.py,sha256=jpf0fCupoRI8xZnPF4OntAQYxDAiVetIfL3S5GUOwUo,8009
|
|
4
4
|
mem_brain_mcp/config.py,sha256=xx2lBkCIeT85t0HxtORwZHSU3hZT_EdsThpfjwPJhbQ,1261
|
|
5
|
-
mem_brain_mcp/server.py,sha256=
|
|
6
|
-
mem_brain_mcp-1.0.
|
|
7
|
-
mem_brain_mcp-1.0.
|
|
8
|
-
mem_brain_mcp-1.0.
|
|
9
|
-
mem_brain_mcp-1.0.
|
|
5
|
+
mem_brain_mcp/server.py,sha256=MZHMahLvpmq9h7j8ipsOfF2j7X_d965vBk_QQBk6Cxw,42272
|
|
6
|
+
mem_brain_mcp-1.1.0.dist-info/METADATA,sha256=hahjVHRKHO1g18XJlP5Whmu6x3L_5iK9sjzJ21kB27A,5846
|
|
7
|
+
mem_brain_mcp-1.1.0.dist-info/WHEEL,sha256=WLgqFyCfm_KASv4WHyYy0P3pM_m7J5L9k2skdKLirC8,87
|
|
8
|
+
mem_brain_mcp-1.1.0.dist-info/entry_points.txt,sha256=NH6QYQ-Sd8eJn5crpe_DL1PvGeUlL3y65968xPhmwG8,62
|
|
9
|
+
mem_brain_mcp-1.1.0.dist-info/RECORD,,
|
|
File without changes
|
|
File without changes
|