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 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,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
- | `update_memory(memory_id, content=..., tags=...)` | Information evolves or changes |
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
- **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
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" | UPDATE existing memory (permanent change) |
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 """# A-Mem Workflow Guide
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 - matches both memory content & link descriptions)
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
- ## Avoiding Duplicates
207
+ ## System Handles Updates Automatically
343
208
 
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
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" | UPDATE existing memory (permanent change) |
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(include_dynamic_context: bool = True) -> str:
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
- if include_dynamic_context:
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 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
- )
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
- 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
- )
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
- 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
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
- 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
- )
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 get_neighborhood: {e}", exc_info=True)
1477
- raise ToolError(f"Error getting neighborhood: {str(e)}")
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.8
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
- # Build the wheel
200
- python3 -m build
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=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=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,,