mem-brain-mcp 1.0.8__py3-none-any.whl → 1.0.9__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 +15 -751
- {mem_brain_mcp-1.0.8.dist-info → mem_brain_mcp-1.0.9.dist-info}/METADATA +1 -1
- {mem_brain_mcp-1.0.8.dist-info → mem_brain_mcp-1.0.9.dist-info}/RECORD +5 -5
- {mem_brain_mcp-1.0.8.dist-info → mem_brain_mcp-1.0.9.dist-info}/WHEEL +0 -0
- {mem_brain_mcp-1.0.8.dist-info → mem_brain_mcp-1.0.9.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,16 @@ 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
|
-
| `update_memory(memory_id, content=..., tags=...)` | Information evolves or changes |
|
|
57
51
|
| `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
52
|
|
|
68
53
|
---
|
|
69
54
|
|
|
@@ -78,10 +63,10 @@ AGENT_INSTRUCTIONS = """You are an intelligent assistant with a persistent, evol
|
|
|
78
63
|
- Type: `preference`, `constraint`, `goal`, `fact`, `event`
|
|
79
64
|
- Priority: `important`, `routine`, `temporary`
|
|
80
65
|
|
|
81
|
-
**
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
66
|
+
**System handles updates automatically:**
|
|
67
|
+
- When user changes preferences, just add the new memory
|
|
68
|
+
- The system automatically links related memories and manages updates
|
|
69
|
+
- No manual linking or unlinking needed
|
|
85
70
|
|
|
86
71
|
---
|
|
87
72
|
|
|
@@ -90,20 +75,11 @@ AGENT_INSTRUCTIONS = """You are an intelligent assistant with a persistent, evol
|
|
|
90
75
|
| Signal | Action |
|
|
91
76
|
|--------|--------|
|
|
92
77
|
| "I'm trying X", "exploring Y" | ADD new memory (temporary exploration) |
|
|
93
|
-
| "I no longer like X", "I switched to Y" |
|
|
78
|
+
| "I no longer like X", "I switched to Y" | ADD new memory (the system handles updates automatically) |
|
|
94
79
|
| Contradictory with equal weight | ADD with temporal context ("as of 2025") |
|
|
95
80
|
|
|
96
81
|
---
|
|
97
82
|
|
|
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
83
|
## ✅ BEST PRACTICES
|
|
108
84
|
|
|
109
85
|
| DO | DON'T |
|
|
@@ -111,7 +87,6 @@ AGENT_INSTRUCTIONS = """You are an intelligent assistant with a persistent, evol
|
|
|
111
87
|
| Search before answering personal Q's | Guess without searching |
|
|
112
88
|
| Check `related_memories` field | Ignore graph connections |
|
|
113
89
|
| Store explicit facts | Store vague conversation |
|
|
114
|
-
| Update when info changes | Create duplicates |
|
|
115
90
|
| Synthesize across memories | Just list facts |
|
|
116
91
|
|
|
117
92
|
**Remember:** You're not a database. Connect the dots to provide thoughtful, personalized responses."""
|
|
@@ -170,68 +145,6 @@ mcp = FastMCP("Mem-Brain MCP")
|
|
|
170
145
|
api_client = APIClient()
|
|
171
146
|
|
|
172
147
|
|
|
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
148
|
# ============================================================================
|
|
236
149
|
# RESOURCES (Documentation that LLMs can read)
|
|
237
150
|
# ============================================================================
|
|
@@ -240,7 +153,7 @@ async def _get_dynamic_context() -> str:
|
|
|
240
153
|
@mcp.resource("mem-brain://docs/workflow-guide")
|
|
241
154
|
def workflow_guide() -> str:
|
|
242
155
|
"""Complete guide to the memory workflow: search strategies, pattern recognition, storage guidelines, and best practices."""
|
|
243
|
-
return """#
|
|
156
|
+
return """# Memory Workflow Guide
|
|
244
157
|
|
|
245
158
|
## 🎯 CORE DIRECTIVE
|
|
246
159
|
**Synthesize**, don't just retrieve. Connect user's request to their past preferences, habits, and constraints.
|
|
@@ -250,7 +163,7 @@ def workflow_guide() -> str:
|
|
|
250
163
|
**1. SEARCH FIRST & SMART** — Before answering personal questions, call `search_memories`.
|
|
251
164
|
- **Formulate specific, natural language queries**, NOT simple keywords.
|
|
252
165
|
- ❌ `query="maga"` (Weak)
|
|
253
|
-
- ✅ `query="Who is Maga and what is his relationship to me?"` (Strong
|
|
166
|
+
- ✅ `query="Who is Maga and what is his relationship to me?"` (Strong)
|
|
254
167
|
- Check `related_memories` field — these are auto-expanded graph neighbors.
|
|
255
168
|
- Synthesize: If "coffee" result links to "acid reflux", suggest cold brew.
|
|
256
169
|
|
|
@@ -261,8 +174,6 @@ def workflow_guide() -> str:
|
|
|
261
174
|
**3. PASSIVE STORAGE** — When user reveals preferences, store the **FACT** (not conversation).
|
|
262
175
|
- User: "I think I wanna try that sushi spot" → Store: "User interested in new sushi restaurant"
|
|
263
176
|
|
|
264
|
-
**4. KEEP IT CURRENT** — If user contradicts a past memory, use `update_memory`.
|
|
265
|
-
|
|
266
177
|
## ✅ BEST PRACTICES
|
|
267
178
|
|
|
268
179
|
| DO | DON'T |
|
|
@@ -270,59 +181,12 @@ def workflow_guide() -> str:
|
|
|
270
181
|
| Search before answering personal Q's | Guess without searching |
|
|
271
182
|
| Check `related_memories` field | Ignore graph connections |
|
|
272
183
|
| Store explicit facts | Store vague conversation |
|
|
273
|
-
| Update when info changes | Create duplicates |
|
|
274
184
|
| Synthesize across memories | Just list facts |
|
|
275
185
|
|
|
276
186
|
**Remember:** You're not a database. Connect the dots to provide thoughtful, personalized responses.
|
|
277
187
|
"""
|
|
278
188
|
|
|
279
189
|
|
|
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
190
|
@mcp.resource("mem-brain://docs/storage-guidelines")
|
|
327
191
|
def storage_guidelines() -> str:
|
|
328
192
|
"""Best practices for storing facts, tagging patterns, and avoiding duplicates."""
|
|
@@ -339,64 +203,21 @@ def storage_guidelines() -> str:
|
|
|
339
203
|
**Types**: `preference`, `constraint`, `goal`, `fact`, `event`
|
|
340
204
|
**Priority**: `important`, `routine`, `temporary`
|
|
341
205
|
|
|
342
|
-
##
|
|
206
|
+
## System Handles Updates Automatically
|
|
343
207
|
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
208
|
+
- When user changes preferences, just add the new memory
|
|
209
|
+
- The system automatically links related memories and manages updates
|
|
210
|
+
- No manual linking or unlinking needed
|
|
347
211
|
|
|
348
212
|
## Changing Preferences
|
|
349
213
|
|
|
350
214
|
| Signal | Action |
|
|
351
215
|
|--------|--------|
|
|
352
216
|
| "I'm trying X", "exploring Y" | ADD new memory (temporary exploration) |
|
|
353
|
-
| "I no longer like X", "I switched to Y" |
|
|
217
|
+
| "I no longer like X", "I switched to Y" | ADD new memory (the system handles updates automatically) |
|
|
354
218
|
| 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
|
-
"""
|
|
363
|
-
|
|
364
|
-
|
|
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
219
|
"""
|
|
380
220
|
|
|
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
221
|
|
|
401
222
|
# ============================================================================
|
|
402
223
|
# TOOLS (Operations)
|
|
@@ -404,14 +225,9 @@ async def refresh_context() -> PromptMessage:
|
|
|
404
225
|
|
|
405
226
|
|
|
406
227
|
@mcp.tool()
|
|
407
|
-
async def get_agent_instructions(
|
|
228
|
+
async def get_agent_instructions() -> str:
|
|
408
229
|
"""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
|
|
230
|
+
return AGENT_INSTRUCTIONS
|
|
415
231
|
|
|
416
232
|
|
|
417
233
|
@mcp.tool()
|
|
@@ -792,187 +608,6 @@ async def get_memories(memory_ids: List[str]) -> str:
|
|
|
792
608
|
raise ToolError(f"Error getting memories: {str(e)}")
|
|
793
609
|
|
|
794
610
|
|
|
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
611
|
@mcp.tool()
|
|
977
612
|
async def delete_memories(
|
|
978
613
|
memory_id: Optional[str] = None, tags: Optional[str] = None, category: Optional[str] = None
|
|
@@ -1106,377 +741,6 @@ async def delete_memories(
|
|
|
1106
741
|
raise ToolError(f"Error deleting memories: {str(e)}")
|
|
1107
742
|
|
|
1108
743
|
|
|
1109
|
-
@mcp.tool()
|
|
1110
|
-
async def unlink_memories(memory_id_1: str, memory_id_2: str) -> str:
|
|
1111
|
-
"""Remove link between two memories when the connection is no longer relevant or accurate.
|
|
1112
|
-
|
|
1113
|
-
Parameters:
|
|
1114
|
-
memory_id_1 (str, REQUIRED): First memory ID in the link to remove.
|
|
1115
|
-
- Example: "480c1f76-bcdf-4491-8781-24510db992e3"
|
|
1116
|
-
- Get memory IDs from search_memories() or get_memories() results
|
|
1117
|
-
|
|
1118
|
-
memory_id_2 (str, REQUIRED): Second memory ID in the link to remove.
|
|
1119
|
-
- Example: "300d9716-a3a6-44d3-b0f4-b28002a65da8"
|
|
1120
|
-
- Get memory IDs from search_memories() or get_memories() results
|
|
1121
|
-
|
|
1122
|
-
Returns:
|
|
1123
|
-
str: Confirmation message that the memories were unlinked.
|
|
1124
|
-
|
|
1125
|
-
Common Errors and Solutions:
|
|
1126
|
-
- Error: "memory_id_1 cannot be empty"
|
|
1127
|
-
Solution: Provide a valid memory ID. Example: unlink_memories(memory_id_1="480c1f76-...", memory_id_2="300d9716-...")
|
|
1128
|
-
|
|
1129
|
-
- Error: "memory_id_2 cannot be empty"
|
|
1130
|
-
Solution: Provide a valid memory ID. Example: unlink_memories(memory_id_1="480c1f76-...", memory_id_2="300d9716-...")
|
|
1131
|
-
|
|
1132
|
-
Examples:
|
|
1133
|
-
# Unlink two memories
|
|
1134
|
-
unlink_memories(
|
|
1135
|
-
memory_id_1="480c1f76-bcdf-4491-8781-24510db992e3",
|
|
1136
|
-
memory_id_2="300d9716-a3a6-44d3-b0f4-b28002a65da8"
|
|
1137
|
-
)
|
|
1138
|
-
"""
|
|
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
|
-
try:
|
|
1193
|
-
logger.info(
|
|
1194
|
-
f"unlink_memories called - memory_id_1: {memory_id_1_str}, memory_id_2: {memory_id_2_str}"
|
|
1195
|
-
)
|
|
1196
|
-
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
|
-
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
|
-
|
|
1339
|
-
to_id_str = to_id.strip()
|
|
1340
|
-
if not to_id_str:
|
|
1341
|
-
raise ToolError(
|
|
1342
|
-
"The 'to_id' parameter cannot be empty or whitespace-only.\n"
|
|
1343
|
-
"Get memory IDs from search_memories() or get_memories() results.\n"
|
|
1344
|
-
'Example: find_path(from_id="480c1f76-...", to_id="300d9716-a3a6-44d3-b0f4-b28002a65da8")'
|
|
1345
|
-
)
|
|
1346
|
-
|
|
1347
|
-
try:
|
|
1348
|
-
logger.info(f"find_path called - from_id: {from_id_str}, to_id: {to_id_str}")
|
|
1349
|
-
client = await _get_api_client()
|
|
1350
|
-
result = await client.find_path(from_id_str, to_id_str)
|
|
1351
|
-
if result.get("status") == "success":
|
|
1352
|
-
path_text = f"Path found (length: {result.get('length', 0)}):\n"
|
|
1353
|
-
for mem in result.get("memories", []):
|
|
1354
|
-
path_text += f" - {mem.get('id', 'unknown')}: {mem.get('content', '')[:100]}\n"
|
|
1355
|
-
return path_text
|
|
1356
|
-
return result.get("message", "No path found")
|
|
1357
|
-
except httpx.HTTPStatusError as e:
|
|
1358
|
-
error_detail = e.response.text if e.response else "Unknown error"
|
|
1359
|
-
logger.error(f"API error: {e.response.status_code} - {error_detail}")
|
|
1360
|
-
if e.response.status_code == 401:
|
|
1361
|
-
raise ToolError(
|
|
1362
|
-
"Authentication failed. Please login using the 'login' tool or configure your JWT token in the MCP client headers."
|
|
1363
|
-
)
|
|
1364
|
-
elif e.response.status_code == 404:
|
|
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
|
-
)
|
|
1448
|
-
|
|
1449
|
-
try:
|
|
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
|
|
1459
|
-
except httpx.HTTPStatusError as e:
|
|
1460
|
-
error_detail = e.response.text if e.response else "Unknown error"
|
|
1461
|
-
logger.error(f"API error: {e.response.status_code} - {error_detail}")
|
|
1462
|
-
if e.response.status_code == 401:
|
|
1463
|
-
raise ToolError(
|
|
1464
|
-
"Authentication failed. Please login using the 'login' tool or configure your JWT token in the MCP client headers."
|
|
1465
|
-
)
|
|
1466
|
-
elif e.response.status_code == 404:
|
|
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
|
-
)
|
|
1473
|
-
except ToolError:
|
|
1474
|
-
raise
|
|
1475
|
-
except Exception as e:
|
|
1476
|
-
logger.error(f"Unexpected error in get_neighborhood: {e}", exc_info=True)
|
|
1477
|
-
raise ToolError(f"Error getting neighborhood: {str(e)}")
|
|
1478
|
-
|
|
1479
|
-
|
|
1480
744
|
# ============================================================================
|
|
1481
745
|
# HELPER FUNCTIONS
|
|
1482
746
|
# ============================================================================
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: mem-brain-mcp
|
|
3
|
-
Version: 1.0.
|
|
3
|
+
Version: 1.0.9
|
|
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
|
|
@@ -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=PwVh9I6UuuiowPuXPaEIuq87EmsipuytwaPWVq5cqfs,39544
|
|
6
|
+
mem_brain_mcp-1.0.9.dist-info/METADATA,sha256=B0t1Hcu1nBOetQdqS26O3pubrYblkN67h999pfigW98,5228
|
|
7
|
+
mem_brain_mcp-1.0.9.dist-info/WHEEL,sha256=WLgqFyCfm_KASv4WHyYy0P3pM_m7J5L9k2skdKLirC8,87
|
|
8
|
+
mem_brain_mcp-1.0.9.dist-info/entry_points.txt,sha256=NH6QYQ-Sd8eJn5crpe_DL1PvGeUlL3y65968xPhmwG8,62
|
|
9
|
+
mem_brain_mcp-1.0.9.dist-info/RECORD,,
|
|
File without changes
|
|
File without changes
|