mem-brain-mcp 1.0.0__py3-none-any.whl → 1.0.2__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.2"
4
4
 
mem_brain_mcp/config.py CHANGED
@@ -16,11 +16,16 @@ class Settings(BaseSettings):
16
16
  )
17
17
 
18
18
  # API Configuration
19
- api_base_url: str = "http://localhost:8000"
20
- api_key: Optional[str] = None
19
+ api_base_url: str = "http://membrain-api-alb-1094729422.ap-south-1.elb.amazonaws.com"
20
+ membrain_api_key: Optional[str] = None
21
21
  # NOTE: default_user_id is deprecated and unused.
22
22
  # Per-user API keys are extracted from request headers for proper isolation.
23
23
  # Each MCP client should configure their own API key via headers.
24
+
25
+ @property
26
+ def api_key(self) -> Optional[str]:
27
+ """Backward compatibility property for api_key."""
28
+ return self.membrain_api_key
24
29
 
25
30
  # MCP Server Configuration
26
31
  mcp_server_host: str = "0.0.0.0"
mem_brain_mcp/server.py CHANGED
@@ -380,112 +380,6 @@ async def refresh_context() -> PromptMessage:
380
380
  # TOOLS (Operations)
381
381
  # ============================================================================
382
382
 
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
383
  @mcp.tool()
490
384
  async def get_agent_instructions(include_dynamic_context: bool = True) -> str:
491
385
  """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,34 +402,79 @@ async def add_memory(
508
402
  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
403
 
510
404
  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
405
+ content (str, REQUIRED): The memory content to store. Must be a non-empty string.
406
+ - Cannot be None, empty string, or whitespace-only
407
+ - Example: "User prefers Python over JavaScript"
408
+ - Example: "User prefers dark mode interfaces"
409
+
410
+ tags (list[str], optional): List of tags to categorize the memory.
411
+ - Can be None (default) or a list of strings
412
+ - Example: ["coding", "preferences"]
413
+ - Example: ["personal", "pets", "animals"]
414
+ - If you pass a single string, it will be converted to a list
415
+
416
+ category (str, optional): Category name for the memory.
417
+ - Can be None (default) or a non-empty string
418
+ - Example: "interests"
419
+ - Example: "preferences"
514
420
 
515
421
  Returns:
516
422
  str: A formatted string with the memory ID and details of the created memory.
517
423
 
424
+ Common Errors and Solutions:
425
+ - Error: "Tool call arguments for mcp were invalid"
426
+ Solution: Ensure 'content' parameter is provided as a string. Example: add_memory(content="User prefers dark mode")
427
+
428
+ - Error: "The 'content' parameter cannot be empty"
429
+ Solution: Provide non-empty content. Example: add_memory(content="User loves Python programming")
430
+
431
+ - Error: "tags must be a list"
432
+ Solution: Pass tags as a list. Example: add_memory(content="...", tags=["coding"]) not tags="coding"
433
+
518
434
  Example workflow:
519
435
  1. search_memories(query="User prefers Python") # Check for existing memories
520
436
  2. If no similar memory found, then: add_memory(content="User prefers Python over JavaScript", tags=["coding", "preferences"])
521
437
 
522
- Example:
438
+ Examples:
439
+ # Basic usage (required parameter only)
440
+ add_memory(content="User prefers dark mode")
441
+
442
+ # With tags
443
+ add_memory(content="User loves Python programming", tags=["coding", "preferences"])
444
+
445
+ # With tags and category
523
446
  add_memory(
524
447
  content="User loves working with TypeScript",
525
448
  tags=["coding", "typescript"],
526
449
  category="interests"
527
450
  )
451
+
452
+ # Tags as empty list (treated as None)
453
+ add_memory(content="User prefers coffee", tags=[])
528
454
  """
529
455
  # Validate parameters with detailed error messages
530
456
  if content is None:
531
- raise ToolError("The 'content' parameter is required but was not provided. Please provide the memory content as a string.")
457
+ raise ToolError(
458
+ "The 'content' parameter is required but was not provided.\n"
459
+ "Example: add_memory(content=\"User prefers dark mode\")\n"
460
+ "Example: add_memory(content=\"User loves Python programming\", tags=[\"coding\"])"
461
+ )
532
462
 
533
463
  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.")
464
+ raise ToolError(
465
+ f"The 'content' parameter must be a string, but got {type(content).__name__}.\n"
466
+ f"Received: {repr(content)}\n"
467
+ "Example: add_memory(content=\"User prefers dark mode\")"
468
+ )
535
469
 
536
470
  content_str = str(content).strip()
537
471
  if not content_str:
538
- raise ToolError("The 'content' parameter cannot be empty. Please provide the memory content as a non-empty string.")
472
+ raise ToolError(
473
+ "The 'content' parameter cannot be empty or whitespace-only.\n"
474
+ "Please provide a non-empty string with actual content.\n"
475
+ "Example: add_memory(content=\"User prefers dark mode\")\n"
476
+ "Example: add_memory(content=\"User loves Python programming\")"
477
+ )
539
478
 
540
479
  try:
541
480
  logger.info(f"add_memory called - content length: {len(content_str)}, tags: {tags}, category: {category}")
@@ -545,16 +484,26 @@ async def add_memory(
545
484
  normalized_tags = None
546
485
  if tags is not None:
547
486
  if isinstance(tags, list):
487
+ # Validate list contents are strings
488
+ if tags:
489
+ invalid_items = [item for item in tags if not isinstance(item, str)]
490
+ if invalid_items:
491
+ raise ToolError(
492
+ f"The 'tags' parameter must be a list of strings, but found non-string items: {invalid_items}\n"
493
+ f"Example: add_memory(content=\"...\", tags=[\"coding\", \"preferences\"])\n"
494
+ f"Example: add_memory(content=\"...\", tags=[\"personal\", \"pets\"])"
495
+ )
548
496
  normalized_tags = tags if tags else None # Empty list becomes None
549
497
  elif isinstance(tags, str):
550
498
  # Handle case where tags might be passed as a single string
551
499
  normalized_tags = [tags]
552
500
  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}")
557
- normalized_tags = None
501
+ raise ToolError(
502
+ f"The 'tags' parameter must be a list of strings or None, but got {type(tags).__name__}.\n"
503
+ f"Received: {repr(tags)}\n"
504
+ "Example: add_memory(content=\"...\", tags=[\"coding\", \"preferences\"])\n"
505
+ "Example: add_memory(content=\"...\", tags=None) # or omit tags parameter"
506
+ )
558
507
 
559
508
  # Normalize category: convert empty string to None
560
509
  normalized_category = category.strip() if category and isinstance(category, str) and category.strip() else None
@@ -586,49 +535,188 @@ async def add_memory(
586
535
 
587
536
  @mcp.tool()
588
537
  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")
538
+ """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.
539
+
540
+ Parameters:
541
+ query (str, REQUIRED): Search query string. Use natural language questions, not keywords.
542
+ - Example: "Who is Rakshith and what did he build?"
543
+ - Example: "What are the user's preferences for programming languages?"
544
+ - Example: "Tell me about memories related to the Dubai presentation"
545
+
546
+ k (int, optional): Number of results to return. Default is 5.
547
+ - Must be between 1 and 100
548
+ - Example: 5 (default)
549
+ - Example: 10
550
+
551
+ Returns:
552
+ str: Formatted search results with memory nodes and relationship edges.
553
+
554
+ Common Errors and Solutions:
555
+ - Error: "Query cannot be empty"
556
+ Solution: Provide a non-empty search query. Example: search_memories(query="What is the user's name?")
557
+
558
+ - Error: "k must be between 1 and 100"
559
+ Solution: Provide k between 1 and 100. Example: search_memories(query="...", k=10)
560
+
561
+ Examples:
562
+ # Basic search
563
+ search_memories(query="Who is Rakshith?")
564
+
565
+ # Search with more results
566
+ search_memories(query="What are the user's programming preferences?", k=10)
567
+
568
+ # Complex query
569
+ search_memories(query="Tell me about memories related to mem-brain and its features")
570
+ """
571
+ # Validate parameters with detailed error messages
572
+ if query is None:
573
+ raise ToolError(
574
+ "The 'query' parameter is required but was not provided.\n"
575
+ "Example: search_memories(query=\"Who is Rakshith?\")\n"
576
+ "Example: search_memories(query=\"What are the user's preferences?\")"
577
+ )
578
+
579
+ if not isinstance(query, str):
580
+ raise ToolError(
581
+ f"The 'query' parameter must be a string, but got {type(query).__name__}.\n"
582
+ f"Received: {repr(query)}\n"
583
+ "Example: search_memories(query=\"Who is Rakshith?\")"
584
+ )
585
+
586
+ query_str = query.strip()
587
+ if not query_str:
588
+ raise ToolError(
589
+ "The 'query' parameter cannot be empty or whitespace-only.\n"
590
+ "Provide a natural language question or search query.\n"
591
+ "Example: search_memories(query=\"Who is Rakshith?\")\n"
592
+ "Example: search_memories(query=\"What are the user's preferences?\")"
593
+ )
594
+
595
+ if not isinstance(k, int):
596
+ raise ToolError(
597
+ f"The 'k' parameter must be an integer, but got {type(k).__name__}.\n"
598
+ f"Received: {repr(k)}\n"
599
+ "Example: search_memories(query=\"...\", k=10)"
600
+ )
601
+
593
602
  if not (1 <= k <= 100):
594
- raise ToolError("k must be between 1 and 100")
603
+ raise ToolError(
604
+ f"The 'k' parameter must be between 1 and 100, but got {k}.\n"
605
+ "Example: search_memories(query=\"...\", k=5)\n"
606
+ "Example: search_memories(query=\"...\", k=10)"
607
+ )
595
608
 
596
609
  try:
610
+ logger.info(f"search_memories called - query length: {len(query_str)}, k: {k}")
597
611
  client = await _get_api_client()
598
- result = await client.search_memories(query, k)
612
+ result = await client.search_memories(query_str, k)
599
613
  return f"Found {result.get('count', 0)} results:\n{_format_search_results(result.get('results', []))}"
600
614
  except httpx.HTTPStatusError as e:
615
+ error_detail = e.response.text if e.response else "Unknown error"
616
+ logger.error(f"API error: {e.response.status_code} - {error_detail}")
601
617
  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}")
618
+ raise ToolError("Authentication failed. Please login using the 'login' tool or configure your JWT token in the MCP client headers.")
619
+ raise ToolError(f"Failed to search memories: HTTP {e.response.status_code} - {error_detail}")
620
+ except ToolError:
621
+ raise
605
622
  except Exception as e:
606
- logger.error(f"Unexpected error: {e}", exc_info=True)
623
+ logger.error(f"Unexpected error in search_memories: {e}", exc_info=True)
607
624
  raise ToolError(f"Error searching memories: {str(e)}")
608
625
 
609
626
 
610
627
  @mcp.tool()
611
628
  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
629
+ """Retrieve one or more memories by ID. Use this when you need full details for specific memories identified from search results.
630
+
631
+ Parameters:
632
+ memory_ids (list[str], REQUIRED): List of memory IDs to retrieve. Must be a non-empty list.
633
+ - Example: ["480c1f76-bcdf-4491-8781-24510db992e3"]
634
+ - Example: ["480c1f76-...", "300d9716-...", "6fb6b23f-..."]
635
+ - Get memory IDs from search_memories() results
636
+
637
+ Returns:
638
+ str: Formatted details of the retrieved memories.
639
+
640
+ Common Errors and Solutions:
641
+ - Error: "memory_ids cannot be empty"
642
+ Solution: Provide a list with at least one memory ID. Example: get_memories(memory_ids=["480c1f76-..."])
643
+
644
+ - Error: "Memory IDs cannot be empty"
645
+ Solution: Ensure all IDs in the list are non-empty strings. Example: get_memories(memory_ids=["480c1f76-..."])
646
+
647
+ - Error: "memory_ids must be a list"
648
+ Solution: Pass memory_ids as a list. Example: get_memories(memory_ids=["..."]) not memory_ids="..."
649
+
650
+ Examples:
651
+ # Get single memory
652
+ get_memories(memory_ids=["480c1f76-bcdf-4491-8781-24510db992e3"])
653
+
654
+ # Get multiple memories
655
+ get_memories(memory_ids=["480c1f76-...", "300d9716-...", "6fb6b23f-..."])
656
+ """
657
+ # Validate parameters with detailed error messages
658
+ if memory_ids is None:
659
+ raise ToolError(
660
+ "The 'memory_ids' parameter is required but was not provided.\n"
661
+ "Example: get_memories(memory_ids=[\"480c1f76-bcdf-4491-8781-24510db992e3\"])\n"
662
+ "Example: get_memories(memory_ids=[\"480c1f76-...\", \"300d9716-...\"])"
663
+ )
664
+
665
+ if not isinstance(memory_ids, list):
666
+ raise ToolError(
667
+ f"The 'memory_ids' parameter must be a list of strings, but got {type(memory_ids).__name__}.\n"
668
+ f"Received: {repr(memory_ids)}\n"
669
+ "Example: get_memories(memory_ids=[\"480c1f76-...\"])"
670
+ )
671
+
614
672
  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")
673
+ raise ToolError(
674
+ "The 'memory_ids' parameter cannot be an empty list.\n"
675
+ "Provide at least one memory ID.\n"
676
+ "Example: get_memories(memory_ids=[\"480c1f76-bcdf-4491-8781-24510db992e3\"])"
677
+ )
678
+
679
+ # Validate each memory ID in the list
680
+ validated_ids = []
681
+ for i, memory_id in enumerate(memory_ids):
682
+ if memory_id is None:
683
+ raise ToolError(
684
+ f"Memory ID at index {i} is None. All memory IDs must be non-empty strings.\n"
685
+ "Example: get_memories(memory_ids=[\"480c1f76-...\"])"
686
+ )
687
+ if not isinstance(memory_id, str):
688
+ raise ToolError(
689
+ f"Memory ID at index {i} must be a string, but got {type(memory_id).__name__}.\n"
690
+ f"Received: {repr(memory_id)}\n"
691
+ "Example: get_memories(memory_ids=[\"480c1f76-...\"])"
692
+ )
693
+ memory_id_str = memory_id.strip()
694
+ if not memory_id_str:
695
+ raise ToolError(
696
+ f"Memory ID at index {i} cannot be empty or whitespace-only.\n"
697
+ "Get memory IDs from search_memories() or get_memories() results.\n"
698
+ "Example: get_memories(memory_ids=[\"480c1f76-bcdf-4491-8781-24510db992e3\"])"
699
+ )
700
+ validated_ids.append(memory_id_str)
619
701
 
620
702
  try:
703
+ logger.info(f"get_memories called - count: {len(validated_ids)}")
621
704
  client = await _get_api_client()
622
- result = await client.get_memories(memory_ids)
705
+ result = await client.get_memories(validated_ids)
623
706
  memories = result.get("memories", [])
624
707
  return f"Retrieved {len(memories)} memories:\n{_format_memories_list(memories)}"
625
708
  except httpx.HTTPStatusError as e:
709
+ error_detail = e.response.text if e.response else "Unknown error"
710
+ logger.error(f"API error: {e.response.status_code} - {error_detail}")
626
711
  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}")
712
+ raise ToolError("Authentication failed. Please login using the 'login' tool or configure your JWT token in the MCP client headers.")
713
+ elif e.response.status_code == 404:
714
+ raise ToolError(f"One or more memories not found.\nVerify the memory IDs are correct by searching for them first.")
715
+ raise ToolError(f"Failed to get memories: HTTP {e.response.status_code} - {error_detail}")
716
+ except ToolError:
717
+ raise
630
718
  except Exception as e:
631
- logger.error(f"Unexpected error: {e}", exc_info=True)
719
+ logger.error(f"Unexpected error in get_memories: {e}", exc_info=True)
632
720
  raise ToolError(f"Error getting memories: {str(e)}")
633
721
 
634
722
 
@@ -638,25 +726,147 @@ async def update_memory(
638
726
  content: Optional[str] = None,
639
727
  tags: Optional[List[str]] = None
640
728
  ) -> 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")
729
+ """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.
730
+
731
+ Parameters:
732
+ memory_id (str, REQUIRED): The ID of the memory to update. Must be a non-empty string.
733
+ - Example: "480c1f76-bcdf-4491-8781-24510db992e3"
734
+ - Get memory IDs from search_memories() or get_memories() results
735
+
736
+ content (str, optional): New content for the memory.
737
+ - Can be None (to keep existing content) or a non-empty string
738
+ - If provided, must not be empty or whitespace-only
739
+ - Example: "User no longer likes TypeScript, prefers Python"
740
+
741
+ tags (list[str], optional): New tags for the memory.
742
+ - Can be None (to keep existing tags) or a list of strings
743
+ - Example: ["coding", "python"]
744
+ - Example: ["preferences", "updated"]
745
+
746
+ Returns:
747
+ str: A formatted string with the updated memory details.
748
+
749
+ Common Errors and Solutions:
750
+ - Error: "Tool call arguments for mcp were invalid"
751
+ Solution: Ensure 'memory_id' parameter is provided as a string. Example: update_memory(memory_id="...")
752
+
753
+ - Error: "memory_id cannot be empty"
754
+ Solution: Provide a valid memory ID from search results. Example: update_memory(memory_id="480c1f76-...")
755
+
756
+ - Error: "At least one of 'content' or 'tags' must be provided"
757
+ Solution: Provide content or tags to update. Example: update_memory(memory_id="...", content="New content")
758
+
759
+ Examples:
760
+ # Update content only
761
+ update_memory(memory_id="480c1f76-...", content="User prefers Python over JavaScript")
762
+
763
+ # Update tags only
764
+ update_memory(memory_id="480c1f76-...", tags=["coding", "preferences"])
765
+
766
+ # Update both content and tags
767
+ update_memory(
768
+ memory_id="480c1f76-...",
769
+ content="User no longer likes TypeScript",
770
+ tags=["coding", "python"]
771
+ )
772
+ """
773
+ # Validate parameters with detailed error messages
774
+ if memory_id is None:
775
+ raise ToolError(
776
+ "The 'memory_id' parameter is required but was not provided.\n"
777
+ "Get memory IDs from search_memories() or get_memories() results.\n"
778
+ "Example: update_memory(memory_id=\"480c1f76-bcdf-4491-8781-24510db992e3\", content=\"New content\")"
779
+ )
780
+
781
+ if not isinstance(memory_id, str):
782
+ raise ToolError(
783
+ f"The 'memory_id' parameter must be a string, but got {type(memory_id).__name__}.\n"
784
+ f"Received: {repr(memory_id)}\n"
785
+ "Example: update_memory(memory_id=\"480c1f76-...\", content=\"New content\")"
786
+ )
787
+
788
+ memory_id_str = memory_id.strip()
789
+ if not memory_id_str:
790
+ raise ToolError(
791
+ "The 'memory_id' parameter cannot be empty or whitespace-only.\n"
792
+ "Get memory IDs from search_memories() or get_memories() results.\n"
793
+ "Example: update_memory(memory_id=\"480c1f76-bcdf-4491-8781-24510db992e3\", content=\"New content\")"
794
+ )
795
+
796
+ # Validate that at least one update parameter is provided
797
+ if content is None and tags is None:
798
+ raise ToolError(
799
+ "At least one of 'content' or 'tags' must be provided to update the memory.\n"
800
+ "Example: update_memory(memory_id=\"...\", content=\"New content\")\n"
801
+ "Example: update_memory(memory_id=\"...\", tags=[\"new\", \"tags\"])"
802
+ )
803
+
804
+ # Validate content if provided
805
+ if content is not None:
806
+ if not isinstance(content, str):
807
+ raise ToolError(
808
+ f"The 'content' parameter must be a string or None, but got {type(content).__name__}.\n"
809
+ f"Received: {repr(content)}\n"
810
+ "Example: update_memory(memory_id=\"...\", content=\"New content\")"
811
+ )
812
+ content_str = str(content).strip()
813
+ if not content_str:
814
+ raise ToolError(
815
+ "The 'content' parameter cannot be empty or whitespace-only.\n"
816
+ "Provide a non-empty string or omit the parameter to keep existing content.\n"
817
+ "Example: update_memory(memory_id=\"...\", content=\"New content\")"
818
+ )
819
+ else:
820
+ content_str = None
821
+
822
+ # Validate tags if provided
823
+ normalized_tags = None
824
+ if tags is not None:
825
+ if isinstance(tags, list):
826
+ # Validate list contents are strings
827
+ if tags:
828
+ invalid_items = [item for item in tags if not isinstance(item, str)]
829
+ if invalid_items:
830
+ raise ToolError(
831
+ f"The 'tags' parameter must be a list of strings, but found non-string items: {invalid_items}\n"
832
+ "Example: update_memory(memory_id=\"...\", tags=[\"coding\", \"preferences\"])\n"
833
+ "Example: update_memory(memory_id=\"...\", tags=None) # or omit tags parameter"
834
+ )
835
+ normalized_tags = tags if tags else None # Empty list becomes None
836
+ elif isinstance(tags, str):
837
+ # Handle case where tags might be passed as a single string
838
+ normalized_tags = [tags]
839
+ else:
840
+ raise ToolError(
841
+ f"The 'tags' parameter must be a list of strings or None, but got {type(tags).__name__}.\n"
842
+ f"Received: {repr(tags)}\n"
843
+ "Example: update_memory(memory_id=\"...\", tags=[\"coding\", \"preferences\"])\n"
844
+ "Example: update_memory(memory_id=\"...\", tags=None) # or omit tags parameter"
845
+ )
645
846
 
646
847
  try:
848
+ logger.info(f"update_memory called - memory_id: {memory_id_str}, content length: {len(content_str) if content_str else 0}, tags: {normalized_tags}")
849
+
647
850
  client = await _get_api_client()
648
- result = await client.update_memory(memory_id, content, tags)
851
+ result = await client.update_memory(memory_id_str, content_str, normalized_tags)
649
852
  memory = result.get('memory')
650
853
  if memory:
651
854
  return f"Memory updated:\n{_format_memory(memory)}"
652
- return f"Memory {memory_id} updated"
855
+ return f"Memory {memory_id_str} updated"
653
856
  except httpx.HTTPStatusError as e:
857
+ error_detail = e.response.text if e.response else "Unknown error"
858
+ logger.error(f"API error: {e.response.status_code} - {error_detail}")
654
859
  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}")
860
+ raise ToolError("Authentication failed. Please login using the 'login' tool or configure your JWT token in the MCP client headers.")
861
+ elif e.response.status_code == 400:
862
+ raise ToolError(f"Invalid request: {error_detail}\nExample: update_memory(memory_id=\"...\", content=\"New content\")")
863
+ elif e.response.status_code == 404:
864
+ raise ToolError(f"Memory not found: {memory_id_str}\nVerify the memory_id is correct by searching for it first.")
865
+ raise ToolError(f"Failed to update memory: HTTP {e.response.status_code} - {error_detail}")
866
+ except ToolError:
867
+ raise
658
868
  except Exception as e:
659
- logger.error(f"Unexpected error: {e}", exc_info=True)
869
+ logger.error(f"Unexpected error in update_memory: {e}", exc_info=True)
660
870
  raise ToolError(f"Error updating memory: {str(e)}")
661
871
 
662
872
 
@@ -666,49 +876,243 @@ async def delete_memories(
666
876
  tags: Optional[str] = None,
667
877
  category: Optional[str] = None
668
878
  ) -> 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."""
879
+ """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.
880
+
881
+ Parameters:
882
+ memory_id (str, optional): Specific memory ID to delete. Takes precedence over filters.
883
+ - Example: "480c1f76-bcdf-4491-8781-24510db992e3"
884
+ - Get memory IDs from search_memories() or get_memories() results
885
+
886
+ tags (str, optional): Comma-separated tags for filter-based deletion.
887
+ - Example: "coding,preferences"
888
+ - Example: "personal,pets"
889
+ - Only used if memory_id is not provided
890
+
891
+ category (str, optional): Category name for filter-based deletion.
892
+ - Example: "interests"
893
+ - Example: "preferences"
894
+ - Only used if memory_id is not provided
895
+
896
+ Returns:
897
+ str: A message indicating how many memories were deleted and their IDs.
898
+
899
+ Common Errors and Solutions:
900
+ - Error: "At least one parameter must be provided"
901
+ Solution: Provide memory_id, tags, or category. Example: delete_memories(memory_id="...")
902
+
903
+ - Error: "memory_id cannot be empty"
904
+ Solution: Provide a valid memory ID or omit the parameter. Example: delete_memories(memory_id="480c1f76-...")
905
+
906
+ Examples:
907
+ # Delete by memory ID
908
+ delete_memories(memory_id="480c1f76-bcdf-4491-8781-24510db992e3")
909
+
910
+ # Delete by tags
911
+ delete_memories(tags="coding,preferences")
912
+
913
+ # Delete by category
914
+ delete_memories(category="interests")
915
+ """
916
+ # Validate that at least one parameter is provided
917
+ if memory_id is None and tags is None and category is None:
918
+ raise ToolError(
919
+ "At least one parameter (memory_id, tags, or category) must be provided to delete memories.\n"
920
+ "Example: delete_memories(memory_id=\"480c1f76-bcdf-4491-8781-24510db992e3\")\n"
921
+ "Example: delete_memories(tags=\"coding,preferences\")\n"
922
+ "Example: delete_memories(category=\"interests\")"
923
+ )
924
+
925
+ # Validate memory_id if provided
926
+ if memory_id is not None:
927
+ if not isinstance(memory_id, str):
928
+ raise ToolError(
929
+ f"The 'memory_id' parameter must be a string or None, but got {type(memory_id).__name__}.\n"
930
+ f"Received: {repr(memory_id)}\n"
931
+ "Example: delete_memories(memory_id=\"480c1f76-bcdf-4491-8781-24510db992e3\")"
932
+ )
933
+ memory_id_str = memory_id.strip()
934
+ if not memory_id_str:
935
+ raise ToolError(
936
+ "The 'memory_id' parameter cannot be empty or whitespace-only.\n"
937
+ "Get memory IDs from search_memories() or get_memories() results.\n"
938
+ "Example: delete_memories(memory_id=\"480c1f76-bcdf-4491-8781-24510db992e3\")"
939
+ )
940
+ else:
941
+ memory_id_str = None
942
+
943
+ # Validate tags if provided
944
+ if tags is not None:
945
+ if not isinstance(tags, str):
946
+ raise ToolError(
947
+ f"The 'tags' parameter must be a string or None, but got {type(tags).__name__}.\n"
948
+ f"Received: {repr(tags)}\n"
949
+ "Example: delete_memories(tags=\"coding,preferences\")"
950
+ )
951
+ tags_str = tags.strip()
952
+ if not tags_str:
953
+ raise ToolError(
954
+ "The 'tags' parameter cannot be empty or whitespace-only.\n"
955
+ "Provide comma-separated tags or omit the parameter.\n"
956
+ "Example: delete_memories(tags=\"coding,preferences\")"
957
+ )
958
+ else:
959
+ tags_str = None
960
+
961
+ # Validate category if provided
962
+ if category is not None:
963
+ if not isinstance(category, str):
964
+ raise ToolError(
965
+ f"The 'category' parameter must be a string or None, but got {type(category).__name__}.\n"
966
+ f"Received: {repr(category)}\n"
967
+ "Example: delete_memories(category=\"interests\")"
968
+ )
969
+ category_str = category.strip()
970
+ if not category_str:
971
+ raise ToolError(
972
+ "The 'category' parameter cannot be empty or whitespace-only.\n"
973
+ "Provide a category name or omit the parameter.\n"
974
+ "Example: delete_memories(category=\"interests\")"
975
+ )
976
+ else:
977
+ category_str = None
978
+
670
979
  try:
980
+ logger.info(f"delete_memories called - memory_id: {memory_id_str}, tags: {tags_str}, category: {category_str}")
671
981
  client = await _get_api_client()
672
- result = await client.delete_memories(memory_id, tags, category)
982
+ result = await client.delete_memories(memory_id_str, tags_str, category_str)
673
983
  deleted_ids = result.get('memory_ids', [])[:10]
674
984
  return f"Deleted {result.get('deleted_count', 0)} memories. IDs: {', '.join(deleted_ids)}"
675
985
  except httpx.HTTPStatusError as e:
986
+ error_detail = e.response.text if e.response else "Unknown error"
987
+ logger.error(f"API error: {e.response.status_code} - {error_detail}")
676
988
  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}")
989
+ raise ToolError("Authentication failed. Please login using the 'login' tool or configure your JWT token in the MCP client headers.")
990
+ elif e.response.status_code == 404:
991
+ raise ToolError(f"Memory not found: {memory_id_str}\nVerify the memory_id is correct by searching for it first.")
992
+ raise ToolError(f"Failed to delete memories: HTTP {e.response.status_code} - {error_detail}")
993
+ except ToolError:
994
+ raise
680
995
  except Exception as e:
681
- logger.error(f"Unexpected error: {e}", exc_info=True)
996
+ logger.error(f"Unexpected error in delete_memories: {e}", exc_info=True)
682
997
  raise ToolError(f"Error deleting memories: {str(e)}")
683
998
 
684
999
 
685
1000
  @mcp.tool()
686
1001
  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")
1002
+ """Remove link between two memories when the connection is no longer relevant or accurate.
1003
+
1004
+ Parameters:
1005
+ memory_id_1 (str, REQUIRED): First memory ID in the link to remove.
1006
+ - Example: "480c1f76-bcdf-4491-8781-24510db992e3"
1007
+ - Get memory IDs from search_memories() or get_memories() results
1008
+
1009
+ memory_id_2 (str, REQUIRED): Second memory ID in the link to remove.
1010
+ - Example: "300d9716-a3a6-44d3-b0f4-b28002a65da8"
1011
+ - Get memory IDs from search_memories() or get_memories() results
1012
+
1013
+ Returns:
1014
+ str: Confirmation message that the memories were unlinked.
1015
+
1016
+ Common Errors and Solutions:
1017
+ - Error: "memory_id_1 cannot be empty"
1018
+ Solution: Provide a valid memory ID. Example: unlink_memories(memory_id_1="480c1f76-...", memory_id_2="300d9716-...")
1019
+
1020
+ - Error: "memory_id_2 cannot be empty"
1021
+ Solution: Provide a valid memory ID. Example: unlink_memories(memory_id_1="480c1f76-...", memory_id_2="300d9716-...")
1022
+
1023
+ Examples:
1024
+ # Unlink two memories
1025
+ unlink_memories(
1026
+ memory_id_1="480c1f76-bcdf-4491-8781-24510db992e3",
1027
+ memory_id_2="300d9716-a3a6-44d3-b0f4-b28002a65da8"
1028
+ )
1029
+ """
1030
+ # Validate parameters with detailed error messages
1031
+ if memory_id_1 is None:
1032
+ raise ToolError(
1033
+ "The 'memory_id_1' parameter is required but was not provided.\n"
1034
+ "Get memory IDs from search_memories() or get_memories() results.\n"
1035
+ "Example: unlink_memories(memory_id_1=\"480c1f76-...\", memory_id_2=\"300d9716-...\")"
1036
+ )
1037
+
1038
+ if not isinstance(memory_id_1, str):
1039
+ raise ToolError(
1040
+ f"The 'memory_id_1' parameter must be a string, but got {type(memory_id_1).__name__}.\n"
1041
+ f"Received: {repr(memory_id_1)}\n"
1042
+ "Example: unlink_memories(memory_id_1=\"480c1f76-...\", memory_id_2=\"300d9716-...\")"
1043
+ )
1044
+
1045
+ memory_id_1_str = memory_id_1.strip()
1046
+ if not memory_id_1_str:
1047
+ raise ToolError(
1048
+ "The 'memory_id_1' parameter cannot be empty or whitespace-only.\n"
1049
+ "Get memory IDs from search_memories() or get_memories() results.\n"
1050
+ "Example: unlink_memories(memory_id_1=\"480c1f76-bcdf-4491-8781-24510db992e3\", memory_id_2=\"300d9716-...\")"
1051
+ )
1052
+
1053
+ if memory_id_2 is None:
1054
+ raise ToolError(
1055
+ "The 'memory_id_2' parameter is required but was not provided.\n"
1056
+ "Get memory IDs from search_memories() or get_memories() results.\n"
1057
+ "Example: unlink_memories(memory_id_1=\"480c1f76-...\", memory_id_2=\"300d9716-...\")"
1058
+ )
1059
+
1060
+ if not isinstance(memory_id_2, str):
1061
+ raise ToolError(
1062
+ f"The 'memory_id_2' parameter must be a string, but got {type(memory_id_2).__name__}.\n"
1063
+ f"Received: {repr(memory_id_2)}\n"
1064
+ "Example: unlink_memories(memory_id_1=\"480c1f76-...\", memory_id_2=\"300d9716-...\")"
1065
+ )
1066
+
1067
+ memory_id_2_str = memory_id_2.strip()
1068
+ if not memory_id_2_str:
1069
+ raise ToolError(
1070
+ "The 'memory_id_2' parameter cannot be empty or whitespace-only.\n"
1071
+ "Get memory IDs from search_memories() or get_memories() results.\n"
1072
+ "Example: unlink_memories(memory_id_1=\"480c1f76-...\", memory_id_2=\"300d9716-a3a6-44d3-b0f4-b28002a65da8\")"
1073
+ )
1074
+
1075
+ # Ensure the IDs are different
1076
+ if memory_id_1_str == memory_id_2_str:
1077
+ raise ToolError(
1078
+ "memory_id_1 and memory_id_2 must be different.\n"
1079
+ "You cannot unlink a memory from itself.\n"
1080
+ "Example: unlink_memories(memory_id_1=\"480c1f76-...\", memory_id_2=\"300d9716-...\")"
1081
+ )
693
1082
 
694
1083
  try:
1084
+ logger.info(f"unlink_memories called - memory_id_1: {memory_id_1_str}, memory_id_2: {memory_id_2_str}")
695
1085
  client = await _get_api_client()
696
- result = await client.unlink_memories(memory_id_1, memory_id_2)
1086
+ result = await client.unlink_memories(memory_id_1_str, memory_id_2_str)
697
1087
  return result.get("message", "Memories unlinked")
698
1088
  except httpx.HTTPStatusError as e:
1089
+ error_detail = e.response.text if e.response else "Unknown error"
1090
+ logger.error(f"API error: {e.response.status_code} - {error_detail}")
699
1091
  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}")
1092
+ raise ToolError("Authentication failed. Please login using the 'login' tool or configure your JWT token in the MCP client headers.")
1093
+ elif e.response.status_code == 404:
1094
+ raise ToolError(f"One or both memories not found.\nVerify the memory IDs are correct by searching for them first.")
1095
+ raise ToolError(f"Failed to unlink memories: HTTP {e.response.status_code} - {error_detail}")
1096
+ except ToolError:
1097
+ raise
703
1098
  except Exception as e:
704
- logger.error(f"Unexpected error: {e}", exc_info=True)
1099
+ logger.error(f"Unexpected error in unlink_memories: {e}", exc_info=True)
705
1100
  raise ToolError(f"Error unlinking memories: {str(e)}")
706
1101
 
707
1102
 
708
1103
  @mcp.tool()
709
1104
  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."""
1105
+ """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.
1106
+
1107
+ Returns:
1108
+ str: Formatted statistics including total memories, links, and top tags.
1109
+
1110
+ Examples:
1111
+ # Get statistics
1112
+ get_stats()
1113
+ """
711
1114
  try:
1115
+ logger.info("get_stats called")
712
1116
  client = await _get_api_client()
713
1117
  result = await client.get_stats()
714
1118
  top_tags = ', '.join([f"{tag}({count})" for tag, count in result.get('top_tags', [])[:10]])
@@ -718,27 +1122,97 @@ Total Links: {result.get('total_links', 0)}
718
1122
  Average Links per Memory: {result.get('avg_links_per_memory', 0):.2f}
719
1123
  Top Tags: {top_tags}"""
720
1124
  except httpx.HTTPStatusError as e:
1125
+ error_detail = e.response.text if e.response else "Unknown error"
1126
+ logger.error(f"API error: {e.response.status_code} - {error_detail}")
721
1127
  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}")
1128
+ raise ToolError("Authentication failed. Please login using the 'login' tool or configure your JWT token in the MCP client headers.")
1129
+ raise ToolError(f"Failed to get stats: HTTP {e.response.status_code} - {error_detail}")
1130
+ except ToolError:
1131
+ raise
725
1132
  except Exception as e:
726
- logger.error(f"Unexpected error: {e}", exc_info=True)
1133
+ logger.error(f"Unexpected error in get_stats: {e}", exc_info=True)
727
1134
  raise ToolError(f"Error getting stats: {str(e)}")
728
1135
 
729
1136
 
730
1137
  @mcp.tool()
731
1138
  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")
1139
+ """Find shortest path between two memories in the memory graph. Use this to explain connections between seemingly unrelated memories.
1140
+
1141
+ Parameters:
1142
+ from_id (str, REQUIRED): Source memory ID to start the path from.
1143
+ - Example: "480c1f76-bcdf-4491-8781-24510db992e3"
1144
+ - Get memory IDs from search_memories() or get_memories() results
1145
+
1146
+ to_id (str, REQUIRED): Target memory ID to find path to.
1147
+ - Example: "300d9716-a3a6-44d3-b0f4-b28002a65da8"
1148
+ - Get memory IDs from search_memories() or get_memories() results
1149
+
1150
+ Returns:
1151
+ str: The shortest path between the two memories, or a message if no path exists.
1152
+
1153
+ Common Errors and Solutions:
1154
+ - Error: "from_id cannot be empty"
1155
+ Solution: Provide a valid memory ID. Example: find_path(from_id="480c1f76-...", to_id="300d9716-...")
1156
+
1157
+ - Error: "to_id cannot be empty"
1158
+ Solution: Provide a valid memory ID. Example: find_path(from_id="480c1f76-...", to_id="300d9716-...")
1159
+
1160
+ Examples:
1161
+ # Find path between two memories
1162
+ find_path(
1163
+ from_id="480c1f76-bcdf-4491-8781-24510db992e3",
1164
+ to_id="300d9716-a3a6-44d3-b0f4-b28002a65da8"
1165
+ )
1166
+ """
1167
+ # Validate parameters with detailed error messages
1168
+ if from_id is None:
1169
+ raise ToolError(
1170
+ "The 'from_id' parameter is required but was not provided.\n"
1171
+ "Get memory IDs from search_memories() or get_memories() results.\n"
1172
+ "Example: find_path(from_id=\"480c1f76-...\", to_id=\"300d9716-...\")"
1173
+ )
1174
+
1175
+ if not isinstance(from_id, str):
1176
+ raise ToolError(
1177
+ f"The 'from_id' parameter must be a string, but got {type(from_id).__name__}.\n"
1178
+ f"Received: {repr(from_id)}\n"
1179
+ "Example: find_path(from_id=\"480c1f76-...\", to_id=\"300d9716-...\")"
1180
+ )
1181
+
1182
+ from_id_str = from_id.strip()
1183
+ if not from_id_str:
1184
+ raise ToolError(
1185
+ "The 'from_id' parameter cannot be empty or whitespace-only.\n"
1186
+ "Get memory IDs from search_memories() or get_memories() results.\n"
1187
+ "Example: find_path(from_id=\"480c1f76-bcdf-4491-8781-24510db992e3\", to_id=\"300d9716-...\")"
1188
+ )
1189
+
1190
+ if to_id is None:
1191
+ raise ToolError(
1192
+ "The 'to_id' parameter is required but was not provided.\n"
1193
+ "Get memory IDs from search_memories() or get_memories() results.\n"
1194
+ "Example: find_path(from_id=\"480c1f76-...\", to_id=\"300d9716-...\")"
1195
+ )
1196
+
1197
+ if not isinstance(to_id, str):
1198
+ raise ToolError(
1199
+ f"The 'to_id' parameter must be a string, but got {type(to_id).__name__}.\n"
1200
+ f"Received: {repr(to_id)}\n"
1201
+ "Example: find_path(from_id=\"480c1f76-...\", to_id=\"300d9716-...\")"
1202
+ )
1203
+
1204
+ to_id_str = to_id.strip()
1205
+ if not to_id_str:
1206
+ raise ToolError(
1207
+ "The 'to_id' parameter cannot be empty or whitespace-only.\n"
1208
+ "Get memory IDs from search_memories() or get_memories() results.\n"
1209
+ "Example: find_path(from_id=\"480c1f76-...\", to_id=\"300d9716-a3a6-44d3-b0f4-b28002a65da8\")"
1210
+ )
738
1211
 
739
1212
  try:
1213
+ logger.info(f"find_path called - from_id: {from_id_str}, to_id: {to_id_str}")
740
1214
  client = await _get_api_client()
741
- result = await client.find_path(from_id, to_id)
1215
+ result = await client.find_path(from_id_str, to_id_str)
742
1216
  if result.get("status") == "success":
743
1217
  path_text = f"Path found (length: {result.get('length', 0)}):\n"
744
1218
  for mem in result.get("memories", []):
@@ -746,27 +1220,97 @@ async def find_path(from_id: str, to_id: str) -> str:
746
1220
  return path_text
747
1221
  return result.get("message", "No path found")
748
1222
  except httpx.HTTPStatusError as e:
1223
+ error_detail = e.response.text if e.response else "Unknown error"
1224
+ logger.error(f"API error: {e.response.status_code} - {error_detail}")
749
1225
  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}")
1226
+ raise ToolError("Authentication failed. Please login using the 'login' tool or configure your JWT token in the MCP client headers.")
1227
+ elif e.response.status_code == 404:
1228
+ raise ToolError(f"One or both memories not found.\nVerify the memory IDs are correct by searching for them first.")
1229
+ raise ToolError(f"Failed to find path: HTTP {e.response.status_code} - {error_detail}")
1230
+ except ToolError:
1231
+ raise
753
1232
  except Exception as e:
754
- logger.error(f"Unexpected error: {e}", exc_info=True)
1233
+ logger.error(f"Unexpected error in find_path: {e}", exc_info=True)
755
1234
  raise ToolError(f"Error finding path: {str(e)}")
756
1235
 
757
1236
 
758
1237
  @mcp.tool()
759
1238
  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")
1239
+ """Get all memories within N hops of a given memory. Use this for deep context and understanding relationships around important memories.
1240
+
1241
+ Parameters:
1242
+ memory_id (str, REQUIRED): Center memory ID to get neighborhood around.
1243
+ - Example: "480c1f76-bcdf-4491-8781-24510db992e3"
1244
+ - Get memory IDs from search_memories() or get_memories() results
1245
+
1246
+ hops (int, optional): Number of hops to traverse. Default is 2.
1247
+ - Must be between 1 and 5
1248
+ - 1 hop = direct connections only
1249
+ - 2 hops = direct connections + their connections
1250
+ - Example: 2 (default)
1251
+ - Example: 3
1252
+
1253
+ Returns:
1254
+ str: Formatted list of memories in the neighborhood with their hop distances.
1255
+
1256
+ Common Errors and Solutions:
1257
+ - Error: "memory_id cannot be empty"
1258
+ Solution: Provide a valid memory ID. Example: get_neighborhood(memory_id="480c1f76-...")
1259
+
1260
+ - Error: "hops must be between 1 and 5"
1261
+ Solution: Provide hops between 1 and 5. Example: get_neighborhood(memory_id="...", hops=3)
1262
+
1263
+ Examples:
1264
+ # Get neighborhood with default 2 hops
1265
+ get_neighborhood(memory_id="480c1f76-bcdf-4491-8781-24510db992e3")
1266
+
1267
+ # Get neighborhood with 3 hops
1268
+ get_neighborhood(memory_id="480c1f76-bcdf-4491-8781-24510db992e3", hops=3)
1269
+
1270
+ # Get direct connections only (1 hop)
1271
+ get_neighborhood(memory_id="480c1f76-bcdf-4491-8781-24510db992e3", hops=1)
1272
+ """
1273
+ # Validate parameters with detailed error messages
1274
+ if memory_id is None:
1275
+ raise ToolError(
1276
+ "The 'memory_id' parameter is required but was not provided.\n"
1277
+ "Get memory IDs from search_memories() or get_memories() results.\n"
1278
+ "Example: get_neighborhood(memory_id=\"480c1f76-bcdf-4491-8781-24510db992e3\")"
1279
+ )
1280
+
1281
+ if not isinstance(memory_id, str):
1282
+ raise ToolError(
1283
+ f"The 'memory_id' parameter must be a string, but got {type(memory_id).__name__}.\n"
1284
+ f"Received: {repr(memory_id)}\n"
1285
+ "Example: get_neighborhood(memory_id=\"480c1f76-bcdf-4491-8781-24510db992e3\")"
1286
+ )
1287
+
1288
+ memory_id_str = memory_id.strip()
1289
+ if not memory_id_str:
1290
+ raise ToolError(
1291
+ "The 'memory_id' parameter cannot be empty or whitespace-only.\n"
1292
+ "Get memory IDs from search_memories() or get_memories() results.\n"
1293
+ "Example: get_neighborhood(memory_id=\"480c1f76-bcdf-4491-8781-24510db992e3\")"
1294
+ )
1295
+
1296
+ if not isinstance(hops, int):
1297
+ raise ToolError(
1298
+ f"The 'hops' parameter must be an integer, but got {type(hops).__name__}.\n"
1299
+ f"Received: {repr(hops)}\n"
1300
+ "Example: get_neighborhood(memory_id=\"...\", hops=2)"
1301
+ )
1302
+
764
1303
  if not (1 <= hops <= 5):
765
- raise ToolError("hops must be between 1 and 5")
1304
+ raise ToolError(
1305
+ f"The 'hops' parameter must be between 1 and 5, but got {hops}.\n"
1306
+ "Example: get_neighborhood(memory_id=\"...\", hops=2)\n"
1307
+ "Example: get_neighborhood(memory_id=\"...\", hops=3)"
1308
+ )
766
1309
 
767
1310
  try:
1311
+ logger.info(f"get_neighborhood called - memory_id: {memory_id_str}, hops: {hops}")
768
1312
  client = await _get_api_client()
769
- result = await client.get_neighborhood(memory_id, hops)
1313
+ result = await client.get_neighborhood(memory_id_str, hops)
770
1314
  neighborhood_text = f"Neighborhood (hops={result.get('hops', 2)}, total={result.get('total_in_neighborhood', 0)}):\n"
771
1315
  for mem in result.get("neighborhood", []):
772
1316
  hop_dist = mem.get("hop_distance", 0)
@@ -774,12 +1318,17 @@ async def get_neighborhood(memory_id: str, hops: int = 2) -> str:
774
1318
  neighborhood_text += f" [{hop_dist}]{is_center} {mem.get('id', 'unknown')}: {mem.get('content', '')[:100]}\n"
775
1319
  return neighborhood_text
776
1320
  except httpx.HTTPStatusError as e:
1321
+ error_detail = e.response.text if e.response else "Unknown error"
1322
+ logger.error(f"API error: {e.response.status_code} - {error_detail}")
777
1323
  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}")
1324
+ raise ToolError("Authentication failed. Please login using the 'login' tool or configure your JWT token in the MCP client headers.")
1325
+ elif e.response.status_code == 404:
1326
+ raise ToolError(f"Memory not found: {memory_id_str}\nVerify the memory_id is correct by searching for it first.")
1327
+ raise ToolError(f"Failed to get neighborhood: HTTP {e.response.status_code} - {error_detail}")
1328
+ except ToolError:
1329
+ raise
781
1330
  except Exception as e:
782
- logger.error(f"Unexpected error: {e}", exc_info=True)
1331
+ logger.error(f"Unexpected error in get_neighborhood: {e}", exc_info=True)
783
1332
  raise ToolError(f"Error getting neighborhood: {str(e)}")
784
1333
 
785
1334
 
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: mem-brain-mcp
3
- Version: 1.0.0
3
+ Version: 1.0.2
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
@@ -38,21 +38,40 @@ MCP (Model Context Protocol) server that exposes Mem-Brain API functionality as
38
38
  - **HTTP/SSE Transport**: Run independently, accessible remotely
39
39
  - **CLI Interface**: Packaged for easy global execution
40
40
 
41
- ## Instant Execution (Recommended)
41
+ ## Installation
42
+
43
+ ### From PyPI (Recommended)
44
+
45
+ Install from PyPI using `pip` or `uv`:
46
+
47
+ ```bash
48
+ # Using pip
49
+ pip install mem-brain-mcp
50
+
51
+ # Using uv
52
+ uv pip install mem-brain-mcp
53
+ ```
54
+
55
+ Then run globally:
56
+
57
+ ```bash
58
+ mem-brain-mcp
59
+ ```
60
+
61
+ ### Instant Execution with uvx
42
62
 
43
- You can run the MCP server instantly without manual installation using `uv`:
63
+ You can run the MCP server instantly without manual installation using `uvx`:
44
64
 
45
65
  ```bash
46
- # Run using uvx (loads environment from .env or shell)
66
+ # Run using uvx (uses default API URL)
47
67
  uvx mem-brain-mcp
48
68
 
49
- # Run with custom API URL
50
- export API_BASE_URL=http://your-api-alb-url.com
69
+ # Override API URL or set JWT token if needed
70
+ export API_BASE_URL=http://your-custom-api-url.com
71
+ export MEMBRAIN_API_KEY=your-jwt-token-here
51
72
  uvx mem-brain-mcp
52
73
  ```
53
74
 
54
- ## Installation
55
-
56
75
  ### From Source
57
76
 
58
77
  1. Install using `uv` (recommended) or `pip`:
@@ -70,13 +89,14 @@ mem-brain-mcp
70
89
 
71
90
  ## Configuration
72
91
 
73
- The server reads configuration from environment variables or a `.env` file in the current working directory:
92
+ The server reads configuration from environment variables or a `.env` file in the current working directory. Most settings have sensible defaults:
74
93
 
75
94
  ```env
76
- # API Configuration
77
- API_BASE_URL=http://localhost:8000
78
- # NOTE: API_KEY is optional here - per-user API keys are configured in MCP clients
79
- API_KEY=your_api_key_here # Optional: fallback for single-user scenarios
95
+ # API Configuration (optional - defaults to production API)
96
+ API_BASE_URL=http://membrain-api-alb-1094729422.ap-south-1.elb.amazonaws.com
97
+ # NOTE: MEMBRAIN_API_KEY is actually a JWT access token (from login/signup)
98
+ # Per-user JWT tokens are typically configured in MCP clients via headers
99
+ MEMBRAIN_API_KEY=your_jwt_token_here # Optional: fallback for single-user scenarios
80
100
 
81
101
  # MCP Server Configuration
82
102
  MCP_SERVER_HOST=0.0.0.0
@@ -86,9 +106,11 @@ MCP_SERVER_PORT=8100
86
106
  LOG_LEVEL=INFO
87
107
  ```
88
108
 
89
- ## Per-User API Key Configuration
109
+ **Note**: The `API_BASE_URL` defaults to the production Mem-Brain API endpoint, so you typically don't need to set it unless you're using a custom API instance.
110
+
111
+ ## Per-User JWT Token Configuration
90
112
 
91
- Each user must configure their own API key in their MCP client for proper user isolation. The server extracts tokens from request headers.
113
+ Each user must configure their own JWT access token in their MCP client for proper user isolation. The server extracts tokens from request headers. Get your JWT token by logging in or signing up to the Mem-Brain API.
92
114
 
93
115
  ### Cursor IDE (`~/.cursor/mcp.json`)
94
116
 
@@ -129,10 +151,20 @@ Add to your Claude Desktop configuration (`~/Library/Application Support/Claude/
129
151
  "mcpServers": {
130
152
  "mem-brain": {
131
153
  "command": "uvx",
132
- "args": ["mem-brain-mcp"],
154
+ "args": ["mem-brain-mcp"]
155
+ }
156
+ }
157
+ }
158
+ ```
159
+
160
+ **Note**: After installing from PyPI, you can also use `mem-brain-mcp` directly. The API URL is set by default, but you can override it if needed:
161
+ ```json
162
+ {
163
+ "mcpServers": {
164
+ "mem-brain": {
165
+ "command": "mem-brain-mcp",
133
166
  "env": {
134
- "API_BASE_URL": "http://your-deployed-url",
135
- "JWT_SECRET_KEY": "your-secret"
167
+ "API_BASE_URL": "http://your-custom-api-url"
136
168
  }
137
169
  }
138
170
  }
@@ -0,0 +1,9 @@
1
+ mem_brain_mcp/__init__.py,sha256=NMoDpXlRhGO858M6lrLqPN0fz7hKpqler4e96WniWxg,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=qnevDzJkt1rH-slV2r0pP_SeFkknZVqWRnZEKH_IeVo,66308
6
+ mem_brain_mcp-1.0.2.dist-info/METADATA,sha256=zLQMVD-sIRSH03dYAP6RovZH_49XqREG061Yf87ROW4,5228
7
+ mem_brain_mcp-1.0.2.dist-info/WHEEL,sha256=WLgqFyCfm_KASv4WHyYy0P3pM_m7J5L9k2skdKLirC8,87
8
+ mem_brain_mcp-1.0.2.dist-info/entry_points.txt,sha256=NH6QYQ-Sd8eJn5crpe_DL1PvGeUlL3y65968xPhmwG8,62
9
+ mem_brain_mcp-1.0.2.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=mu2HQPKkEjV_2QnlQ2gq7u0niXIvDCwpOgo7dRJjEcI,1055
5
- mem_brain_mcp/server.py,sha256=mlbl3-D3OFb4HHP54daqiP3nMWEdiTv3upEdUFnlhAA,39604
6
- mem_brain_mcp-1.0.0.dist-info/METADATA,sha256=xeGMlWratQ-1jPrI3A8E3hc_AdrxI9VLOBPZStgWmWk,4327
7
- mem_brain_mcp-1.0.0.dist-info/WHEEL,sha256=WLgqFyCfm_KASv4WHyYy0P3pM_m7J5L9k2skdKLirC8,87
8
- mem_brain_mcp-1.0.0.dist-info/entry_points.txt,sha256=NH6QYQ-Sd8eJn5crpe_DL1PvGeUlL3y65968xPhmwG8,62
9
- mem_brain_mcp-1.0.0.dist-info/RECORD,,