mem-brain-mcp 1.0.1__py3-none-any.whl → 1.0.3__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/__init__.py CHANGED
@@ -1,4 +1,4 @@
1
1
  """Mem-Brain MCP Server - Exposes Mem-Brain API as MCP tools."""
2
2
 
3
- __version__ = "1.0.0"
3
+ __version__ = "1.0.3"
4
4
 
mem_brain_mcp/server.py CHANGED
@@ -1,5 +1,6 @@
1
1
  """MCP Server for Mem-Brain API using FastMCP."""
2
2
 
3
+ import json
3
4
  import logging
4
5
  from typing import Any, Dict, List, Optional, Union
5
6
  import httpx
@@ -380,112 +381,6 @@ async def refresh_context() -> PromptMessage:
380
381
  # TOOLS (Operations)
381
382
  # ============================================================================
382
383
 
383
- @mcp.tool()
384
- async def login(email: str, password: str) -> str:
385
- """Login to mem-brain API and get JWT token. Store the token for subsequent requests.
386
-
387
- Args:
388
- email: User email address
389
- password: User password
390
-
391
- Returns:
392
- Success message with instructions for using the token
393
- """
394
- try:
395
- import httpx
396
- async with httpx.AsyncClient(timeout=30.0) as client:
397
- response = await client.post(
398
- f"{settings.api_base_url}/api/v1/auth/login",
399
- json={"email": email, "password": password},
400
- headers={"Content-Type": "application/json"}
401
- )
402
- response.raise_for_status()
403
- data = response.json()
404
-
405
- access_token = data.get("access_token")
406
- refresh_token = data.get("refresh_token")
407
-
408
- if access_token:
409
- # Store tokens in context (in a real implementation, you'd use FastMCP context)
410
- logger.info(f"Login successful for {email}")
411
- return f"""Login successful!
412
-
413
- Access Token: {access_token[:50]}...
414
-
415
- Use this token in the Authorization header for subsequent requests:
416
- Authorization: Bearer {access_token}
417
-
418
- The token will be automatically used for all memory operations.
419
- Refresh token saved for automatic token renewal."""
420
- else:
421
- raise ToolError("Login failed: No token received")
422
- except httpx.HTTPStatusError as e:
423
- if e.response.status_code == 401:
424
- raise ToolError("Login failed: Invalid email or password")
425
- logger.error(f"Login error: {e.response.status_code} - {e.response.text}")
426
- raise ToolError(f"Login failed: {e.response.status_code}")
427
- except Exception as e:
428
- logger.error(f"Unexpected login error: {e}", exc_info=True)
429
- raise ToolError(f"Login failed: {str(e)}")
430
-
431
-
432
- @mcp.tool()
433
- async def signup(email: str, password: str, full_name: str, organization_name: str) -> str:
434
- """Sign up for a new mem-brain account and organization.
435
-
436
- Args:
437
- email: User email address
438
- password: User password (min 8 chars, must contain letters and numbers)
439
- full_name: User's full name
440
- organization_name: Name for your organization
441
-
442
- Returns:
443
- Success message with access token
444
- """
445
- try:
446
- import httpx
447
- async with httpx.AsyncClient(timeout=30.0) as client:
448
- response = await client.post(
449
- f"{settings.api_base_url}/api/v1/auth/signup",
450
- json={
451
- "email": email,
452
- "password": password,
453
- "full_name": full_name,
454
- "organization_name": organization_name
455
- },
456
- headers={"Content-Type": "application/json"}
457
- )
458
- response.raise_for_status()
459
- data = response.json()
460
-
461
- access_token = data.get("access_token")
462
-
463
- if access_token:
464
- logger.info(f"Signup successful for {email}")
465
- return f"""Account created successfully!
466
-
467
- Organization: {organization_name}
468
- Email: {email}
469
-
470
- Access Token: {access_token[:50]}...
471
-
472
- Use this token in the Authorization header for subsequent requests:
473
- Authorization: Bearer {access_token}
474
-
475
- The token will be automatically used for all memory operations."""
476
- else:
477
- raise ToolError("Signup failed: No token received")
478
- except httpx.HTTPStatusError as e:
479
- error_detail = e.response.text if e.response else "Unknown error"
480
- logger.error(f"Signup error: {e.response.status_code} - {error_detail}")
481
- if e.response.status_code == 400:
482
- raise ToolError(f"Signup failed: {error_detail}")
483
- raise ToolError(f"Signup failed: {e.response.status_code}")
484
- except Exception as e:
485
- logger.error(f"Unexpected signup error: {e}", exc_info=True)
486
- raise ToolError(f"Signup failed: {str(e)}")
487
-
488
-
489
384
  @mcp.tool()
490
385
  async def get_agent_instructions(include_dynamic_context: bool = True) -> str:
491
386
  """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."""
@@ -508,53 +403,128 @@ async def add_memory(
508
403
  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.
509
404
 
510
405
  Parameters:
511
- content (str, required): The memory content to store. Must be a non-empty string. Example: "User prefers Python over JavaScript"
512
- tags (list[str], optional): List of tags to categorize the memory. Example: ["coding", "preferences"] or None
513
- category (str, optional): Category name for the memory. Example: "interests" or None
406
+ content (str, REQUIRED): The memory content to store. Must be a non-empty string.
407
+ - Cannot be None, empty string, or whitespace-only
408
+ - Example: "User prefers Python over JavaScript"
409
+ - Example: "User prefers dark mode interfaces"
410
+
411
+ tags (list[str] or str, optional): Tags to categorize the memory.
412
+ - Can be None (default), a list of strings, a comma-separated string, or a JSON array string
413
+ - If omitted, the system will auto-generate tags based on content
414
+ - Example: ["coding", "preferences"]
415
+ - Example: "coding,preferences" (comma-separated)
416
+ - Example: '["coding", "preferences"]' (JSON string)
417
+ - Note: The system auto-generates relevant tags, so providing tags is optional
418
+
419
+ category (str, optional): Category name for the memory.
420
+ - Can be None (default) or a non-empty string
421
+ - Example: "interests"
422
+ - Example: "preferences"
514
423
 
515
424
  Returns:
516
425
  str: A formatted string with the memory ID and details of the created memory.
517
426
 
427
+ Common Errors and Solutions:
428
+ - Error: "Tool call arguments for mcp were invalid"
429
+ Solution: Ensure 'content' parameter is provided as a string. Example: add_memory(content="User prefers dark mode")
430
+
431
+ - Error: "The 'content' parameter cannot be empty"
432
+ Solution: Provide non-empty content. Example: add_memory(content="User loves Python programming")
433
+
434
+ - Error: "tags must be a list"
435
+ Solution: Pass tags as a list. Example: add_memory(content="...", tags=["coding"]) not tags="coding"
436
+
518
437
  Example workflow:
519
438
  1. search_memories(query="User prefers Python") # Check for existing memories
520
439
  2. If no similar memory found, then: add_memory(content="User prefers Python over JavaScript", tags=["coding", "preferences"])
521
440
 
522
- Example:
441
+ Examples:
442
+ # Basic usage (required parameter only)
443
+ add_memory(content="User prefers dark mode")
444
+
445
+ # With tags
446
+ add_memory(content="User loves Python programming", tags=["coding", "preferences"])
447
+
448
+ # With tags and category
523
449
  add_memory(
524
450
  content="User loves working with TypeScript",
525
451
  tags=["coding", "typescript"],
526
452
  category="interests"
527
453
  )
454
+
455
+ # Tags as empty list (treated as None)
456
+ add_memory(content="User prefers coffee", tags=[])
528
457
  """
529
458
  # Validate parameters with detailed error messages
530
459
  if content is None:
531
- raise ToolError("The 'content' parameter is required but was not provided. Please provide the memory content as a string.")
460
+ raise ToolError(
461
+ "The 'content' parameter is required but was not provided.\n"
462
+ "Example: add_memory(content=\"User prefers dark mode\")\n"
463
+ "Example: add_memory(content=\"User loves Python programming\", tags=[\"coding\"])"
464
+ )
532
465
 
533
466
  if not isinstance(content, str):
534
- raise ToolError(f"The 'content' parameter must be a string, but got {type(content).__name__}. Please provide the memory content as a string.")
467
+ raise ToolError(
468
+ f"The 'content' parameter must be a string, but got {type(content).__name__}.\n"
469
+ f"Received: {repr(content)}\n"
470
+ "Example: add_memory(content=\"User prefers dark mode\")"
471
+ )
535
472
 
536
473
  content_str = str(content).strip()
537
474
  if not content_str:
538
- raise ToolError("The 'content' parameter cannot be empty. Please provide the memory content as a non-empty string.")
475
+ raise ToolError(
476
+ "The 'content' parameter cannot be empty or whitespace-only.\n"
477
+ "Please provide a non-empty string with actual content.\n"
478
+ "Example: add_memory(content=\"User prefers dark mode\")\n"
479
+ "Example: add_memory(content=\"User loves Python programming\")"
480
+ )
539
481
 
540
482
  try:
541
483
  logger.info(f"add_memory called - content length: {len(content_str)}, tags: {tags}, category: {category}")
542
484
  logger.debug(f"add_memory full content: {content_str[:100]}...")
543
485
 
544
- # Normalize tags: convert empty list to None, ensure it's a list if provided
486
+ # Normalize tags: handle various input formats and convert to list of strings
545
487
  normalized_tags = None
546
488
  if tags is not None:
547
489
  if isinstance(tags, list):
490
+ # Validate list contents are strings
491
+ if tags:
492
+ invalid_items = [item for item in tags if not isinstance(item, str)]
493
+ if invalid_items:
494
+ raise ToolError(
495
+ f"The 'tags' parameter must be a list of strings, but found non-string items: {invalid_items}\n"
496
+ f"Example: add_memory(content=\"...\", tags=[\"coding\", \"preferences\"])\n"
497
+ f"Example: add_memory(content=\"...\", tags=[\"personal\", \"pets\"])"
498
+ )
548
499
  normalized_tags = tags if tags else None # Empty list becomes None
549
500
  elif isinstance(tags, str):
550
- # Handle case where tags might be passed as a single string
551
- normalized_tags = [tags]
552
- else:
553
- try:
554
- normalized_tags = list(tags) if tags else None
555
- except (TypeError, ValueError):
556
- logger.warning(f"Could not convert tags to list: {tags}")
501
+ tags_str = tags.strip()
502
+ if not tags_str:
557
503
  normalized_tags = None
504
+ else:
505
+ # Try to parse as JSON array first (e.g., '["tag1", "tag2"]')
506
+ try:
507
+ parsed = json.loads(tags_str)
508
+ if isinstance(parsed, list):
509
+ normalized_tags = [str(item).strip() for item in parsed if str(item).strip()]
510
+ else:
511
+ # If JSON but not a list, treat as single tag
512
+ normalized_tags = [tags_str]
513
+ except (json.JSONDecodeError, ValueError):
514
+ # Not JSON, try comma-separated string
515
+ if ',' in tags_str:
516
+ normalized_tags = [tag.strip() for tag in tags_str.split(',') if tag.strip()]
517
+ else:
518
+ # Single tag string
519
+ normalized_tags = [tags_str]
520
+ else:
521
+ raise ToolError(
522
+ f"The 'tags' parameter must be a list of strings, a comma-separated string, or None, but got {type(tags).__name__}.\n"
523
+ f"Received: {repr(tags)}\n"
524
+ "Example: add_memory(content=\"...\", tags=[\"coding\", \"preferences\"])\n"
525
+ "Example: add_memory(content=\"...\", tags=\"coding,preferences\")\n"
526
+ "Example: add_memory(content=\"...\", tags=None) # or omit tags parameter"
527
+ )
558
528
 
559
529
  # Normalize category: convert empty string to None
560
530
  normalized_category = category.strip() if category and isinstance(category, str) and category.strip() else None
@@ -586,49 +556,188 @@ async def add_memory(
586
556
 
587
557
  @mcp.tool()
588
558
  async def search_memories(query: str, k: int = 5) -> str:
589
- """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."""
590
- # Validate parameters
591
- if not query or not query.strip():
592
- raise ToolError("Query cannot be empty")
559
+ """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.
560
+
561
+ Parameters:
562
+ query (str, REQUIRED): Search query string. Use natural language questions, not keywords.
563
+ - Example: "Who is Rakshith and what did he build?"
564
+ - Example: "What are the user's preferences for programming languages?"
565
+ - Example: "Tell me about memories related to the Dubai presentation"
566
+
567
+ k (int, optional): Number of results to return. Default is 5.
568
+ - Must be between 1 and 100
569
+ - Example: 5 (default)
570
+ - Example: 10
571
+
572
+ Returns:
573
+ str: Formatted search results with memory nodes and relationship edges.
574
+
575
+ Common Errors and Solutions:
576
+ - Error: "Query cannot be empty"
577
+ Solution: Provide a non-empty search query. Example: search_memories(query="What is the user's name?")
578
+
579
+ - Error: "k must be between 1 and 100"
580
+ Solution: Provide k between 1 and 100. Example: search_memories(query="...", k=10)
581
+
582
+ Examples:
583
+ # Basic search
584
+ search_memories(query="Who is Rakshith?")
585
+
586
+ # Search with more results
587
+ search_memories(query="What are the user's programming preferences?", k=10)
588
+
589
+ # Complex query
590
+ search_memories(query="Tell me about memories related to mem-brain and its features")
591
+ """
592
+ # Validate parameters with detailed error messages
593
+ if query is None:
594
+ raise ToolError(
595
+ "The 'query' parameter is required but was not provided.\n"
596
+ "Example: search_memories(query=\"Who is Rakshith?\")\n"
597
+ "Example: search_memories(query=\"What are the user's preferences?\")"
598
+ )
599
+
600
+ if not isinstance(query, str):
601
+ raise ToolError(
602
+ f"The 'query' parameter must be a string, but got {type(query).__name__}.\n"
603
+ f"Received: {repr(query)}\n"
604
+ "Example: search_memories(query=\"Who is Rakshith?\")"
605
+ )
606
+
607
+ query_str = query.strip()
608
+ if not query_str:
609
+ raise ToolError(
610
+ "The 'query' parameter cannot be empty or whitespace-only.\n"
611
+ "Provide a natural language question or search query.\n"
612
+ "Example: search_memories(query=\"Who is Rakshith?\")\n"
613
+ "Example: search_memories(query=\"What are the user's preferences?\")"
614
+ )
615
+
616
+ if not isinstance(k, int):
617
+ raise ToolError(
618
+ f"The 'k' parameter must be an integer, but got {type(k).__name__}.\n"
619
+ f"Received: {repr(k)}\n"
620
+ "Example: search_memories(query=\"...\", k=10)"
621
+ )
622
+
593
623
  if not (1 <= k <= 100):
594
- raise ToolError("k must be between 1 and 100")
624
+ raise ToolError(
625
+ f"The 'k' parameter must be between 1 and 100, but got {k}.\n"
626
+ "Example: search_memories(query=\"...\", k=5)\n"
627
+ "Example: search_memories(query=\"...\", k=10)"
628
+ )
595
629
 
596
630
  try:
631
+ logger.info(f"search_memories called - query length: {len(query_str)}, k: {k}")
597
632
  client = await _get_api_client()
598
- result = await client.search_memories(query, k)
633
+ result = await client.search_memories(query_str, k)
599
634
  return f"Found {result.get('count', 0)} results:\n{_format_search_results(result.get('results', []))}"
600
635
  except httpx.HTTPStatusError as e:
636
+ error_detail = e.response.text if e.response else "Unknown error"
637
+ logger.error(f"API error: {e.response.status_code} - {error_detail}")
601
638
  if e.response.status_code == 401:
602
- raise ToolError("Authentication failed. Please check your API key.")
603
- logger.error(f"API error: {e.response.status_code} - {e.response.text}")
604
- raise ToolError(f"Failed to search memories: {e.response.status_code}")
639
+ raise ToolError("Authentication failed. Please login using the 'login' tool or configure your JWT token in the MCP client headers.")
640
+ raise ToolError(f"Failed to search memories: HTTP {e.response.status_code} - {error_detail}")
641
+ except ToolError:
642
+ raise
605
643
  except Exception as e:
606
- logger.error(f"Unexpected error: {e}", exc_info=True)
644
+ logger.error(f"Unexpected error in search_memories: {e}", exc_info=True)
607
645
  raise ToolError(f"Error searching memories: {str(e)}")
608
646
 
609
647
 
610
648
  @mcp.tool()
611
649
  async def get_memories(memory_ids: List[str]) -> str:
612
- """Retrieve one or more memories by ID. Use this when you need full details for specific memories identified from search results."""
613
- # Validate parameters
650
+ """Retrieve one or more memories by ID. Use this when you need full details for specific memories identified from search results.
651
+
652
+ Parameters:
653
+ memory_ids (list[str], REQUIRED): List of memory IDs to retrieve. Must be a non-empty list.
654
+ - Example: ["480c1f76-bcdf-4491-8781-24510db992e3"]
655
+ - Example: ["480c1f76-...", "300d9716-...", "6fb6b23f-..."]
656
+ - Get memory IDs from search_memories() results
657
+
658
+ Returns:
659
+ str: Formatted details of the retrieved memories.
660
+
661
+ Common Errors and Solutions:
662
+ - Error: "memory_ids cannot be empty"
663
+ Solution: Provide a list with at least one memory ID. Example: get_memories(memory_ids=["480c1f76-..."])
664
+
665
+ - Error: "Memory IDs cannot be empty"
666
+ Solution: Ensure all IDs in the list are non-empty strings. Example: get_memories(memory_ids=["480c1f76-..."])
667
+
668
+ - Error: "memory_ids must be a list"
669
+ Solution: Pass memory_ids as a list. Example: get_memories(memory_ids=["..."]) not memory_ids="..."
670
+
671
+ Examples:
672
+ # Get single memory
673
+ get_memories(memory_ids=["480c1f76-bcdf-4491-8781-24510db992e3"])
674
+
675
+ # Get multiple memories
676
+ get_memories(memory_ids=["480c1f76-...", "300d9716-...", "6fb6b23f-..."])
677
+ """
678
+ # Validate parameters with detailed error messages
679
+ if memory_ids is None:
680
+ raise ToolError(
681
+ "The 'memory_ids' parameter is required but was not provided.\n"
682
+ "Example: get_memories(memory_ids=[\"480c1f76-bcdf-4491-8781-24510db992e3\"])\n"
683
+ "Example: get_memories(memory_ids=[\"480c1f76-...\", \"300d9716-...\"])"
684
+ )
685
+
686
+ if not isinstance(memory_ids, list):
687
+ raise ToolError(
688
+ f"The 'memory_ids' parameter must be a list of strings, but got {type(memory_ids).__name__}.\n"
689
+ f"Received: {repr(memory_ids)}\n"
690
+ "Example: get_memories(memory_ids=[\"480c1f76-...\"])"
691
+ )
692
+
614
693
  if not memory_ids:
615
- raise ToolError("memory_ids cannot be empty")
616
- for memory_id in memory_ids:
617
- if not memory_id or not memory_id.strip():
618
- raise ToolError("Memory IDs cannot be empty")
694
+ raise ToolError(
695
+ "The 'memory_ids' parameter cannot be an empty list.\n"
696
+ "Provide at least one memory ID.\n"
697
+ "Example: get_memories(memory_ids=[\"480c1f76-bcdf-4491-8781-24510db992e3\"])"
698
+ )
699
+
700
+ # Validate each memory ID in the list
701
+ validated_ids = []
702
+ for i, memory_id in enumerate(memory_ids):
703
+ if memory_id is None:
704
+ raise ToolError(
705
+ f"Memory ID at index {i} is None. All memory IDs must be non-empty strings.\n"
706
+ "Example: get_memories(memory_ids=[\"480c1f76-...\"])"
707
+ )
708
+ if not isinstance(memory_id, str):
709
+ raise ToolError(
710
+ f"Memory ID at index {i} must be a string, but got {type(memory_id).__name__}.\n"
711
+ f"Received: {repr(memory_id)}\n"
712
+ "Example: get_memories(memory_ids=[\"480c1f76-...\"])"
713
+ )
714
+ memory_id_str = memory_id.strip()
715
+ if not memory_id_str:
716
+ raise ToolError(
717
+ f"Memory ID at index {i} cannot be empty or whitespace-only.\n"
718
+ "Get memory IDs from search_memories() or get_memories() results.\n"
719
+ "Example: get_memories(memory_ids=[\"480c1f76-bcdf-4491-8781-24510db992e3\"])"
720
+ )
721
+ validated_ids.append(memory_id_str)
619
722
 
620
723
  try:
724
+ logger.info(f"get_memories called - count: {len(validated_ids)}")
621
725
  client = await _get_api_client()
622
- result = await client.get_memories(memory_ids)
726
+ result = await client.get_memories(validated_ids)
623
727
  memories = result.get("memories", [])
624
728
  return f"Retrieved {len(memories)} memories:\n{_format_memories_list(memories)}"
625
729
  except httpx.HTTPStatusError as e:
730
+ error_detail = e.response.text if e.response else "Unknown error"
731
+ logger.error(f"API error: {e.response.status_code} - {error_detail}")
626
732
  if e.response.status_code == 401:
627
- raise ToolError("Authentication failed. Please check your API key.")
628
- logger.error(f"API error: {e.response.status_code} - {e.response.text}")
629
- raise ToolError(f"Failed to get memories: {e.response.status_code}")
733
+ raise ToolError("Authentication failed. Please login using the 'login' tool or configure your JWT token in the MCP client headers.")
734
+ elif e.response.status_code == 404:
735
+ raise ToolError(f"One or more memories not found.\nVerify the memory IDs are correct by searching for them first.")
736
+ raise ToolError(f"Failed to get memories: HTTP {e.response.status_code} - {error_detail}")
737
+ except ToolError:
738
+ raise
630
739
  except Exception as e:
631
- logger.error(f"Unexpected error: {e}", exc_info=True)
740
+ logger.error(f"Unexpected error in get_memories: {e}", exc_info=True)
632
741
  raise ToolError(f"Error getting memories: {str(e)}")
633
742
 
634
743
 
@@ -638,25 +747,168 @@ async def update_memory(
638
747
  content: Optional[str] = None,
639
748
  tags: Optional[List[str]] = None
640
749
  ) -> str:
641
- """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."""
642
- # Validate parameters
643
- if not memory_id or not memory_id.strip():
644
- raise ToolError("memory_id cannot be empty")
750
+ """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.
751
+
752
+ Parameters:
753
+ memory_id (str, REQUIRED): The ID of the memory to update. Must be a non-empty string.
754
+ - Example: "480c1f76-bcdf-4491-8781-24510db992e3"
755
+ - Get memory IDs from search_memories() or get_memories() results
756
+
757
+ content (str, optional): New content for the memory.
758
+ - Can be None (to keep existing content) or a non-empty string
759
+ - If provided, must not be empty or whitespace-only
760
+ - Example: "User no longer likes TypeScript, prefers Python"
761
+
762
+ tags (list[str] or str, optional): New tags for the memory.
763
+ - Can be None (to keep existing tags), a list of strings, a comma-separated string, or a JSON array string
764
+ - If provided, replaces existing tags
765
+ - Example: ["coding", "python"]
766
+ - Example: "coding,python" (comma-separated)
767
+ - Example: '["coding", "python"]' (JSON string)
768
+ - Note: The system can auto-generate tags if you omit this parameter
769
+
770
+ Returns:
771
+ str: A formatted string with the updated memory details.
772
+
773
+ Common Errors and Solutions:
774
+ - Error: "Tool call arguments for mcp were invalid"
775
+ Solution: Ensure 'memory_id' parameter is provided as a string. Example: update_memory(memory_id="...")
776
+
777
+ - Error: "memory_id cannot be empty"
778
+ Solution: Provide a valid memory ID from search results. Example: update_memory(memory_id="480c1f76-...")
779
+
780
+ - Error: "At least one of 'content' or 'tags' must be provided"
781
+ Solution: Provide content or tags to update. Example: update_memory(memory_id="...", content="New content")
782
+
783
+ Examples:
784
+ # Update content only
785
+ update_memory(memory_id="480c1f76-...", content="User prefers Python over JavaScript")
786
+
787
+ # Update tags only
788
+ update_memory(memory_id="480c1f76-...", tags=["coding", "preferences"])
789
+
790
+ # Update both content and tags
791
+ update_memory(
792
+ memory_id="480c1f76-...",
793
+ content="User no longer likes TypeScript",
794
+ tags=["coding", "python"]
795
+ )
796
+ """
797
+ # Validate parameters with detailed error messages
798
+ if memory_id is None:
799
+ raise ToolError(
800
+ "The 'memory_id' parameter is required but was not provided.\n"
801
+ "Get memory IDs from search_memories() or get_memories() results.\n"
802
+ "Example: update_memory(memory_id=\"480c1f76-bcdf-4491-8781-24510db992e3\", content=\"New content\")"
803
+ )
804
+
805
+ if not isinstance(memory_id, str):
806
+ raise ToolError(
807
+ f"The 'memory_id' parameter must be a string, but got {type(memory_id).__name__}.\n"
808
+ f"Received: {repr(memory_id)}\n"
809
+ "Example: update_memory(memory_id=\"480c1f76-...\", content=\"New content\")"
810
+ )
811
+
812
+ memory_id_str = memory_id.strip()
813
+ if not memory_id_str:
814
+ raise ToolError(
815
+ "The 'memory_id' parameter cannot be empty or whitespace-only.\n"
816
+ "Get memory IDs from search_memories() or get_memories() results.\n"
817
+ "Example: update_memory(memory_id=\"480c1f76-bcdf-4491-8781-24510db992e3\", content=\"New content\")"
818
+ )
819
+
820
+ # Validate that at least one update parameter is provided
821
+ if content is None and tags is None:
822
+ raise ToolError(
823
+ "At least one of 'content' or 'tags' must be provided to update the memory.\n"
824
+ "Example: update_memory(memory_id=\"...\", content=\"New content\")\n"
825
+ "Example: update_memory(memory_id=\"...\", tags=[\"new\", \"tags\"])"
826
+ )
827
+
828
+ # Validate content if provided
829
+ if content is not None:
830
+ if not isinstance(content, str):
831
+ raise ToolError(
832
+ f"The 'content' parameter must be a string or None, but got {type(content).__name__}.\n"
833
+ f"Received: {repr(content)}\n"
834
+ "Example: update_memory(memory_id=\"...\", content=\"New content\")"
835
+ )
836
+ content_str = str(content).strip()
837
+ if not content_str:
838
+ raise ToolError(
839
+ "The 'content' parameter cannot be empty or whitespace-only.\n"
840
+ "Provide a non-empty string or omit the parameter to keep existing content.\n"
841
+ "Example: update_memory(memory_id=\"...\", content=\"New content\")"
842
+ )
843
+ else:
844
+ content_str = None
845
+
846
+ # Validate tags if provided - handle various input formats
847
+ normalized_tags = None
848
+ if tags is not None:
849
+ if isinstance(tags, list):
850
+ # Validate list contents are strings
851
+ if tags:
852
+ invalid_items = [item for item in tags if not isinstance(item, str)]
853
+ if invalid_items:
854
+ raise ToolError(
855
+ f"The 'tags' parameter must be a list of strings, but found non-string items: {invalid_items}\n"
856
+ "Example: update_memory(memory_id=\"...\", tags=[\"coding\", \"preferences\"])\n"
857
+ "Example: update_memory(memory_id=\"...\", tags=None) # or omit tags parameter"
858
+ )
859
+ normalized_tags = tags if tags else None # Empty list becomes None
860
+ elif isinstance(tags, str):
861
+ tags_str = tags.strip()
862
+ if not tags_str:
863
+ normalized_tags = None
864
+ else:
865
+ # Try to parse as JSON array first (e.g., '["tag1", "tag2"]')
866
+ try:
867
+ parsed = json.loads(tags_str)
868
+ if isinstance(parsed, list):
869
+ normalized_tags = [str(item).strip() for item in parsed if str(item).strip()]
870
+ else:
871
+ # If JSON but not a list, treat as single tag
872
+ normalized_tags = [tags_str]
873
+ except (json.JSONDecodeError, ValueError):
874
+ # Not JSON, try comma-separated string
875
+ if ',' in tags_str:
876
+ normalized_tags = [tag.strip() for tag in tags_str.split(',') if tag.strip()]
877
+ else:
878
+ # Single tag string
879
+ normalized_tags = [tags_str]
880
+ else:
881
+ raise ToolError(
882
+ f"The 'tags' parameter must be a list of strings, a comma-separated string, or None, but got {type(tags).__name__}.\n"
883
+ f"Received: {repr(tags)}\n"
884
+ "Example: update_memory(memory_id=\"...\", tags=[\"coding\", \"preferences\"])\n"
885
+ "Example: update_memory(memory_id=\"...\", tags=\"coding,preferences\")\n"
886
+ "Example: update_memory(memory_id=\"...\", tags=None) # or omit tags parameter"
887
+ )
645
888
 
646
889
  try:
890
+ logger.info(f"update_memory called - memory_id: {memory_id_str}, content length: {len(content_str) if content_str else 0}, tags: {normalized_tags}")
891
+
647
892
  client = await _get_api_client()
648
- result = await client.update_memory(memory_id, content, tags)
893
+ result = await client.update_memory(memory_id_str, content_str, normalized_tags)
649
894
  memory = result.get('memory')
650
895
  if memory:
651
896
  return f"Memory updated:\n{_format_memory(memory)}"
652
- return f"Memory {memory_id} updated"
897
+ return f"Memory {memory_id_str} updated"
653
898
  except httpx.HTTPStatusError as e:
899
+ error_detail = e.response.text if e.response else "Unknown error"
900
+ logger.error(f"API error: {e.response.status_code} - {error_detail}")
654
901
  if e.response.status_code == 401:
655
- raise ToolError("Authentication failed. Please check your API key.")
656
- logger.error(f"API error: {e.response.status_code} - {e.response.text}")
657
- raise ToolError(f"Failed to update memory: {e.response.status_code}")
902
+ raise ToolError("Authentication failed. Please login using the 'login' tool or configure your JWT token in the MCP client headers.")
903
+ elif e.response.status_code == 400:
904
+ raise ToolError(f"Invalid request: {error_detail}\nExample: update_memory(memory_id=\"...\", content=\"New content\")")
905
+ elif e.response.status_code == 404:
906
+ raise ToolError(f"Memory not found: {memory_id_str}\nVerify the memory_id is correct by searching for it first.")
907
+ raise ToolError(f"Failed to update memory: HTTP {e.response.status_code} - {error_detail}")
908
+ except ToolError:
909
+ raise
658
910
  except Exception as e:
659
- logger.error(f"Unexpected error: {e}", exc_info=True)
911
+ logger.error(f"Unexpected error in update_memory: {e}", exc_info=True)
660
912
  raise ToolError(f"Error updating memory: {str(e)}")
661
913
 
662
914
 
@@ -666,49 +918,243 @@ async def delete_memories(
666
918
  tags: Optional[str] = None,
667
919
  category: Optional[str] = None
668
920
  ) -> str:
669
- """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."""
921
+ """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.
922
+
923
+ Parameters:
924
+ memory_id (str, optional): Specific memory ID to delete. Takes precedence over filters.
925
+ - Example: "480c1f76-bcdf-4491-8781-24510db992e3"
926
+ - Get memory IDs from search_memories() or get_memories() results
927
+
928
+ tags (str, optional): Comma-separated tags for filter-based deletion.
929
+ - Example: "coding,preferences"
930
+ - Example: "personal,pets"
931
+ - Only used if memory_id is not provided
932
+
933
+ category (str, optional): Category name for filter-based deletion.
934
+ - Example: "interests"
935
+ - Example: "preferences"
936
+ - Only used if memory_id is not provided
937
+
938
+ Returns:
939
+ str: A message indicating how many memories were deleted and their IDs.
940
+
941
+ Common Errors and Solutions:
942
+ - Error: "At least one parameter must be provided"
943
+ Solution: Provide memory_id, tags, or category. Example: delete_memories(memory_id="...")
944
+
945
+ - Error: "memory_id cannot be empty"
946
+ Solution: Provide a valid memory ID or omit the parameter. Example: delete_memories(memory_id="480c1f76-...")
947
+
948
+ Examples:
949
+ # Delete by memory ID
950
+ delete_memories(memory_id="480c1f76-bcdf-4491-8781-24510db992e3")
951
+
952
+ # Delete by tags
953
+ delete_memories(tags="coding,preferences")
954
+
955
+ # Delete by category
956
+ delete_memories(category="interests")
957
+ """
958
+ # Validate that at least one parameter is provided
959
+ if memory_id is None and tags is None and category is None:
960
+ raise ToolError(
961
+ "At least one parameter (memory_id, tags, or category) must be provided to delete memories.\n"
962
+ "Example: delete_memories(memory_id=\"480c1f76-bcdf-4491-8781-24510db992e3\")\n"
963
+ "Example: delete_memories(tags=\"coding,preferences\")\n"
964
+ "Example: delete_memories(category=\"interests\")"
965
+ )
966
+
967
+ # Validate memory_id if provided
968
+ if memory_id is not None:
969
+ if not isinstance(memory_id, str):
970
+ raise ToolError(
971
+ f"The 'memory_id' parameter must be a string or None, but got {type(memory_id).__name__}.\n"
972
+ f"Received: {repr(memory_id)}\n"
973
+ "Example: delete_memories(memory_id=\"480c1f76-bcdf-4491-8781-24510db992e3\")"
974
+ )
975
+ memory_id_str = memory_id.strip()
976
+ if not memory_id_str:
977
+ raise ToolError(
978
+ "The 'memory_id' parameter cannot be empty or whitespace-only.\n"
979
+ "Get memory IDs from search_memories() or get_memories() results.\n"
980
+ "Example: delete_memories(memory_id=\"480c1f76-bcdf-4491-8781-24510db992e3\")"
981
+ )
982
+ else:
983
+ memory_id_str = None
984
+
985
+ # Validate tags if provided
986
+ if tags is not None:
987
+ if not isinstance(tags, str):
988
+ raise ToolError(
989
+ f"The 'tags' parameter must be a string or None, but got {type(tags).__name__}.\n"
990
+ f"Received: {repr(tags)}\n"
991
+ "Example: delete_memories(tags=\"coding,preferences\")"
992
+ )
993
+ tags_str = tags.strip()
994
+ if not tags_str:
995
+ raise ToolError(
996
+ "The 'tags' parameter cannot be empty or whitespace-only.\n"
997
+ "Provide comma-separated tags or omit the parameter.\n"
998
+ "Example: delete_memories(tags=\"coding,preferences\")"
999
+ )
1000
+ else:
1001
+ tags_str = None
1002
+
1003
+ # Validate category if provided
1004
+ if category is not None:
1005
+ if not isinstance(category, str):
1006
+ raise ToolError(
1007
+ f"The 'category' parameter must be a string or None, but got {type(category).__name__}.\n"
1008
+ f"Received: {repr(category)}\n"
1009
+ "Example: delete_memories(category=\"interests\")"
1010
+ )
1011
+ category_str = category.strip()
1012
+ if not category_str:
1013
+ raise ToolError(
1014
+ "The 'category' parameter cannot be empty or whitespace-only.\n"
1015
+ "Provide a category name or omit the parameter.\n"
1016
+ "Example: delete_memories(category=\"interests\")"
1017
+ )
1018
+ else:
1019
+ category_str = None
1020
+
670
1021
  try:
1022
+ logger.info(f"delete_memories called - memory_id: {memory_id_str}, tags: {tags_str}, category: {category_str}")
671
1023
  client = await _get_api_client()
672
- result = await client.delete_memories(memory_id, tags, category)
1024
+ result = await client.delete_memories(memory_id_str, tags_str, category_str)
673
1025
  deleted_ids = result.get('memory_ids', [])[:10]
674
1026
  return f"Deleted {result.get('deleted_count', 0)} memories. IDs: {', '.join(deleted_ids)}"
675
1027
  except httpx.HTTPStatusError as e:
1028
+ error_detail = e.response.text if e.response else "Unknown error"
1029
+ logger.error(f"API error: {e.response.status_code} - {error_detail}")
676
1030
  if e.response.status_code == 401:
677
- raise ToolError("Authentication failed. Please check your API key.")
678
- logger.error(f"API error: {e.response.status_code} - {e.response.text}")
679
- raise ToolError(f"Failed to delete memories: {e.response.status_code}")
1031
+ raise ToolError("Authentication failed. Please login using the 'login' tool or configure your JWT token in the MCP client headers.")
1032
+ elif e.response.status_code == 404:
1033
+ raise ToolError(f"Memory not found: {memory_id_str}\nVerify the memory_id is correct by searching for it first.")
1034
+ raise ToolError(f"Failed to delete memories: HTTP {e.response.status_code} - {error_detail}")
1035
+ except ToolError:
1036
+ raise
680
1037
  except Exception as e:
681
- logger.error(f"Unexpected error: {e}", exc_info=True)
1038
+ logger.error(f"Unexpected error in delete_memories: {e}", exc_info=True)
682
1039
  raise ToolError(f"Error deleting memories: {str(e)}")
683
1040
 
684
1041
 
685
1042
  @mcp.tool()
686
1043
  async def unlink_memories(memory_id_1: str, memory_id_2: str) -> str:
687
- """Remove link between two memories when the connection is no longer relevant or accurate."""
688
- # Validate parameters
689
- if not memory_id_1 or not memory_id_1.strip():
690
- raise ToolError("memory_id_1 cannot be empty")
691
- if not memory_id_2 or not memory_id_2.strip():
692
- raise ToolError("memory_id_2 cannot be empty")
1044
+ """Remove link between two memories when the connection is no longer relevant or accurate.
1045
+
1046
+ Parameters:
1047
+ memory_id_1 (str, REQUIRED): First memory ID in the link to remove.
1048
+ - Example: "480c1f76-bcdf-4491-8781-24510db992e3"
1049
+ - Get memory IDs from search_memories() or get_memories() results
1050
+
1051
+ memory_id_2 (str, REQUIRED): Second memory ID in the link to remove.
1052
+ - Example: "300d9716-a3a6-44d3-b0f4-b28002a65da8"
1053
+ - Get memory IDs from search_memories() or get_memories() results
1054
+
1055
+ Returns:
1056
+ str: Confirmation message that the memories were unlinked.
1057
+
1058
+ Common Errors and Solutions:
1059
+ - Error: "memory_id_1 cannot be empty"
1060
+ Solution: Provide a valid memory ID. Example: unlink_memories(memory_id_1="480c1f76-...", memory_id_2="300d9716-...")
1061
+
1062
+ - Error: "memory_id_2 cannot be empty"
1063
+ Solution: Provide a valid memory ID. Example: unlink_memories(memory_id_1="480c1f76-...", memory_id_2="300d9716-...")
1064
+
1065
+ Examples:
1066
+ # Unlink two memories
1067
+ unlink_memories(
1068
+ memory_id_1="480c1f76-bcdf-4491-8781-24510db992e3",
1069
+ memory_id_2="300d9716-a3a6-44d3-b0f4-b28002a65da8"
1070
+ )
1071
+ """
1072
+ # Validate parameters with detailed error messages
1073
+ if memory_id_1 is None:
1074
+ raise ToolError(
1075
+ "The 'memory_id_1' parameter is required but was not provided.\n"
1076
+ "Get memory IDs from search_memories() or get_memories() results.\n"
1077
+ "Example: unlink_memories(memory_id_1=\"480c1f76-...\", memory_id_2=\"300d9716-...\")"
1078
+ )
1079
+
1080
+ if not isinstance(memory_id_1, str):
1081
+ raise ToolError(
1082
+ f"The 'memory_id_1' parameter must be a string, but got {type(memory_id_1).__name__}.\n"
1083
+ f"Received: {repr(memory_id_1)}\n"
1084
+ "Example: unlink_memories(memory_id_1=\"480c1f76-...\", memory_id_2=\"300d9716-...\")"
1085
+ )
1086
+
1087
+ memory_id_1_str = memory_id_1.strip()
1088
+ if not memory_id_1_str:
1089
+ raise ToolError(
1090
+ "The 'memory_id_1' parameter cannot be empty or whitespace-only.\n"
1091
+ "Get memory IDs from search_memories() or get_memories() results.\n"
1092
+ "Example: unlink_memories(memory_id_1=\"480c1f76-bcdf-4491-8781-24510db992e3\", memory_id_2=\"300d9716-...\")"
1093
+ )
1094
+
1095
+ if memory_id_2 is None:
1096
+ raise ToolError(
1097
+ "The 'memory_id_2' parameter is required but was not provided.\n"
1098
+ "Get memory IDs from search_memories() or get_memories() results.\n"
1099
+ "Example: unlink_memories(memory_id_1=\"480c1f76-...\", memory_id_2=\"300d9716-...\")"
1100
+ )
1101
+
1102
+ if not isinstance(memory_id_2, str):
1103
+ raise ToolError(
1104
+ f"The 'memory_id_2' parameter must be a string, but got {type(memory_id_2).__name__}.\n"
1105
+ f"Received: {repr(memory_id_2)}\n"
1106
+ "Example: unlink_memories(memory_id_1=\"480c1f76-...\", memory_id_2=\"300d9716-...\")"
1107
+ )
1108
+
1109
+ memory_id_2_str = memory_id_2.strip()
1110
+ if not memory_id_2_str:
1111
+ raise ToolError(
1112
+ "The 'memory_id_2' parameter cannot be empty or whitespace-only.\n"
1113
+ "Get memory IDs from search_memories() or get_memories() results.\n"
1114
+ "Example: unlink_memories(memory_id_1=\"480c1f76-...\", memory_id_2=\"300d9716-a3a6-44d3-b0f4-b28002a65da8\")"
1115
+ )
1116
+
1117
+ # Ensure the IDs are different
1118
+ if memory_id_1_str == memory_id_2_str:
1119
+ raise ToolError(
1120
+ "memory_id_1 and memory_id_2 must be different.\n"
1121
+ "You cannot unlink a memory from itself.\n"
1122
+ "Example: unlink_memories(memory_id_1=\"480c1f76-...\", memory_id_2=\"300d9716-...\")"
1123
+ )
693
1124
 
694
1125
  try:
1126
+ logger.info(f"unlink_memories called - memory_id_1: {memory_id_1_str}, memory_id_2: {memory_id_2_str}")
695
1127
  client = await _get_api_client()
696
- result = await client.unlink_memories(memory_id_1, memory_id_2)
1128
+ result = await client.unlink_memories(memory_id_1_str, memory_id_2_str)
697
1129
  return result.get("message", "Memories unlinked")
698
1130
  except httpx.HTTPStatusError as e:
1131
+ error_detail = e.response.text if e.response else "Unknown error"
1132
+ logger.error(f"API error: {e.response.status_code} - {error_detail}")
699
1133
  if e.response.status_code == 401:
700
- raise ToolError("Authentication failed. Please check your API key.")
701
- logger.error(f"API error: {e.response.status_code} - {e.response.text}")
702
- raise ToolError(f"Failed to unlink memories: {e.response.status_code}")
1134
+ raise ToolError("Authentication failed. Please login using the 'login' tool or configure your JWT token in the MCP client headers.")
1135
+ elif e.response.status_code == 404:
1136
+ raise ToolError(f"One or both memories not found.\nVerify the memory IDs are correct by searching for them first.")
1137
+ raise ToolError(f"Failed to unlink memories: HTTP {e.response.status_code} - {error_detail}")
1138
+ except ToolError:
1139
+ raise
703
1140
  except Exception as e:
704
- logger.error(f"Unexpected error: {e}", exc_info=True)
1141
+ logger.error(f"Unexpected error in unlink_memories: {e}", exc_info=True)
705
1142
  raise ToolError(f"Error unlinking memories: {str(e)}")
706
1143
 
707
1144
 
708
1145
  @mcp.tool()
709
1146
  async def get_stats() -> str:
710
- """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."""
1147
+ """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.
1148
+
1149
+ Returns:
1150
+ str: Formatted statistics including total memories, links, and top tags.
1151
+
1152
+ Examples:
1153
+ # Get statistics
1154
+ get_stats()
1155
+ """
711
1156
  try:
1157
+ logger.info("get_stats called")
712
1158
  client = await _get_api_client()
713
1159
  result = await client.get_stats()
714
1160
  top_tags = ', '.join([f"{tag}({count})" for tag, count in result.get('top_tags', [])[:10]])
@@ -718,27 +1164,97 @@ Total Links: {result.get('total_links', 0)}
718
1164
  Average Links per Memory: {result.get('avg_links_per_memory', 0):.2f}
719
1165
  Top Tags: {top_tags}"""
720
1166
  except httpx.HTTPStatusError as e:
1167
+ error_detail = e.response.text if e.response else "Unknown error"
1168
+ logger.error(f"API error: {e.response.status_code} - {error_detail}")
721
1169
  if e.response.status_code == 401:
722
- raise ToolError("Authentication failed. Please check your API key.")
723
- logger.error(f"API error: {e.response.status_code} - {e.response.text}")
724
- raise ToolError(f"Failed to get stats: {e.response.status_code}")
1170
+ raise ToolError("Authentication failed. Please login using the 'login' tool or configure your JWT token in the MCP client headers.")
1171
+ raise ToolError(f"Failed to get stats: HTTP {e.response.status_code} - {error_detail}")
1172
+ except ToolError:
1173
+ raise
725
1174
  except Exception as e:
726
- logger.error(f"Unexpected error: {e}", exc_info=True)
1175
+ logger.error(f"Unexpected error in get_stats: {e}", exc_info=True)
727
1176
  raise ToolError(f"Error getting stats: {str(e)}")
728
1177
 
729
1178
 
730
1179
  @mcp.tool()
731
1180
  async def find_path(from_id: str, to_id: str) -> str:
732
- """Find shortest path between two memories in the memory graph. Use this to explain connections between seemingly unrelated memories."""
733
- # Validate parameters
734
- if not from_id or not from_id.strip():
735
- raise ToolError("from_id cannot be empty")
736
- if not to_id or not to_id.strip():
737
- raise ToolError("to_id cannot be empty")
1181
+ """Find shortest path between two memories in the memory graph. Use this to explain connections between seemingly unrelated memories.
1182
+
1183
+ Parameters:
1184
+ from_id (str, REQUIRED): Source memory ID to start the path from.
1185
+ - Example: "480c1f76-bcdf-4491-8781-24510db992e3"
1186
+ - Get memory IDs from search_memories() or get_memories() results
1187
+
1188
+ to_id (str, REQUIRED): Target memory ID to find path to.
1189
+ - Example: "300d9716-a3a6-44d3-b0f4-b28002a65da8"
1190
+ - Get memory IDs from search_memories() or get_memories() results
1191
+
1192
+ Returns:
1193
+ str: The shortest path between the two memories, or a message if no path exists.
1194
+
1195
+ Common Errors and Solutions:
1196
+ - Error: "from_id cannot be empty"
1197
+ Solution: Provide a valid memory ID. Example: find_path(from_id="480c1f76-...", to_id="300d9716-...")
1198
+
1199
+ - Error: "to_id cannot be empty"
1200
+ Solution: Provide a valid memory ID. Example: find_path(from_id="480c1f76-...", to_id="300d9716-...")
1201
+
1202
+ Examples:
1203
+ # Find path between two memories
1204
+ find_path(
1205
+ from_id="480c1f76-bcdf-4491-8781-24510db992e3",
1206
+ to_id="300d9716-a3a6-44d3-b0f4-b28002a65da8"
1207
+ )
1208
+ """
1209
+ # Validate parameters with detailed error messages
1210
+ if from_id is None:
1211
+ raise ToolError(
1212
+ "The 'from_id' parameter is required but was not provided.\n"
1213
+ "Get memory IDs from search_memories() or get_memories() results.\n"
1214
+ "Example: find_path(from_id=\"480c1f76-...\", to_id=\"300d9716-...\")"
1215
+ )
1216
+
1217
+ if not isinstance(from_id, str):
1218
+ raise ToolError(
1219
+ f"The 'from_id' parameter must be a string, but got {type(from_id).__name__}.\n"
1220
+ f"Received: {repr(from_id)}\n"
1221
+ "Example: find_path(from_id=\"480c1f76-...\", to_id=\"300d9716-...\")"
1222
+ )
1223
+
1224
+ from_id_str = from_id.strip()
1225
+ if not from_id_str:
1226
+ raise ToolError(
1227
+ "The 'from_id' parameter cannot be empty or whitespace-only.\n"
1228
+ "Get memory IDs from search_memories() or get_memories() results.\n"
1229
+ "Example: find_path(from_id=\"480c1f76-bcdf-4491-8781-24510db992e3\", to_id=\"300d9716-...\")"
1230
+ )
1231
+
1232
+ if to_id is None:
1233
+ raise ToolError(
1234
+ "The 'to_id' parameter is required but was not provided.\n"
1235
+ "Get memory IDs from search_memories() or get_memories() results.\n"
1236
+ "Example: find_path(from_id=\"480c1f76-...\", to_id=\"300d9716-...\")"
1237
+ )
1238
+
1239
+ if not isinstance(to_id, str):
1240
+ raise ToolError(
1241
+ f"The 'to_id' parameter must be a string, but got {type(to_id).__name__}.\n"
1242
+ f"Received: {repr(to_id)}\n"
1243
+ "Example: find_path(from_id=\"480c1f76-...\", to_id=\"300d9716-...\")"
1244
+ )
1245
+
1246
+ to_id_str = to_id.strip()
1247
+ if not to_id_str:
1248
+ raise ToolError(
1249
+ "The 'to_id' parameter cannot be empty or whitespace-only.\n"
1250
+ "Get memory IDs from search_memories() or get_memories() results.\n"
1251
+ "Example: find_path(from_id=\"480c1f76-...\", to_id=\"300d9716-a3a6-44d3-b0f4-b28002a65da8\")"
1252
+ )
738
1253
 
739
1254
  try:
1255
+ logger.info(f"find_path called - from_id: {from_id_str}, to_id: {to_id_str}")
740
1256
  client = await _get_api_client()
741
- result = await client.find_path(from_id, to_id)
1257
+ result = await client.find_path(from_id_str, to_id_str)
742
1258
  if result.get("status") == "success":
743
1259
  path_text = f"Path found (length: {result.get('length', 0)}):\n"
744
1260
  for mem in result.get("memories", []):
@@ -746,27 +1262,97 @@ async def find_path(from_id: str, to_id: str) -> str:
746
1262
  return path_text
747
1263
  return result.get("message", "No path found")
748
1264
  except httpx.HTTPStatusError as e:
1265
+ error_detail = e.response.text if e.response else "Unknown error"
1266
+ logger.error(f"API error: {e.response.status_code} - {error_detail}")
749
1267
  if e.response.status_code == 401:
750
- raise ToolError("Authentication failed. Please check your API key.")
751
- logger.error(f"API error: {e.response.status_code} - {e.response.text}")
752
- raise ToolError(f"Failed to find path: {e.response.status_code}")
1268
+ raise ToolError("Authentication failed. Please login using the 'login' tool or configure your JWT token in the MCP client headers.")
1269
+ elif e.response.status_code == 404:
1270
+ raise ToolError(f"One or both memories not found.\nVerify the memory IDs are correct by searching for them first.")
1271
+ raise ToolError(f"Failed to find path: HTTP {e.response.status_code} - {error_detail}")
1272
+ except ToolError:
1273
+ raise
753
1274
  except Exception as e:
754
- logger.error(f"Unexpected error: {e}", exc_info=True)
1275
+ logger.error(f"Unexpected error in find_path: {e}", exc_info=True)
755
1276
  raise ToolError(f"Error finding path: {str(e)}")
756
1277
 
757
1278
 
758
1279
  @mcp.tool()
759
1280
  async def get_neighborhood(memory_id: str, hops: int = 2) -> str:
760
- """Get all memories within N hops of a given memory. Use this for deep context and understanding relationships around important memories."""
761
- # Validate parameters
762
- if not memory_id or not memory_id.strip():
763
- raise ToolError("memory_id cannot be empty")
1281
+ """Get all memories within N hops of a given memory. Use this for deep context and understanding relationships around important memories.
1282
+
1283
+ Parameters:
1284
+ memory_id (str, REQUIRED): Center memory ID to get neighborhood around.
1285
+ - Example: "480c1f76-bcdf-4491-8781-24510db992e3"
1286
+ - Get memory IDs from search_memories() or get_memories() results
1287
+
1288
+ hops (int, optional): Number of hops to traverse. Default is 2.
1289
+ - Must be between 1 and 5
1290
+ - 1 hop = direct connections only
1291
+ - 2 hops = direct connections + their connections
1292
+ - Example: 2 (default)
1293
+ - Example: 3
1294
+
1295
+ Returns:
1296
+ str: Formatted list of memories in the neighborhood with their hop distances.
1297
+
1298
+ Common Errors and Solutions:
1299
+ - Error: "memory_id cannot be empty"
1300
+ Solution: Provide a valid memory ID. Example: get_neighborhood(memory_id="480c1f76-...")
1301
+
1302
+ - Error: "hops must be between 1 and 5"
1303
+ Solution: Provide hops between 1 and 5. Example: get_neighborhood(memory_id="...", hops=3)
1304
+
1305
+ Examples:
1306
+ # Get neighborhood with default 2 hops
1307
+ get_neighborhood(memory_id="480c1f76-bcdf-4491-8781-24510db992e3")
1308
+
1309
+ # Get neighborhood with 3 hops
1310
+ get_neighborhood(memory_id="480c1f76-bcdf-4491-8781-24510db992e3", hops=3)
1311
+
1312
+ # Get direct connections only (1 hop)
1313
+ get_neighborhood(memory_id="480c1f76-bcdf-4491-8781-24510db992e3", hops=1)
1314
+ """
1315
+ # Validate parameters with detailed error messages
1316
+ if memory_id is None:
1317
+ raise ToolError(
1318
+ "The 'memory_id' parameter is required but was not provided.\n"
1319
+ "Get memory IDs from search_memories() or get_memories() results.\n"
1320
+ "Example: get_neighborhood(memory_id=\"480c1f76-bcdf-4491-8781-24510db992e3\")"
1321
+ )
1322
+
1323
+ if not isinstance(memory_id, str):
1324
+ raise ToolError(
1325
+ f"The 'memory_id' parameter must be a string, but got {type(memory_id).__name__}.\n"
1326
+ f"Received: {repr(memory_id)}\n"
1327
+ "Example: get_neighborhood(memory_id=\"480c1f76-bcdf-4491-8781-24510db992e3\")"
1328
+ )
1329
+
1330
+ memory_id_str = memory_id.strip()
1331
+ if not memory_id_str:
1332
+ raise ToolError(
1333
+ "The 'memory_id' parameter cannot be empty or whitespace-only.\n"
1334
+ "Get memory IDs from search_memories() or get_memories() results.\n"
1335
+ "Example: get_neighborhood(memory_id=\"480c1f76-bcdf-4491-8781-24510db992e3\")"
1336
+ )
1337
+
1338
+ if not isinstance(hops, int):
1339
+ raise ToolError(
1340
+ f"The 'hops' parameter must be an integer, but got {type(hops).__name__}.\n"
1341
+ f"Received: {repr(hops)}\n"
1342
+ "Example: get_neighborhood(memory_id=\"...\", hops=2)"
1343
+ )
1344
+
764
1345
  if not (1 <= hops <= 5):
765
- raise ToolError("hops must be between 1 and 5")
1346
+ raise ToolError(
1347
+ f"The 'hops' parameter must be between 1 and 5, but got {hops}.\n"
1348
+ "Example: get_neighborhood(memory_id=\"...\", hops=2)\n"
1349
+ "Example: get_neighborhood(memory_id=\"...\", hops=3)"
1350
+ )
766
1351
 
767
1352
  try:
1353
+ logger.info(f"get_neighborhood called - memory_id: {memory_id_str}, hops: {hops}")
768
1354
  client = await _get_api_client()
769
- result = await client.get_neighborhood(memory_id, hops)
1355
+ result = await client.get_neighborhood(memory_id_str, hops)
770
1356
  neighborhood_text = f"Neighborhood (hops={result.get('hops', 2)}, total={result.get('total_in_neighborhood', 0)}):\n"
771
1357
  for mem in result.get("neighborhood", []):
772
1358
  hop_dist = mem.get("hop_distance", 0)
@@ -774,12 +1360,17 @@ async def get_neighborhood(memory_id: str, hops: int = 2) -> str:
774
1360
  neighborhood_text += f" [{hop_dist}]{is_center} {mem.get('id', 'unknown')}: {mem.get('content', '')[:100]}\n"
775
1361
  return neighborhood_text
776
1362
  except httpx.HTTPStatusError as e:
1363
+ error_detail = e.response.text if e.response else "Unknown error"
1364
+ logger.error(f"API error: {e.response.status_code} - {error_detail}")
777
1365
  if e.response.status_code == 401:
778
- raise ToolError("Authentication failed. Please check your API key.")
779
- logger.error(f"API error: {e.response.status_code} - {e.response.text}")
780
- raise ToolError(f"Failed to get neighborhood: {e.response.status_code}")
1366
+ raise ToolError("Authentication failed. Please login using the 'login' tool or configure your JWT token in the MCP client headers.")
1367
+ elif e.response.status_code == 404:
1368
+ raise ToolError(f"Memory not found: {memory_id_str}\nVerify the memory_id is correct by searching for it first.")
1369
+ raise ToolError(f"Failed to get neighborhood: HTTP {e.response.status_code} - {error_detail}")
1370
+ except ToolError:
1371
+ raise
781
1372
  except Exception as e:
782
- logger.error(f"Unexpected error: {e}", exc_info=True)
1373
+ logger.error(f"Unexpected error in get_neighborhood: {e}", exc_info=True)
783
1374
  raise ToolError(f"Error getting neighborhood: {str(e)}")
784
1375
 
785
1376
 
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: mem-brain-mcp
3
- Version: 1.0.1
3
+ Version: 1.0.3
4
4
  Summary: MCP Server for Mem-Brain API - Exposes memory operations as MCP tools
5
5
  Keywords: ai,claude,cursor,llm,mcp,memory,model-context-protocol
6
6
  Classifier: Development Status :: 4 - Beta
@@ -0,0 +1,9 @@
1
+ mem_brain_mcp/__init__.py,sha256=4mabvpmReQ0ZVZ-xw8FrwHAdGDhjWQDpHhWg_11lxmY,89
2
+ mem_brain_mcp/__main__.py,sha256=H_mwoKm1FBmu4KzAcQcq-TXZqeNvlrAekAxB1s4F4hA,712
3
+ mem_brain_mcp/client.py,sha256=7KFGcLoPDaOOLiuG2lygQK7xH5Kio-YifDjuSpDoDJ8,6993
4
+ mem_brain_mcp/config.py,sha256=xx2lBkCIeT85t0HxtORwZHSU3hZT_EdsThpfjwPJhbQ,1261
5
+ mem_brain_mcp/server.py,sha256=ieSIkwWp3777ETa1M3J_SBMRUu571e3YsJj_C70UXRM,68812
6
+ mem_brain_mcp-1.0.3.dist-info/METADATA,sha256=dR7sgte11k7CQyL38FTrsRDVBjx4hhil8HCXtbl9abQ,5228
7
+ mem_brain_mcp-1.0.3.dist-info/WHEEL,sha256=WLgqFyCfm_KASv4WHyYy0P3pM_m7J5L9k2skdKLirC8,87
8
+ mem_brain_mcp-1.0.3.dist-info/entry_points.txt,sha256=NH6QYQ-Sd8eJn5crpe_DL1PvGeUlL3y65968xPhmwG8,62
9
+ mem_brain_mcp-1.0.3.dist-info/RECORD,,
@@ -1,9 +0,0 @@
1
- mem_brain_mcp/__init__.py,sha256=DifX5h1zBo_ResWlU7kT_HFCeptgPaJouZ03wxBUSqc,89
2
- mem_brain_mcp/__main__.py,sha256=H_mwoKm1FBmu4KzAcQcq-TXZqeNvlrAekAxB1s4F4hA,712
3
- mem_brain_mcp/client.py,sha256=7KFGcLoPDaOOLiuG2lygQK7xH5Kio-YifDjuSpDoDJ8,6993
4
- mem_brain_mcp/config.py,sha256=xx2lBkCIeT85t0HxtORwZHSU3hZT_EdsThpfjwPJhbQ,1261
5
- mem_brain_mcp/server.py,sha256=mlbl3-D3OFb4HHP54daqiP3nMWEdiTv3upEdUFnlhAA,39604
6
- mem_brain_mcp-1.0.1.dist-info/METADATA,sha256=AaOmWLOT6mGIrw7CysLZMacG3PbrzuOHD-K9XbuNsUs,5228
7
- mem_brain_mcp-1.0.1.dist-info/WHEEL,sha256=WLgqFyCfm_KASv4WHyYy0P3pM_m7J5L9k2skdKLirC8,87
8
- mem_brain_mcp-1.0.1.dist-info/entry_points.txt,sha256=NH6QYQ-Sd8eJn5crpe_DL1PvGeUlL3y65968xPhmwG8,62
9
- mem_brain_mcp-1.0.1.dist-info/RECORD,,