mem-brain-mcp 1.0.6__py3-none-any.whl → 1.0.7__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
@@ -27,6 +27,9 @@ AGENT_INSTRUCTIONS = """You are an intelligent assistant with a persistent, evol
27
27
  - **Formulate specific, natural language queries**, NOT simple keywords.
28
28
  - ❌ `query="maga"` (Weak)
29
29
  - ✅ `query="Who is Maga and what is his relationship to me?"` (Strong - matches both memory content & link descriptions)
30
+ - **Use `keyword_filter` for Scoping**: Deterministically isolate context by project, session, or topic.
31
+ - ✅ `search_memories(query="...", keyword_filter="project-x")` (Matches memories tagged with project-x)
32
+ - ✅ `search_memories(query="...", keyword_filter="session-.*-2026")` (Regex match for 2026 sessions)
30
33
  - Check `related_memories` field — these are auto-expanded graph neighbors.
31
34
  - Synthesize: If "coffee" result links to "acid reflux", suggest cold brew.
32
35
 
@@ -116,20 +119,20 @@ AGENT_INSTRUCTIONS = """You are an intelligent assistant with a persistent, evol
116
119
 
117
120
  def _get_request_token() -> Optional[str]:
118
121
  """Extract JWT token from request headers.
119
-
122
+
120
123
  Returns:
121
124
  JWT token string if found, None otherwise
122
125
  """
123
126
  try:
124
127
  ctx = request_ctx.get()
125
- if hasattr(ctx, 'request') and hasattr(ctx.request, 'headers'):
128
+ if hasattr(ctx, "request") and hasattr(ctx.request, "headers"):
126
129
  headers = ctx.request.headers
127
130
  # Try Authorization Bearer token (primary method)
128
- auth_header = headers.get('authorization', '') or headers.get('Authorization', '')
129
- if auth_header.startswith('Bearer '):
131
+ auth_header = headers.get("authorization", "") or headers.get("Authorization", "")
132
+ if auth_header.startswith("Bearer "):
130
133
  return auth_header[7:]
131
134
  # Fallback to X-API-Key header (for backward compatibility)
132
- api_key = headers.get('x-api-key') or headers.get('X-API-Key')
135
+ api_key = headers.get("x-api-key") or headers.get("X-API-Key")
133
136
  if api_key:
134
137
  return api_key
135
138
  except Exception as e:
@@ -155,7 +158,9 @@ async def _get_api_client() -> APIClient:
155
158
  return api_client # Global instance
156
159
  # No token available
157
160
  logger.error("No authentication token available - neither from headers nor config")
158
- raise ToolError("No authentication token provided. Please login using the login tool or configure your JWT token in your MCP client headers.")
161
+ raise ToolError(
162
+ "No authentication token provided. Please login using the login tool or configure your JWT token in your MCP client headers."
163
+ )
159
164
 
160
165
 
161
166
  # Initialize FastMCP server
@@ -170,14 +175,25 @@ async def _get_dynamic_context() -> str:
170
175
  try:
171
176
  # Get core identity
172
177
  client = await _get_api_client()
173
- identity_response = await client._request("POST", "/memories/search", json={
174
- "query": "user name location job identity", "k": 10
175
- })
178
+ identity_response = await client._request(
179
+ "POST", "/memories/search", json={"query": "user name location job identity", "k": 10}
180
+ )
176
181
 
177
182
  identity_memories = []
178
183
  for mem in identity_response.get("results", []):
179
- tags = mem.get('tags', [])
180
- if any(tag in tags for tag in {'user_info', 'name', 'location', 'job', 'core_identity', 'identity', 'personal'}):
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
+ ):
181
197
  identity_memories.append(mem)
182
198
 
183
199
  identity_section = ""
@@ -187,16 +203,16 @@ async def _get_dynamic_context() -> str:
187
203
  identity_section += f"- {memory['content']}\n"
188
204
 
189
205
  # Get recent context
190
- recent_response = await client._request("POST", "/memories/search", json={
191
- "query": "recent context", "k": 3
192
- })
206
+ recent_response = await client._request(
207
+ "POST", "/memories/search", json={"query": "recent context", "k": 3}
208
+ )
193
209
 
194
210
  recent_section = ""
195
211
  if recent_response.get("results"):
196
212
  recent_section = "## 🕐 Recent Context\n"
197
213
  for memory in recent_response.get("results", [])[:3]:
198
- content = memory['content']
199
- truncated = content[:100] + '...' if len(content) > 100 else content
214
+ content = memory["content"]
215
+ truncated = content[:100] + "..." if len(content) > 100 else content
200
216
  recent_section += f"- {truncated}\n"
201
217
 
202
218
  return f"""### 🧠 YOUR BRAIN (Current Working Context)
@@ -220,6 +236,7 @@ async def _get_dynamic_context() -> str:
220
236
  # RESOURCES (Documentation that LLMs can read)
221
237
  # ============================================================================
222
238
 
239
+
223
240
  @mcp.resource("mem-brain://docs/workflow-guide")
224
241
  def workflow_guide() -> str:
225
242
  """Complete guide to the memory workflow: search strategies, pattern recognition, storage guidelines, and best practices."""
@@ -349,28 +366,26 @@ def storage_guidelines() -> str:
349
366
  # PROMPTS (Bootstrap Intelligence)
350
367
  # ============================================================================
351
368
 
369
+
352
370
  @mcp.prompt
353
371
  async def setup_personal_memory() -> PromptMessage:
354
372
  """Initializes the assistant with the user's identity, recent context, and memory management rules. Run this once at the start of a session."""
355
373
  context_section = await _get_dynamic_context()
356
-
374
+
357
375
  full_instructions = f"""{context_section}{AGENT_INSTRUCTIONS}
358
376
 
359
377
  **Note**: For detailed tool usage, see resource: `mem-brain://docs/tool-reference`
360
378
  For storage guidelines, see resource: `mem-brain://docs/storage-guidelines`
361
379
  """
362
-
363
- return PromptMessage(
364
- role="system",
365
- content=TextContent(type="text", text=full_instructions)
366
- )
380
+
381
+ return PromptMessage(role="system", content=TextContent(type="text", text=full_instructions))
367
382
 
368
383
 
369
384
  @mcp.prompt
370
385
  async def refresh_context() -> PromptMessage:
371
386
  """Refreshes the assistant's context with updated core identity and recent memories. Use when context feels stale."""
372
387
  context_section = await _get_dynamic_context()
373
-
388
+
374
389
  return PromptMessage(
375
390
  role="system",
376
391
  content=TextContent(
@@ -378,8 +393,8 @@ async def refresh_context() -> PromptMessage:
378
393
  text=f"""{context_section}
379
394
 
380
395
  **Context refreshed.** Continue using memory tools as before.
381
- """
382
- )
396
+ """,
397
+ ),
383
398
  )
384
399
 
385
400
 
@@ -387,6 +402,7 @@ async def refresh_context() -> PromptMessage:
387
402
  # TOOLS (Operations)
388
403
  # ============================================================================
389
404
 
405
+
390
406
  @mcp.tool()
391
407
  async def get_agent_instructions(include_dynamic_context: bool = True) -> str:
392
408
  """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."""
@@ -394,70 +410,68 @@ async def get_agent_instructions(include_dynamic_context: bool = True) -> str:
394
410
  context_section = await _get_dynamic_context()
395
411
  else:
396
412
  context_section = ""
397
-
413
+
398
414
  return context_section + AGENT_INSTRUCTIONS
399
415
 
400
416
 
401
417
  @mcp.tool()
402
418
  async def add_memory(
403
- content: str,
404
- tags: Optional[List[str]] = None,
405
- category: Optional[str] = None
419
+ content: str, tags: Optional[List[str]] = None, category: Optional[str] = None
406
420
  ) -> str:
407
421
  """Create a new memory with content, optional tags, and category. Use this when user reveals preferences, goals, or facts. Store FACTS, not conversation. Examples: 'User prefers dark mode' vs 'You said you like dark mode'. See mem-brain://docs/storage-guidelines for tagging patterns. DO NOT store conversation snippets - only store factual information.
408
-
422
+
409
423
  IMPORTANT: Before creating a new memory, you MUST first search existing memories using search_memories() to check if a similar memory already exists. This prevents duplicates and helps maintain memory quality. Only create a new memory if no similar memory is found.
410
-
424
+
411
425
  Parameters:
412
- content (str, REQUIRED): The memory content to store. Must be a non-empty string.
426
+ content (str, REQUIRED): The memory content to store. Must be a non-empty string.
413
427
  - Cannot be None, empty string, or whitespace-only
414
428
  - Example: "User prefers Python over JavaScript"
415
429
  - Example: "User prefers dark mode interfaces"
416
-
417
- tags (list[str] or str, optional): Tags to categorize the memory.
430
+
431
+ tags (list[str] or str, optional): Tags to categorize the memory.
418
432
  - Can be None (default), a list of strings, a comma-separated string, or a JSON array string
419
433
  - If omitted, the system will auto-generate tags based on content
420
434
  - Example: ["coding", "preferences"]
421
435
  - Example: "coding,preferences" (comma-separated)
422
436
  - Example: '["coding", "preferences"]' (JSON string)
423
437
  - Note: The system auto-generates relevant tags, so providing tags is optional
424
-
438
+
425
439
  category (str, optional): Category name for the memory.
426
440
  - Can be None (default) or a non-empty string
427
441
  - Example: "interests"
428
442
  - Example: "preferences"
429
-
443
+
430
444
  Returns:
431
445
  str: A formatted string with the memory ID and details of the created memory.
432
-
446
+
433
447
  Common Errors and Solutions:
434
448
  - Error: "Tool call arguments for mcp were invalid"
435
449
  Solution: Ensure 'content' parameter is provided as a string. Example: add_memory(content="User prefers dark mode")
436
-
450
+
437
451
  - Error: "The 'content' parameter cannot be empty"
438
452
  Solution: Provide non-empty content. Example: add_memory(content="User loves Python programming")
439
-
453
+
440
454
  - Error: "tags must be a list"
441
455
  Solution: Pass tags as a list. Example: add_memory(content="...", tags=["coding"]) not tags="coding"
442
-
456
+
443
457
  Example workflow:
444
458
  1. search_memories(query="User prefers Python") # Check for existing memories
445
459
  2. If no similar memory found, then: add_memory(content="User prefers Python over JavaScript", tags=["coding", "preferences"])
446
-
460
+
447
461
  Examples:
448
462
  # Basic usage (required parameter only)
449
463
  add_memory(content="User prefers dark mode")
450
-
464
+
451
465
  # With tags
452
466
  add_memory(content="User loves Python programming", tags=["coding", "preferences"])
453
-
467
+
454
468
  # With tags and category
455
469
  add_memory(
456
470
  content="User loves working with TypeScript",
457
471
  tags=["coding", "typescript"],
458
472
  category="interests"
459
473
  )
460
-
474
+
461
475
  # Tags as empty list (treated as None)
462
476
  add_memory(content="User prefers coffee", tags=[])
463
477
  """
@@ -465,30 +479,32 @@ async def add_memory(
465
479
  if content is None:
466
480
  raise ToolError(
467
481
  "The 'content' parameter is required but was not provided.\n"
468
- "Example: add_memory(content=\"User prefers dark mode\")\n"
469
- "Example: add_memory(content=\"User loves Python programming\", tags=[\"coding\"])"
482
+ 'Example: add_memory(content="User prefers dark mode")\n'
483
+ 'Example: add_memory(content="User loves Python programming", tags=["coding"])'
470
484
  )
471
-
485
+
472
486
  if not isinstance(content, str):
473
487
  raise ToolError(
474
488
  f"The 'content' parameter must be a string, but got {type(content).__name__}.\n"
475
489
  f"Received: {repr(content)}\n"
476
- "Example: add_memory(content=\"User prefers dark mode\")"
490
+ 'Example: add_memory(content="User prefers dark mode")'
477
491
  )
478
-
492
+
479
493
  content_str = str(content).strip()
480
494
  if not content_str:
481
495
  raise ToolError(
482
496
  "The 'content' parameter cannot be empty or whitespace-only.\n"
483
497
  "Please provide a non-empty string with actual content.\n"
484
- "Example: add_memory(content=\"User prefers dark mode\")\n"
485
- "Example: add_memory(content=\"User loves Python programming\")"
498
+ 'Example: add_memory(content="User prefers dark mode")\n'
499
+ 'Example: add_memory(content="User loves Python programming")'
486
500
  )
487
501
 
488
502
  try:
489
- logger.info(f"add_memory called - content length: {len(content_str)}, tags: {tags}, category: {category}")
503
+ logger.info(
504
+ f"add_memory called - content length: {len(content_str)}, tags: {tags}, category: {category}"
505
+ )
490
506
  logger.debug(f"add_memory full content: {content_str[:100]}...")
491
-
507
+
492
508
  # Normalize tags: handle various input formats and convert to list of strings
493
509
  normalized_tags = None
494
510
  if tags is not None:
@@ -499,8 +515,8 @@ async def add_memory(
499
515
  if invalid_items:
500
516
  raise ToolError(
501
517
  f"The 'tags' parameter must be a list of strings, but found non-string items: {invalid_items}\n"
502
- f"Example: add_memory(content=\"...\", tags=[\"coding\", \"preferences\"])\n"
503
- f"Example: add_memory(content=\"...\", tags=[\"personal\", \"pets\"])"
518
+ f'Example: add_memory(content="...", tags=["coding", "preferences"])\n'
519
+ f'Example: add_memory(content="...", tags=["personal", "pets"])'
504
520
  )
505
521
  normalized_tags = tags if tags else None # Empty list becomes None
506
522
  elif isinstance(tags, str):
@@ -512,14 +528,18 @@ async def add_memory(
512
528
  try:
513
529
  parsed = json.loads(tags_str)
514
530
  if isinstance(parsed, list):
515
- normalized_tags = [str(item).strip() for item in parsed if str(item).strip()]
531
+ normalized_tags = [
532
+ str(item).strip() for item in parsed if str(item).strip()
533
+ ]
516
534
  else:
517
535
  # If JSON but not a list, treat as single tag
518
536
  normalized_tags = [tags_str]
519
537
  except (json.JSONDecodeError, ValueError):
520
538
  # Not JSON, try comma-separated string
521
- if ',' in tags_str:
522
- normalized_tags = [tag.strip() for tag in tags_str.split(',') if tag.strip()]
539
+ if "," in tags_str:
540
+ normalized_tags = [
541
+ tag.strip() for tag in tags_str.split(",") if tag.strip()
542
+ ]
523
543
  else:
524
544
  # Single tag string
525
545
  normalized_tags = [tags_str]
@@ -527,21 +547,27 @@ async def add_memory(
527
547
  raise ToolError(
528
548
  f"The 'tags' parameter must be a list of strings, a comma-separated string, or None, but got {type(tags).__name__}.\n"
529
549
  f"Received: {repr(tags)}\n"
530
- "Example: add_memory(content=\"...\", tags=[\"coding\", \"preferences\"])\n"
531
- "Example: add_memory(content=\"...\", tags=\"coding,preferences\")\n"
532
- "Example: add_memory(content=\"...\", tags=None) # or omit tags parameter"
550
+ 'Example: add_memory(content="...", tags=["coding", "preferences"])\n'
551
+ 'Example: add_memory(content="...", tags="coding,preferences")\n'
552
+ 'Example: add_memory(content="...", tags=None) # or omit tags parameter'
533
553
  )
534
-
554
+
535
555
  # Normalize category: convert empty string to None
536
- normalized_category = category.strip() if category and isinstance(category, str) and category.strip() else None
556
+ normalized_category = (
557
+ category.strip()
558
+ if category and isinstance(category, str) and category.strip()
559
+ else None
560
+ )
537
561
 
538
562
  client = await _get_api_client()
539
- logger.debug(f"Calling API client.add_memory with content='{content_str[:50]}...', tags={normalized_tags}, category={normalized_category}")
540
-
563
+ logger.debug(
564
+ f"Calling API client.add_memory with content='{content_str[:50]}...', tags={normalized_tags}, category={normalized_category}"
565
+ )
566
+
541
567
  result = await client.add_memory(content_str, normalized_tags, normalized_category)
542
-
568
+
543
569
  logger.info(f"Memory created successfully: {result.get('memory_id', 'unknown')}")
544
- memory = result.get('memory')
570
+ memory = result.get("memory")
545
571
  if memory:
546
572
  return f"Memory created: {result.get('memory_id', 'unknown')}\n{_format_memory(memory)}"
547
573
  return f"Memory created: {result.get('memory_id', 'unknown')}"
@@ -549,7 +575,9 @@ async def add_memory(
549
575
  error_detail = e.response.text if e.response else "Unknown error"
550
576
  logger.error(f"API error: {e.response.status_code} - {error_detail}")
551
577
  if e.response.status_code == 401:
552
- raise ToolError("Authentication failed. Please login using the 'login' tool or configure your JWT token in the MCP client headers.")
578
+ raise ToolError(
579
+ "Authentication failed. Please login using the 'login' tool or configure your JWT token in the MCP client headers."
580
+ )
553
581
  elif e.response.status_code == 400:
554
582
  raise ToolError(f"Invalid request: {error_detail}")
555
583
  raise ToolError(f"Failed to create memory: HTTP {e.response.status_code} - {error_detail}")
@@ -561,89 +589,102 @@ async def add_memory(
561
589
 
562
590
 
563
591
  @mcp.tool()
564
- async def search_memories(query: str, k: int = 5) -> str:
565
- """Search memories using semantic similarity. CRITICAL: Formulate specific, natural language queries, NOT simple keywords. Examples: 'Who is Maga and what is their relationship to me?' vs ❌ 'maga'. Check related_memories field for graph connections and synthesize across results. See mem-brain://docs/workflow-guide for search strategies. DO NOT use vague keywords - always use full questions.
566
-
592
+ async def search_memories(
593
+ query: str, k: int = 5, keyword_filter: Optional[Union[str, List[str]]] = None
594
+ ) -> str:
595
+ """Search memories using semantic similarity with optional regex-based tag filtering.
596
+
597
+ CRITICAL: Formulate specific, natural language queries, NOT simple keywords.
598
+ Examples: ✅ 'Who is Maga and what is their relationship to me?' vs ❌ 'maga'.
599
+
600
+ The keyword_filter allows for deterministic scoping (e.g., project-specific or session-specific).
601
+ - String: Match memories where ANY tag matches this regex pattern (case-insensitive).
602
+ - List[str]: Match memories where ALL patterns in the list satisfy at least one tag (AND logic).
603
+
567
604
  Parameters:
568
605
  query (str, REQUIRED): Search query string. Use natural language questions, not keywords.
569
606
  - Example: "Who is Rakshith and what did he build?"
570
607
  - Example: "What are the user's preferences for programming languages?"
571
- - Example: "Tell me about memories related to the Dubai presentation"
572
-
608
+
573
609
  k (int, optional): Number of results to return. Default is 5.
574
610
  - Must be between 1 and 100
575
- - Example: 5 (default)
576
- - Example: 10
577
-
611
+
612
+ keyword_filter (str | list[str], optional): Case-insensitive regex pattern or list of patterns to filter by tags.
613
+ - Example: "session-v1" (String: returns only memories tagged with session-v1)
614
+ - Example: "project-.*-2026" (Regex string: matches any 2026 project tag)
615
+ - Example: ["work", "important"] (List: returns only memories tagged with BOTH work AND important)
616
+
578
617
  Returns:
579
618
  str: Formatted search results with memory nodes and relationship edges.
580
-
619
+
581
620
  Common Errors and Solutions:
582
621
  - Error: "Query cannot be empty"
583
622
  Solution: Provide a non-empty search query. Example: search_memories(query="What is the user's name?")
584
-
623
+
585
624
  - Error: "k must be between 1 and 100"
586
625
  Solution: Provide k between 1 and 100. Example: search_memories(query="...", k=10)
587
-
626
+
588
627
  Examples:
589
- # Basic search
590
- search_memories(query="Who is Rakshith?")
591
-
592
- # Search with more results
593
- search_memories(query="What are the user's programming preferences?", k=10)
594
-
595
- # Complex query
596
- search_memories(query="Tell me about memories related to mem-brain and its features")
628
+ # Scoped search for a specific session
629
+ search_memories(query="What progress was made?", keyword_filter="session-v1")
630
+
631
+ # Scoped search using regex
632
+ search_memories(query="Database decisions", keyword_filter="project-.*")
597
633
  """
598
634
  # Validate parameters with detailed error messages
599
635
  if query is None:
600
636
  raise ToolError(
601
637
  "The 'query' parameter is required but was not provided.\n"
602
- "Example: search_memories(query=\"Who is Rakshith?\")\n"
603
- "Example: search_memories(query=\"What are the user's preferences?\")"
638
+ 'Example: search_memories(query="Who is Rakshith?")\n'
639
+ 'Example: search_memories(query="What are the user\'s preferences?")'
604
640
  )
605
-
641
+
606
642
  if not isinstance(query, str):
607
643
  raise ToolError(
608
644
  f"The 'query' parameter must be a string, but got {type(query).__name__}.\n"
609
645
  f"Received: {repr(query)}\n"
610
- "Example: search_memories(query=\"Who is Rakshith?\")"
646
+ 'Example: search_memories(query="Who is Rakshith?")'
611
647
  )
612
-
648
+
613
649
  query_str = query.strip()
614
650
  if not query_str:
615
651
  raise ToolError(
616
652
  "The 'query' parameter cannot be empty or whitespace-only.\n"
617
653
  "Provide a natural language question or search query.\n"
618
- "Example: search_memories(query=\"Who is Rakshith?\")\n"
619
- "Example: search_memories(query=\"What are the user's preferences?\")"
654
+ 'Example: search_memories(query="Who is Rakshith?")\n'
655
+ 'Example: search_memories(query="What are the user\'s preferences?")'
620
656
  )
621
-
657
+
622
658
  if not isinstance(k, int):
623
659
  raise ToolError(
624
660
  f"The 'k' parameter must be an integer, but got {type(k).__name__}.\n"
625
661
  f"Received: {repr(k)}\n"
626
- "Example: search_memories(query=\"...\", k=10)"
662
+ 'Example: search_memories(query="...", k=10)'
627
663
  )
628
-
664
+
629
665
  if not (1 <= k <= 100):
630
666
  raise ToolError(
631
667
  f"The 'k' parameter must be between 1 and 100, but got {k}.\n"
632
- "Example: search_memories(query=\"...\", k=5)\n"
633
- "Example: search_memories(query=\"...\", k=10)"
668
+ 'Example: search_memories(query="...", k=10)'
634
669
  )
635
670
 
636
671
  try:
637
- logger.info(f"search_memories called - query length: {len(query_str)}, k: {k}")
672
+ logger.info(
673
+ f"search_memories called - query: '{query_str[:50]}...', k: {k}, filter: {keyword_filter}"
674
+ )
638
675
  client = await _get_api_client()
639
- result = await client.search_memories(query_str, k)
676
+ result = await client.search_memories(query_str, k, keyword_filter)
640
677
  return f"Found {result.get('count', 0)} results:\n{_format_search_results(result.get('results', []))}"
641
678
  except httpx.HTTPStatusError as e:
642
679
  error_detail = e.response.text if e.response else "Unknown error"
643
680
  logger.error(f"API error: {e.response.status_code} - {error_detail}")
644
681
  if e.response.status_code == 401:
645
- raise ToolError("Authentication failed. Please login using the 'login' tool or configure your JWT token in the MCP client headers.")
646
- raise ToolError(f"Failed to search memories: HTTP {e.response.status_code} - {error_detail}")
682
+ raise ToolError(
683
+ "Authentication failed. Please login using the 'login' tool or configure your JWT token in the MCP client headers."
684
+ )
685
+ raise ToolError(
686
+ f"Failed to search memories: HTTP {e.response.status_code} - {error_detail}"
687
+ )
647
688
  except ToolError:
648
689
  raise
649
690
  except Exception as e:
@@ -654,30 +695,30 @@ async def search_memories(query: str, k: int = 5) -> str:
654
695
  @mcp.tool()
655
696
  async def get_memories(memory_ids: List[str]) -> str:
656
697
  """Retrieve one or more memories by ID. Use this when you need full details for specific memories identified from search results.
657
-
698
+
658
699
  Parameters:
659
700
  memory_ids (list[str], REQUIRED): List of memory IDs to retrieve. Must be a non-empty list.
660
701
  - Example: ["480c1f76-bcdf-4491-8781-24510db992e3"]
661
702
  - Example: ["480c1f76-...", "300d9716-...", "6fb6b23f-..."]
662
703
  - Get memory IDs from search_memories() results
663
-
704
+
664
705
  Returns:
665
706
  str: Formatted details of the retrieved memories.
666
-
707
+
667
708
  Common Errors and Solutions:
668
709
  - Error: "memory_ids cannot be empty"
669
710
  Solution: Provide a list with at least one memory ID. Example: get_memories(memory_ids=["480c1f76-..."])
670
-
711
+
671
712
  - Error: "Memory IDs cannot be empty"
672
713
  Solution: Ensure all IDs in the list are non-empty strings. Example: get_memories(memory_ids=["480c1f76-..."])
673
-
714
+
674
715
  - Error: "memory_ids must be a list"
675
716
  Solution: Pass memory_ids as a list. Example: get_memories(memory_ids=["..."]) not memory_ids="..."
676
-
717
+
677
718
  Examples:
678
719
  # Get single memory
679
720
  get_memories(memory_ids=["480c1f76-bcdf-4491-8781-24510db992e3"])
680
-
721
+
681
722
  # Get multiple memories
682
723
  get_memories(memory_ids=["480c1f76-...", "300d9716-...", "6fb6b23f-..."])
683
724
  """
@@ -685,44 +726,44 @@ async def get_memories(memory_ids: List[str]) -> str:
685
726
  if memory_ids is None:
686
727
  raise ToolError(
687
728
  "The 'memory_ids' parameter is required but was not provided.\n"
688
- "Example: get_memories(memory_ids=[\"480c1f76-bcdf-4491-8781-24510db992e3\"])\n"
689
- "Example: get_memories(memory_ids=[\"480c1f76-...\", \"300d9716-...\"])"
729
+ 'Example: get_memories(memory_ids=["480c1f76-bcdf-4491-8781-24510db992e3"])\n'
730
+ 'Example: get_memories(memory_ids=["480c1f76-...", "300d9716-..."])'
690
731
  )
691
-
732
+
692
733
  if not isinstance(memory_ids, list):
693
734
  raise ToolError(
694
735
  f"The 'memory_ids' parameter must be a list of strings, but got {type(memory_ids).__name__}.\n"
695
736
  f"Received: {repr(memory_ids)}\n"
696
- "Example: get_memories(memory_ids=[\"480c1f76-...\"])"
737
+ 'Example: get_memories(memory_ids=["480c1f76-..."])'
697
738
  )
698
-
739
+
699
740
  if not memory_ids:
700
741
  raise ToolError(
701
742
  "The 'memory_ids' parameter cannot be an empty list.\n"
702
743
  "Provide at least one memory ID.\n"
703
- "Example: get_memories(memory_ids=[\"480c1f76-bcdf-4491-8781-24510db992e3\"])"
744
+ 'Example: get_memories(memory_ids=["480c1f76-bcdf-4491-8781-24510db992e3"])'
704
745
  )
705
-
746
+
706
747
  # Validate each memory ID in the list
707
748
  validated_ids = []
708
749
  for i, memory_id in enumerate(memory_ids):
709
750
  if memory_id is None:
710
751
  raise ToolError(
711
752
  f"Memory ID at index {i} is None. All memory IDs must be non-empty strings.\n"
712
- "Example: get_memories(memory_ids=[\"480c1f76-...\"])"
753
+ 'Example: get_memories(memory_ids=["480c1f76-..."])'
713
754
  )
714
755
  if not isinstance(memory_id, str):
715
756
  raise ToolError(
716
757
  f"Memory ID at index {i} must be a string, but got {type(memory_id).__name__}.\n"
717
758
  f"Received: {repr(memory_id)}\n"
718
- "Example: get_memories(memory_ids=[\"480c1f76-...\"])"
759
+ 'Example: get_memories(memory_ids=["480c1f76-..."])'
719
760
  )
720
761
  memory_id_str = memory_id.strip()
721
762
  if not memory_id_str:
722
763
  raise ToolError(
723
764
  f"Memory ID at index {i} cannot be empty or whitespace-only.\n"
724
765
  "Get memory IDs from search_memories() or get_memories() results.\n"
725
- "Example: get_memories(memory_ids=[\"480c1f76-bcdf-4491-8781-24510db992e3\"])"
766
+ 'Example: get_memories(memory_ids=["480c1f76-bcdf-4491-8781-24510db992e3"])'
726
767
  )
727
768
  validated_ids.append(memory_id_str)
728
769
 
@@ -736,9 +777,13 @@ async def get_memories(memory_ids: List[str]) -> str:
736
777
  error_detail = e.response.text if e.response else "Unknown error"
737
778
  logger.error(f"API error: {e.response.status_code} - {error_detail}")
738
779
  if e.response.status_code == 401:
739
- raise ToolError("Authentication failed. Please login using the 'login' tool or configure your JWT token in the MCP client headers.")
780
+ raise ToolError(
781
+ "Authentication failed. Please login using the 'login' tool or configure your JWT token in the MCP client headers."
782
+ )
740
783
  elif e.response.status_code == 404:
741
- raise ToolError(f"One or more memories not found.\nVerify the memory IDs are correct by searching for them first.")
784
+ raise ToolError(
785
+ f"One or more memories not found.\nVerify the memory IDs are correct by searching for them first."
786
+ )
742
787
  raise ToolError(f"Failed to get memories: HTTP {e.response.status_code} - {error_detail}")
743
788
  except ToolError:
744
789
  raise
@@ -749,22 +794,20 @@ async def get_memories(memory_ids: List[str]) -> str:
749
794
 
750
795
  @mcp.tool()
751
796
  async def update_memory(
752
- memory_id: str,
753
- content: Optional[str] = None,
754
- tags: Optional[List[str]] = None
797
+ memory_id: str, content: Optional[str] = None, tags: Optional[List[str]] = None
755
798
  ) -> str:
756
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.
757
-
800
+
758
801
  Parameters:
759
802
  memory_id (str, REQUIRED): The ID of the memory to update. Must be a non-empty string.
760
803
  - Example: "480c1f76-bcdf-4491-8781-24510db992e3"
761
804
  - Get memory IDs from search_memories() or get_memories() results
762
-
763
- content (str, optional): New content for the memory.
805
+
806
+ content (str, optional): New content for the memory.
764
807
  - Can be None (to keep existing content) or a non-empty string
765
808
  - If provided, must not be empty or whitespace-only
766
809
  - Example: "User no longer likes TypeScript, prefers Python"
767
-
810
+
768
811
  tags (list[str] or str, optional): New tags for the memory.
769
812
  - Can be None (to keep existing tags), a list of strings, a comma-separated string, or a JSON array string
770
813
  - If provided, replaces existing tags
@@ -772,27 +815,27 @@ async def update_memory(
772
815
  - Example: "coding,python" (comma-separated)
773
816
  - Example: '["coding", "python"]' (JSON string)
774
817
  - Note: The system can auto-generate tags if you omit this parameter
775
-
818
+
776
819
  Returns:
777
820
  str: A formatted string with the updated memory details.
778
-
821
+
779
822
  Common Errors and Solutions:
780
823
  - Error: "Tool call arguments for mcp were invalid"
781
824
  Solution: Ensure 'memory_id' parameter is provided as a string. Example: update_memory(memory_id="...")
782
-
825
+
783
826
  - Error: "memory_id cannot be empty"
784
827
  Solution: Provide a valid memory ID from search results. Example: update_memory(memory_id="480c1f76-...")
785
-
828
+
786
829
  - Error: "At least one of 'content' or 'tags' must be provided"
787
830
  Solution: Provide content or tags to update. Example: update_memory(memory_id="...", content="New content")
788
-
831
+
789
832
  Examples:
790
833
  # Update content only
791
834
  update_memory(memory_id="480c1f76-...", content="User prefers Python over JavaScript")
792
-
835
+
793
836
  # Update tags only
794
837
  update_memory(memory_id="480c1f76-...", tags=["coding", "preferences"])
795
-
838
+
796
839
  # Update both content and tags
797
840
  update_memory(
798
841
  memory_id="480c1f76-...",
@@ -805,50 +848,50 @@ async def update_memory(
805
848
  raise ToolError(
806
849
  "The 'memory_id' parameter is required but was not provided.\n"
807
850
  "Get memory IDs from search_memories() or get_memories() results.\n"
808
- "Example: update_memory(memory_id=\"480c1f76-bcdf-4491-8781-24510db992e3\", content=\"New content\")"
851
+ 'Example: update_memory(memory_id="480c1f76-bcdf-4491-8781-24510db992e3", content="New content")'
809
852
  )
810
-
853
+
811
854
  if not isinstance(memory_id, str):
812
855
  raise ToolError(
813
856
  f"The 'memory_id' parameter must be a string, but got {type(memory_id).__name__}.\n"
814
857
  f"Received: {repr(memory_id)}\n"
815
- "Example: update_memory(memory_id=\"480c1f76-...\", content=\"New content\")"
858
+ 'Example: update_memory(memory_id="480c1f76-...", content="New content")'
816
859
  )
817
-
860
+
818
861
  memory_id_str = memory_id.strip()
819
862
  if not memory_id_str:
820
863
  raise ToolError(
821
864
  "The 'memory_id' parameter cannot be empty or whitespace-only.\n"
822
865
  "Get memory IDs from search_memories() or get_memories() results.\n"
823
- "Example: update_memory(memory_id=\"480c1f76-bcdf-4491-8781-24510db992e3\", content=\"New content\")"
866
+ 'Example: update_memory(memory_id="480c1f76-bcdf-4491-8781-24510db992e3", content="New content")'
824
867
  )
825
-
868
+
826
869
  # Validate that at least one update parameter is provided
827
870
  if content is None and tags is None:
828
871
  raise ToolError(
829
872
  "At least one of 'content' or 'tags' must be provided to update the memory.\n"
830
- "Example: update_memory(memory_id=\"...\", content=\"New content\")\n"
831
- "Example: update_memory(memory_id=\"...\", tags=[\"new\", \"tags\"])"
873
+ 'Example: update_memory(memory_id="...", content="New content")\n'
874
+ 'Example: update_memory(memory_id="...", tags=["new", "tags"])'
832
875
  )
833
-
876
+
834
877
  # Validate content if provided
835
878
  if content is not None:
836
879
  if not isinstance(content, str):
837
880
  raise ToolError(
838
881
  f"The 'content' parameter must be a string or None, but got {type(content).__name__}.\n"
839
882
  f"Received: {repr(content)}\n"
840
- "Example: update_memory(memory_id=\"...\", content=\"New content\")"
883
+ 'Example: update_memory(memory_id="...", content="New content")'
841
884
  )
842
885
  content_str = str(content).strip()
843
886
  if not content_str:
844
887
  raise ToolError(
845
888
  "The 'content' parameter cannot be empty or whitespace-only.\n"
846
889
  "Provide a non-empty string or omit the parameter to keep existing content.\n"
847
- "Example: update_memory(memory_id=\"...\", content=\"New content\")"
890
+ 'Example: update_memory(memory_id="...", content="New content")'
848
891
  )
849
892
  else:
850
893
  content_str = None
851
-
894
+
852
895
  # Validate tags if provided - handle various input formats
853
896
  normalized_tags = None
854
897
  if tags is not None:
@@ -859,8 +902,8 @@ async def update_memory(
859
902
  if invalid_items:
860
903
  raise ToolError(
861
904
  f"The 'tags' parameter must be a list of strings, but found non-string items: {invalid_items}\n"
862
- "Example: update_memory(memory_id=\"...\", tags=[\"coding\", \"preferences\"])\n"
863
- "Example: update_memory(memory_id=\"...\", tags=None) # or omit tags parameter"
905
+ 'Example: update_memory(memory_id="...", tags=["coding", "preferences"])\n'
906
+ 'Example: update_memory(memory_id="...", tags=None) # or omit tags parameter'
864
907
  )
865
908
  normalized_tags = tags if tags else None # Empty list becomes None
866
909
  elif isinstance(tags, str):
@@ -872,14 +915,18 @@ async def update_memory(
872
915
  try:
873
916
  parsed = json.loads(tags_str)
874
917
  if isinstance(parsed, list):
875
- normalized_tags = [str(item).strip() for item in parsed if str(item).strip()]
918
+ normalized_tags = [
919
+ str(item).strip() for item in parsed if str(item).strip()
920
+ ]
876
921
  else:
877
922
  # If JSON but not a list, treat as single tag
878
923
  normalized_tags = [tags_str]
879
924
  except (json.JSONDecodeError, ValueError):
880
925
  # Not JSON, try comma-separated string
881
- if ',' in tags_str:
882
- normalized_tags = [tag.strip() for tag in tags_str.split(',') if tag.strip()]
926
+ if "," in tags_str:
927
+ normalized_tags = [
928
+ tag.strip() for tag in tags_str.split(",") if tag.strip()
929
+ ]
883
930
  else:
884
931
  # Single tag string
885
932
  normalized_tags = [tags_str]
@@ -887,17 +934,19 @@ async def update_memory(
887
934
  raise ToolError(
888
935
  f"The 'tags' parameter must be a list of strings, a comma-separated string, or None, but got {type(tags).__name__}.\n"
889
936
  f"Received: {repr(tags)}\n"
890
- "Example: update_memory(memory_id=\"...\", tags=[\"coding\", \"preferences\"])\n"
891
- "Example: update_memory(memory_id=\"...\", tags=\"coding,preferences\")\n"
892
- "Example: update_memory(memory_id=\"...\", tags=None) # or omit tags parameter"
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'
893
940
  )
894
941
 
895
942
  try:
896
- logger.info(f"update_memory called - memory_id: {memory_id_str}, content length: {len(content_str) if content_str else 0}, tags: {normalized_tags}")
897
-
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
+
898
947
  client = await _get_api_client()
899
948
  result = await client.update_memory(memory_id_str, content_str, normalized_tags)
900
- memory = result.get('memory')
949
+ memory = result.get("memory")
901
950
  if memory:
902
951
  return f"Memory updated:\n{_format_memory(memory)}"
903
952
  return f"Memory {memory_id_str} updated"
@@ -905,11 +954,17 @@ async def update_memory(
905
954
  error_detail = e.response.text if e.response else "Unknown error"
906
955
  logger.error(f"API error: {e.response.status_code} - {error_detail}")
907
956
  if e.response.status_code == 401:
908
- raise ToolError("Authentication failed. Please login using the 'login' tool or configure your JWT token in the MCP client headers.")
957
+ raise ToolError(
958
+ "Authentication failed. Please login using the 'login' tool or configure your JWT token in the MCP client headers."
959
+ )
909
960
  elif e.response.status_code == 400:
910
- raise ToolError(f"Invalid request: {error_detail}\nExample: update_memory(memory_id=\"...\", content=\"New content\")")
961
+ raise ToolError(
962
+ f'Invalid request: {error_detail}\nExample: update_memory(memory_id="...", content="New content")'
963
+ )
911
964
  elif e.response.status_code == 404:
912
- raise ToolError(f"Memory not found: {memory_id_str}\nVerify the memory_id is correct by searching for it first.")
965
+ raise ToolError(
966
+ f"Memory not found: {memory_id_str}\nVerify the memory_id is correct by searching for it first."
967
+ )
913
968
  raise ToolError(f"Failed to update memory: HTTP {e.response.status_code} - {error_detail}")
914
969
  except ToolError:
915
970
  raise
@@ -920,44 +975,42 @@ async def update_memory(
920
975
 
921
976
  @mcp.tool()
922
977
  async def delete_memories(
923
- memory_id: Optional[str] = None,
924
- tags: Optional[str] = None,
925
- category: Optional[str] = None
978
+ memory_id: Optional[str] = None, tags: Optional[str] = None, category: Optional[str] = None
926
979
  ) -> str:
927
980
  """Delete memories by ID or by filter (tags/category). If memory_id is provided, it takes precedence over filters. Use for removing wrong memories or when user explicitly requests deletion.
928
-
981
+
929
982
  Parameters:
930
983
  memory_id (str, optional): Specific memory ID to delete. Takes precedence over filters.
931
984
  - Example: "480c1f76-bcdf-4491-8781-24510db992e3"
932
985
  - Get memory IDs from search_memories() or get_memories() results
933
-
986
+
934
987
  tags (str, optional): Comma-separated tags for filter-based deletion.
935
988
  - Example: "coding,preferences"
936
989
  - Example: "personal,pets"
937
990
  - Only used if memory_id is not provided
938
-
991
+
939
992
  category (str, optional): Category name for filter-based deletion.
940
993
  - Example: "interests"
941
994
  - Example: "preferences"
942
995
  - Only used if memory_id is not provided
943
-
996
+
944
997
  Returns:
945
998
  str: A message indicating how many memories were deleted and their IDs.
946
-
999
+
947
1000
  Common Errors and Solutions:
948
1001
  - Error: "At least one parameter must be provided"
949
1002
  Solution: Provide memory_id, tags, or category. Example: delete_memories(memory_id="...")
950
-
1003
+
951
1004
  - Error: "memory_id cannot be empty"
952
1005
  Solution: Provide a valid memory ID or omit the parameter. Example: delete_memories(memory_id="480c1f76-...")
953
-
1006
+
954
1007
  Examples:
955
1008
  # Delete by memory ID
956
1009
  delete_memories(memory_id="480c1f76-bcdf-4491-8781-24510db992e3")
957
-
1010
+
958
1011
  # Delete by tags
959
1012
  delete_memories(tags="coding,preferences")
960
-
1013
+
961
1014
  # Delete by category
962
1015
  delete_memories(category="interests")
963
1016
  """
@@ -965,79 +1018,87 @@ async def delete_memories(
965
1018
  if memory_id is None and tags is None and category is None:
966
1019
  raise ToolError(
967
1020
  "At least one parameter (memory_id, tags, or category) must be provided to delete memories.\n"
968
- "Example: delete_memories(memory_id=\"480c1f76-bcdf-4491-8781-24510db992e3\")\n"
969
- "Example: delete_memories(tags=\"coding,preferences\")\n"
970
- "Example: delete_memories(category=\"interests\")"
1021
+ 'Example: delete_memories(memory_id="480c1f76-bcdf-4491-8781-24510db992e3")\n'
1022
+ 'Example: delete_memories(tags="coding,preferences")\n'
1023
+ 'Example: delete_memories(category="interests")'
971
1024
  )
972
-
1025
+
973
1026
  # Validate memory_id if provided
974
1027
  if memory_id is not None:
975
1028
  if not isinstance(memory_id, str):
976
1029
  raise ToolError(
977
1030
  f"The 'memory_id' parameter must be a string or None, but got {type(memory_id).__name__}.\n"
978
1031
  f"Received: {repr(memory_id)}\n"
979
- "Example: delete_memories(memory_id=\"480c1f76-bcdf-4491-8781-24510db992e3\")"
1032
+ 'Example: delete_memories(memory_id="480c1f76-bcdf-4491-8781-24510db992e3")'
980
1033
  )
981
1034
  memory_id_str = memory_id.strip()
982
1035
  if not memory_id_str:
983
1036
  raise ToolError(
984
1037
  "The 'memory_id' parameter cannot be empty or whitespace-only.\n"
985
1038
  "Get memory IDs from search_memories() or get_memories() results.\n"
986
- "Example: delete_memories(memory_id=\"480c1f76-bcdf-4491-8781-24510db992e3\")"
1039
+ 'Example: delete_memories(memory_id="480c1f76-bcdf-4491-8781-24510db992e3")'
987
1040
  )
988
1041
  else:
989
1042
  memory_id_str = None
990
-
1043
+
991
1044
  # Validate tags if provided
992
1045
  if tags is not None:
993
1046
  if not isinstance(tags, str):
994
1047
  raise ToolError(
995
1048
  f"The 'tags' parameter must be a string or None, but got {type(tags).__name__}.\n"
996
1049
  f"Received: {repr(tags)}\n"
997
- "Example: delete_memories(tags=\"coding,preferences\")"
1050
+ 'Example: delete_memories(tags="coding,preferences")'
998
1051
  )
999
1052
  tags_str = tags.strip()
1000
1053
  if not tags_str:
1001
1054
  raise ToolError(
1002
1055
  "The 'tags' parameter cannot be empty or whitespace-only.\n"
1003
1056
  "Provide comma-separated tags or omit the parameter.\n"
1004
- "Example: delete_memories(tags=\"coding,preferences\")"
1057
+ 'Example: delete_memories(tags="coding,preferences")'
1005
1058
  )
1006
1059
  else:
1007
1060
  tags_str = None
1008
-
1061
+
1009
1062
  # Validate category if provided
1010
1063
  if category is not None:
1011
1064
  if not isinstance(category, str):
1012
1065
  raise ToolError(
1013
1066
  f"The 'category' parameter must be a string or None, but got {type(category).__name__}.\n"
1014
1067
  f"Received: {repr(category)}\n"
1015
- "Example: delete_memories(category=\"interests\")"
1068
+ 'Example: delete_memories(category="interests")'
1016
1069
  )
1017
1070
  category_str = category.strip()
1018
1071
  if not category_str:
1019
1072
  raise ToolError(
1020
1073
  "The 'category' parameter cannot be empty or whitespace-only.\n"
1021
1074
  "Provide a category name or omit the parameter.\n"
1022
- "Example: delete_memories(category=\"interests\")"
1075
+ 'Example: delete_memories(category="interests")'
1023
1076
  )
1024
1077
  else:
1025
1078
  category_str = None
1026
-
1079
+
1027
1080
  try:
1028
- logger.info(f"delete_memories called - memory_id: {memory_id_str}, tags: {tags_str}, category: {category_str}")
1081
+ logger.info(
1082
+ f"delete_memories called - memory_id: {memory_id_str}, tags: {tags_str}, category: {category_str}"
1083
+ )
1029
1084
  client = await _get_api_client()
1030
1085
  result = await client.delete_memories(memory_id_str, tags_str, category_str)
1031
- deleted_ids = result.get('memory_ids', [])[:10]
1086
+ deleted_ids = result.get("memory_ids", [])[:10]
1032
1087
  return f"Deleted {result.get('deleted_count', 0)} memories. IDs: {', '.join(deleted_ids)}"
1033
1088
  except httpx.HTTPStatusError as e:
1034
1089
  error_detail = e.response.text if e.response else "Unknown error"
1035
1090
  logger.error(f"API error: {e.response.status_code} - {error_detail}")
1036
1091
  if e.response.status_code == 401:
1037
- raise ToolError("Authentication failed. Please login using the 'login' tool or configure your JWT token in the MCP client headers.")
1092
+ raise ToolError(
1093
+ "Authentication failed. Please login using the 'login' tool or configure your JWT token in the MCP client headers."
1094
+ )
1038
1095
  elif e.response.status_code == 404:
1039
- raise ToolError(f"Memory not found: {memory_id_str}\nVerify the memory_id is correct by searching for it first.")
1040
- raise ToolError(f"Failed to delete memories: HTTP {e.response.status_code} - {error_detail}")
1096
+ raise ToolError(
1097
+ f"Memory not found: {memory_id_str}\nVerify the memory_id is correct by searching for it first."
1098
+ )
1099
+ raise ToolError(
1100
+ f"Failed to delete memories: HTTP {e.response.status_code} - {error_detail}"
1101
+ )
1041
1102
  except ToolError:
1042
1103
  raise
1043
1104
  except Exception as e:
@@ -1048,26 +1109,26 @@ async def delete_memories(
1048
1109
  @mcp.tool()
1049
1110
  async def unlink_memories(memory_id_1: str, memory_id_2: str) -> str:
1050
1111
  """Remove link between two memories when the connection is no longer relevant or accurate.
1051
-
1112
+
1052
1113
  Parameters:
1053
1114
  memory_id_1 (str, REQUIRED): First memory ID in the link to remove.
1054
1115
  - Example: "480c1f76-bcdf-4491-8781-24510db992e3"
1055
1116
  - Get memory IDs from search_memories() or get_memories() results
1056
-
1117
+
1057
1118
  memory_id_2 (str, REQUIRED): Second memory ID in the link to remove.
1058
1119
  - Example: "300d9716-a3a6-44d3-b0f4-b28002a65da8"
1059
1120
  - Get memory IDs from search_memories() or get_memories() results
1060
-
1121
+
1061
1122
  Returns:
1062
1123
  str: Confirmation message that the memories were unlinked.
1063
-
1124
+
1064
1125
  Common Errors and Solutions:
1065
1126
  - Error: "memory_id_1 cannot be empty"
1066
1127
  Solution: Provide a valid memory ID. Example: unlink_memories(memory_id_1="480c1f76-...", memory_id_2="300d9716-...")
1067
-
1128
+
1068
1129
  - Error: "memory_id_2 cannot be empty"
1069
1130
  Solution: Provide a valid memory ID. Example: unlink_memories(memory_id_1="480c1f76-...", memory_id_2="300d9716-...")
1070
-
1131
+
1071
1132
  Examples:
1072
1133
  # Unlink two memories
1073
1134
  unlink_memories(
@@ -1080,56 +1141,58 @@ async def unlink_memories(memory_id_1: str, memory_id_2: str) -> str:
1080
1141
  raise ToolError(
1081
1142
  "The 'memory_id_1' parameter is required but was not provided.\n"
1082
1143
  "Get memory IDs from search_memories() or get_memories() results.\n"
1083
- "Example: unlink_memories(memory_id_1=\"480c1f76-...\", memory_id_2=\"300d9716-...\")"
1144
+ 'Example: unlink_memories(memory_id_1="480c1f76-...", memory_id_2="300d9716-...")'
1084
1145
  )
1085
-
1146
+
1086
1147
  if not isinstance(memory_id_1, str):
1087
1148
  raise ToolError(
1088
1149
  f"The 'memory_id_1' parameter must be a string, but got {type(memory_id_1).__name__}.\n"
1089
1150
  f"Received: {repr(memory_id_1)}\n"
1090
- "Example: unlink_memories(memory_id_1=\"480c1f76-...\", memory_id_2=\"300d9716-...\")"
1151
+ 'Example: unlink_memories(memory_id_1="480c1f76-...", memory_id_2="300d9716-...")'
1091
1152
  )
1092
-
1153
+
1093
1154
  memory_id_1_str = memory_id_1.strip()
1094
1155
  if not memory_id_1_str:
1095
1156
  raise ToolError(
1096
1157
  "The 'memory_id_1' parameter cannot be empty or whitespace-only.\n"
1097
1158
  "Get memory IDs from search_memories() or get_memories() results.\n"
1098
- "Example: unlink_memories(memory_id_1=\"480c1f76-bcdf-4491-8781-24510db992e3\", memory_id_2=\"300d9716-...\")"
1159
+ 'Example: unlink_memories(memory_id_1="480c1f76-bcdf-4491-8781-24510db992e3", memory_id_2="300d9716-...")'
1099
1160
  )
1100
-
1161
+
1101
1162
  if memory_id_2 is None:
1102
1163
  raise ToolError(
1103
1164
  "The 'memory_id_2' parameter is required but was not provided.\n"
1104
1165
  "Get memory IDs from search_memories() or get_memories() results.\n"
1105
- "Example: unlink_memories(memory_id_1=\"480c1f76-...\", memory_id_2=\"300d9716-...\")"
1166
+ 'Example: unlink_memories(memory_id_1="480c1f76-...", memory_id_2="300d9716-...")'
1106
1167
  )
1107
-
1168
+
1108
1169
  if not isinstance(memory_id_2, str):
1109
1170
  raise ToolError(
1110
1171
  f"The 'memory_id_2' parameter must be a string, but got {type(memory_id_2).__name__}.\n"
1111
1172
  f"Received: {repr(memory_id_2)}\n"
1112
- "Example: unlink_memories(memory_id_1=\"480c1f76-...\", memory_id_2=\"300d9716-...\")"
1173
+ 'Example: unlink_memories(memory_id_1="480c1f76-...", memory_id_2="300d9716-...")'
1113
1174
  )
1114
-
1175
+
1115
1176
  memory_id_2_str = memory_id_2.strip()
1116
1177
  if not memory_id_2_str:
1117
1178
  raise ToolError(
1118
1179
  "The 'memory_id_2' parameter cannot be empty or whitespace-only.\n"
1119
1180
  "Get memory IDs from search_memories() or get_memories() results.\n"
1120
- "Example: unlink_memories(memory_id_1=\"480c1f76-...\", memory_id_2=\"300d9716-a3a6-44d3-b0f4-b28002a65da8\")"
1181
+ 'Example: unlink_memories(memory_id_1="480c1f76-...", memory_id_2="300d9716-a3a6-44d3-b0f4-b28002a65da8")'
1121
1182
  )
1122
-
1183
+
1123
1184
  # Ensure the IDs are different
1124
1185
  if memory_id_1_str == memory_id_2_str:
1125
1186
  raise ToolError(
1126
1187
  "memory_id_1 and memory_id_2 must be different.\n"
1127
1188
  "You cannot unlink a memory from itself.\n"
1128
- "Example: unlink_memories(memory_id_1=\"480c1f76-...\", memory_id_2=\"300d9716-...\")"
1189
+ 'Example: unlink_memories(memory_id_1="480c1f76-...", memory_id_2="300d9716-...")'
1129
1190
  )
1130
1191
 
1131
1192
  try:
1132
- logger.info(f"unlink_memories called - memory_id_1: {memory_id_1_str}, memory_id_2: {memory_id_2_str}")
1193
+ logger.info(
1194
+ f"unlink_memories called - memory_id_1: {memory_id_1_str}, memory_id_2: {memory_id_2_str}"
1195
+ )
1133
1196
  client = await _get_api_client()
1134
1197
  result = await client.unlink_memories(memory_id_1_str, memory_id_2_str)
1135
1198
  return result.get("message", "Memories unlinked")
@@ -1137,10 +1200,16 @@ async def unlink_memories(memory_id_1: str, memory_id_2: str) -> str:
1137
1200
  error_detail = e.response.text if e.response else "Unknown error"
1138
1201
  logger.error(f"API error: {e.response.status_code} - {error_detail}")
1139
1202
  if e.response.status_code == 401:
1140
- raise ToolError("Authentication failed. Please login using the 'login' tool or configure your JWT token in the MCP client headers.")
1203
+ raise ToolError(
1204
+ "Authentication failed. Please login using the 'login' tool or configure your JWT token in the MCP client headers."
1205
+ )
1141
1206
  elif e.response.status_code == 404:
1142
- raise ToolError(f"One or both memories not found.\nVerify the memory IDs are correct by searching for them first.")
1143
- raise ToolError(f"Failed to unlink memories: HTTP {e.response.status_code} - {error_detail}")
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
+ )
1144
1213
  except ToolError:
1145
1214
  raise
1146
1215
  except Exception as e:
@@ -1151,20 +1220,20 @@ async def unlink_memories(memory_id_1: str, memory_id_2: str) -> str:
1151
1220
  @mcp.tool()
1152
1221
  async def get_stats(_placeholder: Optional[bool] = None) -> str:
1153
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.
1154
-
1223
+
1155
1224
  Parameters:
1156
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.
1157
1226
  - This is a workaround for MCP clients that incorrectly require a parameter for parameterless tools
1158
1227
  - Can be safely omitted or set to None/True/False
1159
1228
  - Example: get_stats() or get_stats(_placeholder=True)
1160
-
1229
+
1161
1230
  Returns:
1162
1231
  str: Formatted statistics including total memories, links, and top tags.
1163
-
1232
+
1164
1233
  Examples:
1165
1234
  # Get statistics (preferred - no parameters needed)
1166
1235
  get_stats()
1167
-
1236
+
1168
1237
  # Get statistics (OpenCode workaround - parameter is ignored)
1169
1238
  get_stats(_placeholder=True)
1170
1239
  """
@@ -1176,18 +1245,22 @@ async def get_stats(_placeholder: Optional[bool] = None) -> str:
1176
1245
  client = await _get_api_client()
1177
1246
  logger.debug(f"API client initialized with base_url: {client.base_url}")
1178
1247
  result = await client.get_stats()
1179
- logger.debug(f"get_stats result received: {list(result.keys()) if isinstance(result, dict) else 'N/A'}")
1180
- top_tags = ', '.join([f"{tag}({count})" for tag, count in result.get('top_tags', [])[:10]])
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]])
1181
1252
  return f"""Memory System Statistics:
1182
- Total Memories: {result.get('total_memories', 0)}
1183
- Total Links: {result.get('total_links', 0)}
1184
- Average Links per Memory: {result.get('avg_links_per_memory', 0):.2f}
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}
1185
1256
  Top Tags: {top_tags}"""
1186
1257
  except httpx.HTTPStatusError as e:
1187
1258
  error_detail = e.response.text if e.response else "Unknown error"
1188
1259
  logger.error(f"API error: {e.response.status_code} - {error_detail}")
1189
1260
  if e.response.status_code == 401:
1190
- raise ToolError("Authentication failed. Please login using the 'login' tool or configure your JWT token in the MCP client headers.")
1261
+ raise ToolError(
1262
+ "Authentication failed. Please login using the 'login' tool or configure your JWT token in the MCP client headers."
1263
+ )
1191
1264
  raise ToolError(f"Failed to get stats: HTTP {e.response.status_code} - {error_detail}")
1192
1265
  except ToolError:
1193
1266
  raise
@@ -1199,26 +1272,26 @@ Top Tags: {top_tags}"""
1199
1272
  @mcp.tool()
1200
1273
  async def find_path(from_id: str, to_id: str) -> str:
1201
1274
  """Find shortest path between two memories in the memory graph. Use this to explain connections between seemingly unrelated memories.
1202
-
1275
+
1203
1276
  Parameters:
1204
1277
  from_id (str, REQUIRED): Source memory ID to start the path from.
1205
1278
  - Example: "480c1f76-bcdf-4491-8781-24510db992e3"
1206
1279
  - Get memory IDs from search_memories() or get_memories() results
1207
-
1280
+
1208
1281
  to_id (str, REQUIRED): Target memory ID to find path to.
1209
1282
  - Example: "300d9716-a3a6-44d3-b0f4-b28002a65da8"
1210
1283
  - Get memory IDs from search_memories() or get_memories() results
1211
-
1284
+
1212
1285
  Returns:
1213
1286
  str: The shortest path between the two memories, or a message if no path exists.
1214
-
1287
+
1215
1288
  Common Errors and Solutions:
1216
1289
  - Error: "from_id cannot be empty"
1217
1290
  Solution: Provide a valid memory ID. Example: find_path(from_id="480c1f76-...", to_id="300d9716-...")
1218
-
1291
+
1219
1292
  - Error: "to_id cannot be empty"
1220
1293
  Solution: Provide a valid memory ID. Example: find_path(from_id="480c1f76-...", to_id="300d9716-...")
1221
-
1294
+
1222
1295
  Examples:
1223
1296
  # Find path between two memories
1224
1297
  find_path(
@@ -1231,44 +1304,44 @@ async def find_path(from_id: str, to_id: str) -> str:
1231
1304
  raise ToolError(
1232
1305
  "The 'from_id' parameter is required but was not provided.\n"
1233
1306
  "Get memory IDs from search_memories() or get_memories() results.\n"
1234
- "Example: find_path(from_id=\"480c1f76-...\", to_id=\"300d9716-...\")"
1307
+ 'Example: find_path(from_id="480c1f76-...", to_id="300d9716-...")'
1235
1308
  )
1236
-
1309
+
1237
1310
  if not isinstance(from_id, str):
1238
1311
  raise ToolError(
1239
1312
  f"The 'from_id' parameter must be a string, but got {type(from_id).__name__}.\n"
1240
1313
  f"Received: {repr(from_id)}\n"
1241
- "Example: find_path(from_id=\"480c1f76-...\", to_id=\"300d9716-...\")"
1314
+ 'Example: find_path(from_id="480c1f76-...", to_id="300d9716-...")'
1242
1315
  )
1243
-
1316
+
1244
1317
  from_id_str = from_id.strip()
1245
1318
  if not from_id_str:
1246
1319
  raise ToolError(
1247
1320
  "The 'from_id' parameter cannot be empty or whitespace-only.\n"
1248
1321
  "Get memory IDs from search_memories() or get_memories() results.\n"
1249
- "Example: find_path(from_id=\"480c1f76-bcdf-4491-8781-24510db992e3\", to_id=\"300d9716-...\")"
1322
+ 'Example: find_path(from_id="480c1f76-bcdf-4491-8781-24510db992e3", to_id="300d9716-...")'
1250
1323
  )
1251
-
1324
+
1252
1325
  if to_id is None:
1253
1326
  raise ToolError(
1254
1327
  "The 'to_id' parameter is required but was not provided.\n"
1255
1328
  "Get memory IDs from search_memories() or get_memories() results.\n"
1256
- "Example: find_path(from_id=\"480c1f76-...\", to_id=\"300d9716-...\")"
1329
+ 'Example: find_path(from_id="480c1f76-...", to_id="300d9716-...")'
1257
1330
  )
1258
-
1331
+
1259
1332
  if not isinstance(to_id, str):
1260
1333
  raise ToolError(
1261
1334
  f"The 'to_id' parameter must be a string, but got {type(to_id).__name__}.\n"
1262
1335
  f"Received: {repr(to_id)}\n"
1263
- "Example: find_path(from_id=\"480c1f76-...\", to_id=\"300d9716-...\")"
1336
+ 'Example: find_path(from_id="480c1f76-...", to_id="300d9716-...")'
1264
1337
  )
1265
-
1338
+
1266
1339
  to_id_str = to_id.strip()
1267
1340
  if not to_id_str:
1268
1341
  raise ToolError(
1269
1342
  "The 'to_id' parameter cannot be empty or whitespace-only.\n"
1270
1343
  "Get memory IDs from search_memories() or get_memories() results.\n"
1271
- "Example: find_path(from_id=\"480c1f76-...\", to_id=\"300d9716-a3a6-44d3-b0f4-b28002a65da8\")"
1344
+ 'Example: find_path(from_id="480c1f76-...", to_id="300d9716-a3a6-44d3-b0f4-b28002a65da8")'
1272
1345
  )
1273
1346
 
1274
1347
  try:
@@ -1285,9 +1358,13 @@ async def find_path(from_id: str, to_id: str) -> str:
1285
1358
  error_detail = e.response.text if e.response else "Unknown error"
1286
1359
  logger.error(f"API error: {e.response.status_code} - {error_detail}")
1287
1360
  if e.response.status_code == 401:
1288
- raise ToolError("Authentication failed. Please login using the 'login' tool or configure your JWT token in the MCP client headers.")
1361
+ raise ToolError(
1362
+ "Authentication failed. Please login using the 'login' tool or configure your JWT token in the MCP client headers."
1363
+ )
1289
1364
  elif e.response.status_code == 404:
1290
- raise ToolError(f"One or both memories not found.\nVerify the memory IDs are correct by searching for them first.")
1365
+ raise ToolError(
1366
+ f"One or both memories not found.\nVerify the memory IDs are correct by searching for them first."
1367
+ )
1291
1368
  raise ToolError(f"Failed to find path: HTTP {e.response.status_code} - {error_detail}")
1292
1369
  except ToolError:
1293
1370
  raise
@@ -1299,36 +1376,36 @@ async def find_path(from_id: str, to_id: str) -> str:
1299
1376
  @mcp.tool()
1300
1377
  async def get_neighborhood(memory_id: str, hops: int = 2) -> str:
1301
1378
  """Get all memories within N hops of a given memory. Use this for deep context and understanding relationships around important memories.
1302
-
1379
+
1303
1380
  Parameters:
1304
1381
  memory_id (str, REQUIRED): Center memory ID to get neighborhood around.
1305
1382
  - Example: "480c1f76-bcdf-4491-8781-24510db992e3"
1306
1383
  - Get memory IDs from search_memories() or get_memories() results
1307
-
1384
+
1308
1385
  hops (int, optional): Number of hops to traverse. Default is 2.
1309
1386
  - Must be between 1 and 5
1310
1387
  - 1 hop = direct connections only
1311
1388
  - 2 hops = direct connections + their connections
1312
1389
  - Example: 2 (default)
1313
1390
  - Example: 3
1314
-
1391
+
1315
1392
  Returns:
1316
1393
  str: Formatted list of memories in the neighborhood with their hop distances.
1317
-
1394
+
1318
1395
  Common Errors and Solutions:
1319
1396
  - Error: "memory_id cannot be empty"
1320
1397
  Solution: Provide a valid memory ID. Example: get_neighborhood(memory_id="480c1f76-...")
1321
-
1398
+
1322
1399
  - Error: "hops must be between 1 and 5"
1323
1400
  Solution: Provide hops between 1 and 5. Example: get_neighborhood(memory_id="...", hops=3)
1324
-
1401
+
1325
1402
  Examples:
1326
1403
  # Get neighborhood with default 2 hops
1327
1404
  get_neighborhood(memory_id="480c1f76-bcdf-4491-8781-24510db992e3")
1328
-
1405
+
1329
1406
  # Get neighborhood with 3 hops
1330
1407
  get_neighborhood(memory_id="480c1f76-bcdf-4491-8781-24510db992e3", hops=3)
1331
-
1408
+
1332
1409
  # Get direct connections only (1 hop)
1333
1410
  get_neighborhood(memory_id="480c1f76-bcdf-4491-8781-24510db992e3", hops=1)
1334
1411
  """
@@ -1337,36 +1414,36 @@ async def get_neighborhood(memory_id: str, hops: int = 2) -> str:
1337
1414
  raise ToolError(
1338
1415
  "The 'memory_id' parameter is required but was not provided.\n"
1339
1416
  "Get memory IDs from search_memories() or get_memories() results.\n"
1340
- "Example: get_neighborhood(memory_id=\"480c1f76-bcdf-4491-8781-24510db992e3\")"
1417
+ 'Example: get_neighborhood(memory_id="480c1f76-bcdf-4491-8781-24510db992e3")'
1341
1418
  )
1342
-
1419
+
1343
1420
  if not isinstance(memory_id, str):
1344
1421
  raise ToolError(
1345
1422
  f"The 'memory_id' parameter must be a string, but got {type(memory_id).__name__}.\n"
1346
1423
  f"Received: {repr(memory_id)}\n"
1347
- "Example: get_neighborhood(memory_id=\"480c1f76-bcdf-4491-8781-24510db992e3\")"
1424
+ 'Example: get_neighborhood(memory_id="480c1f76-bcdf-4491-8781-24510db992e3")'
1348
1425
  )
1349
-
1426
+
1350
1427
  memory_id_str = memory_id.strip()
1351
1428
  if not memory_id_str:
1352
1429
  raise ToolError(
1353
1430
  "The 'memory_id' parameter cannot be empty or whitespace-only.\n"
1354
1431
  "Get memory IDs from search_memories() or get_memories() results.\n"
1355
- "Example: get_neighborhood(memory_id=\"480c1f76-bcdf-4491-8781-24510db992e3\")"
1432
+ 'Example: get_neighborhood(memory_id="480c1f76-bcdf-4491-8781-24510db992e3")'
1356
1433
  )
1357
-
1434
+
1358
1435
  if not isinstance(hops, int):
1359
1436
  raise ToolError(
1360
1437
  f"The 'hops' parameter must be an integer, but got {type(hops).__name__}.\n"
1361
1438
  f"Received: {repr(hops)}\n"
1362
- "Example: get_neighborhood(memory_id=\"...\", hops=2)"
1439
+ 'Example: get_neighborhood(memory_id="...", hops=2)'
1363
1440
  )
1364
-
1441
+
1365
1442
  if not (1 <= hops <= 5):
1366
1443
  raise ToolError(
1367
1444
  f"The 'hops' parameter must be between 1 and 5, but got {hops}.\n"
1368
- "Example: get_neighborhood(memory_id=\"...\", hops=2)\n"
1369
- "Example: get_neighborhood(memory_id=\"...\", hops=3)"
1445
+ 'Example: get_neighborhood(memory_id="...", hops=2)\n'
1446
+ 'Example: get_neighborhood(memory_id="...", hops=3)'
1370
1447
  )
1371
1448
 
1372
1449
  try:
@@ -1383,10 +1460,16 @@ async def get_neighborhood(memory_id: str, hops: int = 2) -> str:
1383
1460
  error_detail = e.response.text if e.response else "Unknown error"
1384
1461
  logger.error(f"API error: {e.response.status_code} - {error_detail}")
1385
1462
  if e.response.status_code == 401:
1386
- raise ToolError("Authentication failed. Please login using the 'login' tool or configure your JWT token in the MCP client headers.")
1463
+ raise ToolError(
1464
+ "Authentication failed. Please login using the 'login' tool or configure your JWT token in the MCP client headers."
1465
+ )
1387
1466
  elif e.response.status_code == 404:
1388
- raise ToolError(f"Memory not found: {memory_id_str}\nVerify the memory_id is correct by searching for it first.")
1389
- raise ToolError(f"Failed to get neighborhood: HTTP {e.response.status_code} - {error_detail}")
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
+ )
1390
1473
  except ToolError:
1391
1474
  raise
1392
1475
  except Exception as e:
@@ -1398,39 +1481,42 @@ async def get_neighborhood(memory_id: str, hops: int = 2) -> str:
1398
1481
  # HELPER FUNCTIONS
1399
1482
  # ============================================================================
1400
1483
 
1484
+
1401
1485
  def _format_memory(memory: Optional[Dict[str, Any]]) -> str:
1402
1486
  """Format a single memory for display."""
1403
1487
  if not memory:
1404
1488
  return "Memory data not available"
1405
-
1489
+
1406
1490
  lines = [
1407
1491
  f"ID: {memory.get('id', 'unknown')}",
1408
1492
  f"Content: {memory.get('content', '')[:200]}",
1409
1493
  f"Tags: {', '.join(memory.get('tags', []))}",
1410
1494
  f"Context: {memory.get('context', 'N/A')}",
1411
- f"Links: {len(memory.get('links', []))} connections"
1495
+ f"Links: {len(memory.get('links', []))} connections",
1412
1496
  ]
1413
-
1497
+
1414
1498
  # Add evolution history if available
1415
- evolution_history = memory.get('evolution_history', [])
1499
+ evolution_history = memory.get("evolution_history", [])
1416
1500
  if evolution_history:
1417
1501
  lines.append(f"Evolution History: {len(evolution_history)} version(s)")
1418
1502
  # Show current version first
1419
- current_content = memory.get('content', '')
1503
+ current_content = memory.get("content", "")
1420
1504
  lines.append(f" Current Version: {current_content}")
1421
1505
  lines.append("")
1422
1506
  # Show historical versions (oldest to newest)
1423
1507
  for i, entry in enumerate(evolution_history, 1):
1424
- if entry.get('type') == 'content_update':
1425
- old_content = entry.get('old_content', '')
1426
- timestamp = entry.get('timestamp', 'unknown')
1508
+ if entry.get("type") == "content_update":
1509
+ old_content = entry.get("old_content", "")
1510
+ timestamp = entry.get("timestamp", "unknown")
1427
1511
  lines.append(f" Version {i} ({timestamp}): {old_content}")
1428
- elif entry.get('type') == 'evolution':
1429
- old_context = entry.get('old_context', 'N/A')
1430
- new_context = entry.get('new_context', 'N/A')
1431
- timestamp = entry.get('timestamp', 'unknown')
1432
- lines.append(f" Evolution {i} ({timestamp}): Context '{old_context}' → '{new_context}'")
1433
-
1512
+ elif entry.get("type") == "evolution":
1513
+ old_context = entry.get("old_context", "N/A")
1514
+ new_context = entry.get("new_context", "N/A")
1515
+ timestamp = entry.get("timestamp", "unknown")
1516
+ lines.append(
1517
+ f" Evolution {i} ({timestamp}): Context '{old_context}' → '{new_context}'"
1518
+ )
1519
+
1434
1520
  return "\n".join(lines)
1435
1521
 
1436
1522
 
@@ -1438,7 +1524,7 @@ def _format_memories_list(memories: List[Dict[str, Any]]) -> str:
1438
1524
  """Format a list of memories for display."""
1439
1525
  if not memories:
1440
1526
  return "No memories found"
1441
-
1527
+
1442
1528
  formatted = []
1443
1529
  for i, mem in enumerate(memories, 1):
1444
1530
  formatted.append(f"{i}. {_format_memory(mem)}")
@@ -1449,7 +1535,7 @@ def _format_search_results(results: List[Dict[str, Any]]) -> str:
1449
1535
  """Format search results for display."""
1450
1536
  if not results:
1451
1537
  return "No results found"
1452
-
1538
+
1453
1539
  formatted = []
1454
1540
  for i, result in enumerate(results, 1):
1455
1541
  if result.get("type") == "memory_node":
@@ -1460,32 +1546,32 @@ def _format_search_results(results: List[Dict[str, Any]]) -> str:
1460
1546
  formatted.append(f" Related: {len(related)} memories")
1461
1547
  elif result.get("type") == "relationship_edge":
1462
1548
  formatted.append(f"{i}. Relationship Edge (score: {result.get('score', 0):.3f})")
1463
- source = result.get('source', {})
1464
- target = result.get('target', {})
1465
-
1549
+ source = result.get("source", {})
1550
+ target = result.get("target", {})
1551
+
1466
1552
  # Show source node data
1467
1553
  formatted.append(f" Source Node:")
1468
1554
  formatted.append(f" ID: {source.get('id', 'unknown')}")
1469
1555
  formatted.append(f" Content: {source.get('content', 'N/A')}")
1470
- if source.get('context') and source.get('context') != 'General':
1556
+ if source.get("context") and source.get("context") != "General":
1471
1557
  formatted.append(f" Context: {source.get('context', 'N/A')}")
1472
- if source.get('tags'):
1558
+ if source.get("tags"):
1473
1559
  formatted.append(f" Tags: {', '.join(source.get('tags', []))}")
1474
- if source.get('keywords'):
1560
+ if source.get("keywords"):
1475
1561
  formatted.append(f" Keywords: {', '.join(source.get('keywords', []))}")
1476
-
1562
+
1477
1563
  # Show target node data
1478
1564
  formatted.append(f" Target Node:")
1479
1565
  formatted.append(f" ID: {target.get('id', 'unknown')}")
1480
1566
  formatted.append(f" Content: {target.get('content', 'N/A')}")
1481
- if target.get('context') and target.get('context') != 'General':
1567
+ if target.get("context") and target.get("context") != "General":
1482
1568
  formatted.append(f" Context: {target.get('context', 'N/A')}")
1483
- if target.get('tags'):
1569
+ if target.get("tags"):
1484
1570
  formatted.append(f" Tags: {', '.join(target.get('tags', []))}")
1485
- if target.get('keywords'):
1571
+ if target.get("keywords"):
1486
1572
  formatted.append(f" Keywords: {', '.join(target.get('keywords', []))}")
1487
1573
  formatted.append("")
1488
-
1574
+
1489
1575
  return "\n".join(formatted)
1490
1576
 
1491
1577
 
@@ -1493,10 +1579,7 @@ def _format_search_results(results: List[Dict[str, Any]]) -> str:
1493
1579
  @mcp.custom_route("/health", methods=["GET"])
1494
1580
  async def health_check(request: Request) -> JSONResponse:
1495
1581
  """Health check endpoint for load balancers."""
1496
- return JSONResponse(
1497
- content={"status": "healthy", "service": "mem-brain-mcp"},
1498
- status_code=200
1499
- )
1582
+ return JSONResponse(content={"status": "healthy", "service": "mem-brain-mcp"}, status_code=200)
1500
1583
 
1501
1584
 
1502
1585
  def _mask_api_url(url: str) -> str:
@@ -1515,14 +1598,16 @@ def _mask_api_url(url: str) -> str:
1515
1598
 
1516
1599
  def run_server():
1517
1600
  """Run the FastMCP server with HTTP transport."""
1518
- logger.info(f"Starting Mem-Brain MCP Server v{__version__} on {settings.mcp_server_host}:{settings.mcp_server_port}")
1601
+ logger.info(
1602
+ f"Starting Mem-Brain MCP Server v{__version__} on {settings.mcp_server_host}:{settings.mcp_server_port}"
1603
+ )
1519
1604
  logger.info(f"API URL: {_mask_api_url(settings.api_url)}")
1520
1605
  logger.info(f"API Key: {'***' if settings.api_key else 'Not set'}")
1521
-
1606
+
1522
1607
  # Configure CORS for browser-based and MCP clients
1523
1608
  from starlette.middleware import Middleware
1524
1609
  from starlette.middleware.cors import CORSMiddleware
1525
-
1610
+
1526
1611
  middleware = [
1527
1612
  Middleware(
1528
1613
  CORSMiddleware,
@@ -1538,11 +1623,11 @@ def run_server():
1538
1623
  expose_headers=["mcp-session-id"],
1539
1624
  )
1540
1625
  ]
1541
-
1626
+
1542
1627
  # Use http_app with CORS middleware and run with uvicorn
1543
1628
  # Note: http_app() handles the /mcp path automatically
1544
1629
  app = mcp.http_app(middleware=middleware, path="/mcp")
1545
-
1630
+
1546
1631
  # Import uvicorn (should be available via FastMCP dependencies)
1547
1632
  try:
1548
1633
  import uvicorn
@@ -1553,10 +1638,10 @@ def run_server():
1553
1638
  transport="http",
1554
1639
  host=settings.mcp_server_host,
1555
1640
  port=settings.mcp_server_port,
1556
- path="/mcp"
1641
+ path="/mcp",
1557
1642
  )
1558
1643
  return
1559
-
1644
+
1560
1645
  uvicorn.run(
1561
1646
  app,
1562
1647
  host=settings.mcp_server_host,