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 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 - matches both memory content & link descriptions)
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
- **Avoiding duplicates:**
82
- 1. If you already searched check if memory exists before adding
83
- 2. If similar memory exists `update_memory` instead
84
- 3. If you haven't searched just add it, evolution handles linking
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" | UPDATE existing memory (permanent change) |
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 """# A-Mem Workflow Guide
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 - matches both memory content & link descriptions)
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
- ## Avoiding Duplicates
206
+ ## System Handles Updates Automatically
343
207
 
344
- 1. If you already searched check if memory exists before adding
345
- 2. If similar memory exists `update_memory` instead
346
- 3. If you haven't searched just add it, evolution handles linking
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" | UPDATE existing memory (permanent change) |
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(include_dynamic_context: bool = True) -> str:
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
- if include_dynamic_context:
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.8
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=c57j4MeAnekMkEnDzen8bi3TX6ZjEAWS1bnk4uJ1414,71875
6
- mem_brain_mcp-1.0.8.dist-info/METADATA,sha256=c86gHcn9gWwBes8VMKi3CBa17GITo3DxXE-Okp-Y184,5228
7
- mem_brain_mcp-1.0.8.dist-info/WHEEL,sha256=WLgqFyCfm_KASv4WHyYy0P3pM_m7J5L9k2skdKLirC8,87
8
- mem_brain_mcp-1.0.8.dist-info/entry_points.txt,sha256=NH6QYQ-Sd8eJn5crpe_DL1PvGeUlL3y65968xPhmwG8,62
9
- mem_brain_mcp-1.0.8.dist-info/RECORD,,
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,,