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