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