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