fix(mcp): validate UUID inputs and add sync_retain tool (#906)
* fix(mcp): validate UUID inputs at engine level and add sync_retain tool (#888) - Add UUID validation in memory_engine for get_memory_unit, delete_memory_unit, get_mental_model, delete_mental_model, get_mental_model_history (raises ValueError) - Catch ValueError → 400 in HTTP route handlers - Add sync_retain MCP tool that calls retain_batch_async directly for immediate availability (no polling needed) - Register sync_retain in _ALL_TOOLS, _SINGLE_BANK_TOOLS, UI MCP_TOOL_GROUPS - Add code-review check for MCP tool registration completeness * fix: remove UUID validation for mental model IDs (column is TEXT, not UUID) Mental model IDs are TEXT columns that accept arbitrary string IDs (e.g., 'team-communication-preferences'). UUID validation was incorrectly added to get_mental_model, delete_mental_model, and get_mental_model_history.
This commit is contained in:
parent
7e23f8e149
commit
48185a4bee
7 changed files with 218 additions and 6 deletions
|
|
@ -157,7 +157,16 @@ If any files in `hindsight-integrations/` were added or changed, verify:
|
||||||
- **Release process** — check that the integration name is in the `VALID_INTEGRATIONS` array in `scripts/release-integration.sh`. If missing, flag it.
|
- **Release process** — check that the integration name is in the `VALID_INTEGRATIONS` array in `scripts/release-integration.sh`. If missing, flag it.
|
||||||
- **Code standards** — the integration code must follow all Python style rules (type hints, no raw dicts, no tuple returns, etc.).
|
- **Code standards** — the integration code must follow all Python style rules (type hints, no raw dicts, no tuple returns, etc.).
|
||||||
|
|
||||||
### 10. Review against other coding standards
|
### 10. Check MCP tool registration completeness
|
||||||
|
|
||||||
|
If any new MCP tools were added or existing tools renamed in `hindsight-api-slim/hindsight_api/mcp_tools.py`:
|
||||||
|
- **`_ALL_TOOLS` set** in `mcp_tools.py` — must include the new tool name
|
||||||
|
- **`tools_to_register` default set** in `register_mcp_tools()` in `mcp_tools.py` — must include the new tool name
|
||||||
|
- **`_SINGLE_BANK_TOOLS` set** in `hindsight-api-slim/hindsight_api/api/mcp.py` — must include the new tool if it is bank-scoped (not a bank-management tool like `list_banks`/`create_bank`)
|
||||||
|
- **`MCP_TOOL_GROUPS`** in `hindsight-control-plane/src/components/bank-config-view.tsx` — must include the new tool in the appropriate group for the UI tool selector
|
||||||
|
- **Tool count assertions** in tests (e.g., `test_mcp_tools.py`) — must be updated to reflect the new count
|
||||||
|
|
||||||
|
### 11. Review against other coding standards
|
||||||
|
|
||||||
Check the diff for violations of the standards listed above:
|
Check the diff for violations of the standards listed above:
|
||||||
- Python files at project root (not allowed)
|
- Python files at project root (not allowed)
|
||||||
|
|
@ -169,7 +178,7 @@ Check the diff for violations of the standards listed above:
|
||||||
- Premature abstractions or speculative helpers
|
- Premature abstractions or speculative helpers
|
||||||
- Backwards-compatibility hacks (unused vars, re-exports, "removed" comments)
|
- Backwards-compatibility hacks (unused vars, re-exports, "removed" comments)
|
||||||
|
|
||||||
### 11. Report findings
|
### 12. Report findings
|
||||||
|
|
||||||
Present a clear summary organized by severity:
|
Present a clear summary organized by severity:
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -2679,6 +2679,8 @@ def _register_routes(app: FastAPI):
|
||||||
return data
|
return data
|
||||||
except OperationValidationError as e:
|
except OperationValidationError as e:
|
||||||
raise HTTPException(status_code=e.status_code, detail=e.reason)
|
raise HTTPException(status_code=e.status_code, detail=e.reason)
|
||||||
|
except ValueError as e:
|
||||||
|
raise HTTPException(status_code=400, detail=str(e))
|
||||||
except (AuthenticationError, HTTPException):
|
except (AuthenticationError, HTTPException):
|
||||||
raise
|
raise
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
|
|
@ -3289,6 +3291,8 @@ def _register_routes(app: FastAPI):
|
||||||
raise
|
raise
|
||||||
except OperationValidationError as e:
|
except OperationValidationError as e:
|
||||||
raise HTTPException(status_code=e.status_code, detail=e.reason)
|
raise HTTPException(status_code=e.status_code, detail=e.reason)
|
||||||
|
except ValueError as e:
|
||||||
|
raise HTTPException(status_code=400, detail=str(e))
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
import traceback
|
import traceback
|
||||||
|
|
||||||
|
|
@ -3322,6 +3326,8 @@ def _register_routes(app: FastAPI):
|
||||||
raise
|
raise
|
||||||
except OperationValidationError as e:
|
except OperationValidationError as e:
|
||||||
raise HTTPException(status_code=e.status_code, detail=e.reason)
|
raise HTTPException(status_code=e.status_code, detail=e.reason)
|
||||||
|
except ValueError as e:
|
||||||
|
raise HTTPException(status_code=400, detail=str(e))
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
import traceback
|
import traceback
|
||||||
|
|
||||||
|
|
@ -3486,6 +3492,8 @@ def _register_routes(app: FastAPI):
|
||||||
return {"status": "deleted"}
|
return {"status": "deleted"}
|
||||||
except OperationValidationError as e:
|
except OperationValidationError as e:
|
||||||
raise HTTPException(status_code=e.status_code, detail=e.reason)
|
raise HTTPException(status_code=e.status_code, detail=e.reason)
|
||||||
|
except ValueError as e:
|
||||||
|
raise HTTPException(status_code=400, detail=str(e))
|
||||||
except (AuthenticationError, HTTPException):
|
except (AuthenticationError, HTTPException):
|
||||||
raise
|
raise
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
|
|
|
||||||
|
|
@ -97,6 +97,7 @@ def create_mcp_server(memory: MemoryEngine, multi_bank: bool = True) -> FastMCP:
|
||||||
_SINGLE_BANK_TOOLS: frozenset[str] = frozenset(
|
_SINGLE_BANK_TOOLS: frozenset[str] = frozenset(
|
||||||
{
|
{
|
||||||
"retain",
|
"retain",
|
||||||
|
"sync_retain",
|
||||||
"recall",
|
"recall",
|
||||||
"reflect",
|
"reflect",
|
||||||
"list_mental_models",
|
"list_mental_models",
|
||||||
|
|
|
||||||
|
|
@ -3792,7 +3792,14 @@ class MemoryEngine(MemoryEngineInterface):
|
||||||
|
|
||||||
Returns:
|
Returns:
|
||||||
Dictionary with deletion result
|
Dictionary with deletion result
|
||||||
|
|
||||||
|
Raises:
|
||||||
|
ValueError: If unit_id is not a valid UUID
|
||||||
"""
|
"""
|
||||||
|
try:
|
||||||
|
unit_uuid = uuid.UUID(unit_id)
|
||||||
|
except ValueError:
|
||||||
|
raise ValueError(f"Invalid unit_id: '{unit_id}' is not a valid UUID")
|
||||||
await self._authenticate_tenant(request_context)
|
await self._authenticate_tenant(request_context)
|
||||||
pool = await self._get_pool()
|
pool = await self._get_pool()
|
||||||
invalidated_obs = 0
|
invalidated_obs = 0
|
||||||
|
|
@ -3802,7 +3809,7 @@ class MemoryEngine(MemoryEngineInterface):
|
||||||
# Get bank_id and fact_type before deletion
|
# Get bank_id and fact_type before deletion
|
||||||
row = await conn.fetchrow(
|
row = await conn.fetchrow(
|
||||||
f"SELECT bank_id, fact_type FROM {fq_table('memory_units')} WHERE id = $1",
|
f"SELECT bank_id, fact_type FROM {fq_table('memory_units')} WHERE id = $1",
|
||||||
unit_id,
|
str(unit_uuid),
|
||||||
)
|
)
|
||||||
bank_id = row["bank_id"] if row else None
|
bank_id = row["bank_id"] if row else None
|
||||||
fact_type = row["fact_type"] if row else None
|
fact_type = row["fact_type"] if row else None
|
||||||
|
|
@ -4697,7 +4704,14 @@ class MemoryEngine(MemoryEngineInterface):
|
||||||
|
|
||||||
Returns:
|
Returns:
|
||||||
Dict with memory unit data or None if not found
|
Dict with memory unit data or None if not found
|
||||||
|
|
||||||
|
Raises:
|
||||||
|
ValueError: If memory_id is not a valid UUID
|
||||||
"""
|
"""
|
||||||
|
try:
|
||||||
|
memory_uuid = uuid.UUID(memory_id)
|
||||||
|
except ValueError:
|
||||||
|
raise ValueError(f"Invalid memory_id: '{memory_id}' is not a valid UUID")
|
||||||
await self._authenticate_tenant(request_context)
|
await self._authenticate_tenant(request_context)
|
||||||
if self._operation_validator:
|
if self._operation_validator:
|
||||||
from hindsight_api.extensions import BankReadContext
|
from hindsight_api.extensions import BankReadContext
|
||||||
|
|
@ -4715,7 +4729,7 @@ class MemoryEngine(MemoryEngineInterface):
|
||||||
FROM {fq_table("memory_units")}
|
FROM {fq_table("memory_units")}
|
||||||
WHERE id = $1 AND bank_id = $2
|
WHERE id = $1 AND bank_id = $2
|
||||||
""",
|
""",
|
||||||
memory_id,
|
str(memory_uuid),
|
||||||
bank_id,
|
bank_id,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
@ -6497,6 +6511,7 @@ class MemoryEngine(MemoryEngineInterface):
|
||||||
|
|
||||||
Returns None if the mental model is not found.
|
Returns None if the mental model is not found.
|
||||||
Returns a list of history entries (most recent first), each with previous_content and changed_at.
|
Returns a list of history entries (most recent first), each with previous_content and changed_at.
|
||||||
|
|
||||||
"""
|
"""
|
||||||
await self._authenticate_tenant(request_context)
|
await self._authenticate_tenant(request_context)
|
||||||
pool = await self._get_pool()
|
pool = await self._get_pool()
|
||||||
|
|
|
||||||
|
|
@ -29,6 +29,7 @@ from hindsight_api.models import RequestContext
|
||||||
_ALL_TOOLS: frozenset[str] = frozenset(
|
_ALL_TOOLS: frozenset[str] = frozenset(
|
||||||
{
|
{
|
||||||
"retain",
|
"retain",
|
||||||
|
"sync_retain",
|
||||||
"recall",
|
"recall",
|
||||||
"reflect",
|
"reflect",
|
||||||
"list_banks",
|
"list_banks",
|
||||||
|
|
@ -202,6 +203,7 @@ def register_mcp_tools(
|
||||||
"""
|
"""
|
||||||
tools_to_register = config.tools or {
|
tools_to_register = config.tools or {
|
||||||
"retain",
|
"retain",
|
||||||
|
"sync_retain",
|
||||||
"recall",
|
"recall",
|
||||||
"reflect",
|
"reflect",
|
||||||
"list_banks",
|
"list_banks",
|
||||||
|
|
@ -235,6 +237,9 @@ def register_mcp_tools(
|
||||||
if "retain" in tools_to_register:
|
if "retain" in tools_to_register:
|
||||||
_register_retain(mcp, memory, config)
|
_register_retain(mcp, memory, config)
|
||||||
|
|
||||||
|
if "sync_retain" in tools_to_register:
|
||||||
|
_register_sync_retain(mcp, memory, config)
|
||||||
|
|
||||||
if "recall" in tools_to_register:
|
if "recall" in tools_to_register:
|
||||||
_register_recall(mcp, memory, config)
|
_register_recall(mcp, memory, config)
|
||||||
|
|
||||||
|
|
@ -630,6 +635,124 @@ def _register_retain(mcp: FastMCP, memory: MemoryEngine, config: MCPToolsConfig)
|
||||||
return {"status": "error", "message": str(e)}
|
return {"status": "error", "message": str(e)}
|
||||||
|
|
||||||
|
|
||||||
|
def _register_sync_retain(mcp: FastMCP, memory: MemoryEngine, config: MCPToolsConfig) -> None:
|
||||||
|
"""Register the sync_retain tool (synchronous retain that waits for completion)."""
|
||||||
|
|
||||||
|
if config.include_bank_id_param:
|
||||||
|
|
||||||
|
@mcp.tool()
|
||||||
|
async def sync_retain(
|
||||||
|
content: str,
|
||||||
|
context: str = "general",
|
||||||
|
timestamp: str | None = None,
|
||||||
|
tags: list[str] | None = None,
|
||||||
|
metadata: dict[str, str] | None = None,
|
||||||
|
document_id: str | None = None,
|
||||||
|
bank_id: str | None = None,
|
||||||
|
strategy: str | None = None,
|
||||||
|
) -> dict:
|
||||||
|
"""Store information to long-term memory and wait for completion.
|
||||||
|
|
||||||
|
Unlike retain (which is asynchronous), this tool blocks until the memory
|
||||||
|
is fully stored and immediately available for recall.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
content: The fact/memory to store (be specific and include relevant details)
|
||||||
|
context: Category for the memory (e.g., 'preferences', 'work', 'hobbies', 'family'). Default: 'general'
|
||||||
|
timestamp: When this event/fact occurred (ISO format, e.g., '2024-01-15T10:30:00Z'). Useful for timeline tracking.
|
||||||
|
tags: Optional tags for scoped visibility filtering (e.g., ['project:alpha', 'user:123'])
|
||||||
|
metadata: Optional key-value metadata to attach (e.g., {'source': 'slack', 'channel': 'general'})
|
||||||
|
document_id: Optional document ID to associate this memory with
|
||||||
|
bank_id: Optional bank to store in (defaults to session bank). Use for cross-bank operations.
|
||||||
|
strategy: Optional named retain strategy (e.g., 'exact' for verbatim storage). Strategies are defined in the bank config.
|
||||||
|
"""
|
||||||
|
target_bank = bank_id or config.bank_id_resolver()
|
||||||
|
if target_bank is None:
|
||||||
|
return {"status": "error", "message": "No bank_id configured"}
|
||||||
|
|
||||||
|
content_dict, error = build_content_dict(content, context, timestamp, tags, metadata, document_id, strategy)
|
||||||
|
if error:
|
||||||
|
return {"status": "error", "message": error}
|
||||||
|
|
||||||
|
request_context = _get_request_context(config)
|
||||||
|
|
||||||
|
try:
|
||||||
|
result = await memory.retain_batch_async(
|
||||||
|
bank_id=target_bank,
|
||||||
|
contents=[content_dict],
|
||||||
|
request_context=request_context,
|
||||||
|
strategy=content_dict.pop("strategy", None),
|
||||||
|
)
|
||||||
|
memory_ids = [uid for batch in result for uid in batch]
|
||||||
|
return {
|
||||||
|
"status": "completed",
|
||||||
|
"message": "Memory stored successfully",
|
||||||
|
"memory_ids": memory_ids,
|
||||||
|
}
|
||||||
|
except OperationValidationError as e:
|
||||||
|
logger.warning(f"Sync retain rejected: {e}")
|
||||||
|
return {"status": "error", "message": str(e)}
|
||||||
|
except Exception as e:
|
||||||
|
logger.error(f"Error in sync retain: {e}", exc_info=True)
|
||||||
|
return {"status": "error", "message": str(e)}
|
||||||
|
|
||||||
|
else:
|
||||||
|
|
||||||
|
@mcp.tool()
|
||||||
|
async def sync_retain(
|
||||||
|
content: str,
|
||||||
|
context: str = "general",
|
||||||
|
timestamp: str | None = None,
|
||||||
|
tags: list[str] | None = None,
|
||||||
|
metadata: dict[str, str] | None = None,
|
||||||
|
document_id: str | None = None,
|
||||||
|
strategy: str | None = None,
|
||||||
|
) -> dict:
|
||||||
|
"""Store information to long-term memory and wait for completion.
|
||||||
|
|
||||||
|
Unlike retain (which is asynchronous), this tool blocks until the memory
|
||||||
|
is fully stored and immediately available for recall.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
content: The fact/memory to store (be specific and include relevant details)
|
||||||
|
context: Category for the memory (e.g., 'preferences', 'work', 'hobbies', 'family'). Default: 'general'
|
||||||
|
timestamp: When this event/fact occurred (ISO format, e.g., '2024-01-15T10:30:00Z'). Useful for timeline tracking.
|
||||||
|
tags: Optional tags for scoped visibility filtering (e.g., ['project:alpha', 'user:123'])
|
||||||
|
metadata: Optional key-value metadata to attach (e.g., {'source': 'slack', 'channel': 'general'})
|
||||||
|
document_id: Optional document ID to associate this memory with
|
||||||
|
strategy: Optional named retain strategy (e.g., 'exact' for verbatim storage). Strategies are defined in the bank config.
|
||||||
|
"""
|
||||||
|
target_bank = config.bank_id_resolver()
|
||||||
|
if target_bank is None:
|
||||||
|
return {"status": "error", "message": "No bank_id configured"}
|
||||||
|
|
||||||
|
content_dict, error = build_content_dict(content, context, timestamp, tags, metadata, document_id, strategy)
|
||||||
|
if error:
|
||||||
|
return {"status": "error", "message": error}
|
||||||
|
|
||||||
|
request_context = _get_request_context(config)
|
||||||
|
|
||||||
|
try:
|
||||||
|
result = await memory.retain_batch_async(
|
||||||
|
bank_id=target_bank,
|
||||||
|
contents=[content_dict],
|
||||||
|
request_context=request_context,
|
||||||
|
strategy=content_dict.pop("strategy", None),
|
||||||
|
)
|
||||||
|
memory_ids = [uid for batch in result for uid in batch]
|
||||||
|
return {
|
||||||
|
"status": "completed",
|
||||||
|
"message": "Memory stored successfully",
|
||||||
|
"memory_ids": memory_ids,
|
||||||
|
}
|
||||||
|
except OperationValidationError as e:
|
||||||
|
logger.warning(f"Sync retain rejected: {e}")
|
||||||
|
return {"status": "error", "message": str(e)}
|
||||||
|
except Exception as e:
|
||||||
|
logger.error(f"Error in sync retain: {e}", exc_info=True)
|
||||||
|
return {"status": "error", "message": str(e)}
|
||||||
|
|
||||||
|
|
||||||
def _register_recall(mcp: FastMCP, memory: MemoryEngine, config: MCPToolsConfig) -> None:
|
def _register_recall(mcp: FastMCP, memory: MemoryEngine, config: MCPToolsConfig) -> None:
|
||||||
"""Register the recall tool."""
|
"""Register the recall tool."""
|
||||||
description = config.recall_description or DEFAULT_MCP_RECALL_DESCRIPTION
|
description = config.recall_description or DEFAULT_MCP_RECALL_DESCRIPTION
|
||||||
|
|
|
||||||
|
|
@ -342,7 +342,8 @@ class TestMentalModelToolRegistration:
|
||||||
assert "update_bank" in tools
|
assert "update_bank" in tools
|
||||||
assert "delete_bank" in tools
|
assert "delete_bank" in tools
|
||||||
assert "clear_memories" in tools
|
assert "clear_memories" in tools
|
||||||
assert len(tools) == 29
|
assert "sync_retain" in tools
|
||||||
|
assert len(tools) == 30
|
||||||
|
|
||||||
|
|
||||||
@pytest.fixture
|
@pytest.fixture
|
||||||
|
|
@ -1107,12 +1108,67 @@ class TestMemoryBrowsingTools:
|
||||||
assert '"deleted"' in result
|
assert '"deleted"' in result
|
||||||
assert mock_memory.delete_memory_unit.call_args.kwargs["unit_id"] == "mem-1"
|
assert mock_memory.delete_memory_unit.call_args.kwargs["unit_id"] == "mem-1"
|
||||||
|
|
||||||
|
async def test_get_memory_invalid_uuid(self, mock_memory):
|
||||||
|
mock_memory.get_memory_unit.side_effect = ValueError("Invalid memory_id: 'nonexistent' is not a valid UUID")
|
||||||
|
mcp = _make_mcp_server(mock_memory, {"get_memory"}, include_bank_id=True)
|
||||||
|
result = await _tools(mcp)["get_memory"].fn(memory_id="nonexistent")
|
||||||
|
assert "not a valid UUID" in result
|
||||||
|
|
||||||
|
async def test_get_memory_invalid_uuid_single_bank(self, mock_memory):
|
||||||
|
mock_memory.get_memory_unit.side_effect = ValueError("Invalid memory_id: 'bad' is not a valid UUID")
|
||||||
|
mcp = _make_mcp_server(mock_memory, {"get_memory"}, include_bank_id=False)
|
||||||
|
result = await _tools(mcp)["get_memory"].fn(memory_id="bad")
|
||||||
|
assert "not a valid UUID" in result["error"]
|
||||||
|
|
||||||
|
async def test_delete_memory_invalid_uuid(self, mock_memory):
|
||||||
|
mock_memory.delete_memory_unit.side_effect = ValueError("Invalid unit_id: 'bad' is not a valid UUID")
|
||||||
|
mcp = _make_mcp_server(mock_memory, {"delete_memory"}, include_bank_id=True)
|
||||||
|
result = await _tools(mcp)["delete_memory"].fn(memory_id="bad")
|
||||||
|
assert "not a valid UUID" in result
|
||||||
|
|
||||||
async def test_list_memories_single_bank(self, mock_memory):
|
async def test_list_memories_single_bank(self, mock_memory):
|
||||||
mcp = _make_mcp_server(mock_memory, {"list_memories"}, include_bank_id=False)
|
mcp = _make_mcp_server(mock_memory, {"list_memories"}, include_bank_id=False)
|
||||||
result = await _tools(mcp)["list_memories"].fn()
|
result = await _tools(mcp)["list_memories"].fn()
|
||||||
assert isinstance(result, dict)
|
assert isinstance(result, dict)
|
||||||
|
|
||||||
|
|
||||||
|
# =========================================================================
|
||||||
|
# Sync Retain Tool Tests
|
||||||
|
# =========================================================================
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.asyncio
|
||||||
|
class TestSyncRetainTool:
|
||||||
|
async def test_sync_retain_basic(self, mock_memory):
|
||||||
|
mock_memory.retain_batch_async.return_value = [["unit-1", "unit-2"]]
|
||||||
|
mcp = _make_mcp_server(mock_memory, {"sync_retain"}, include_bank_id=True)
|
||||||
|
result = await _tools(mcp)["sync_retain"].fn(content="test memory")
|
||||||
|
assert result["status"] == "completed"
|
||||||
|
assert result["memory_ids"] == ["unit-1", "unit-2"]
|
||||||
|
|
||||||
|
async def test_sync_retain_single_bank(self, mock_memory):
|
||||||
|
mock_memory.retain_batch_async.return_value = [["unit-1"]]
|
||||||
|
mcp = _make_mcp_server(mock_memory, {"sync_retain"}, include_bank_id=False)
|
||||||
|
result = await _tools(mcp)["sync_retain"].fn(content="test memory")
|
||||||
|
assert result["status"] == "completed"
|
||||||
|
assert result["memory_ids"] == ["unit-1"]
|
||||||
|
|
||||||
|
async def test_sync_retain_with_tags(self, mock_memory):
|
||||||
|
mock_memory.retain_batch_async.return_value = [["unit-1"]]
|
||||||
|
mcp = _make_mcp_server(mock_memory, {"sync_retain"}, include_bank_id=True)
|
||||||
|
result = await _tools(mcp)["sync_retain"].fn(content="test", tags=["project:alpha"])
|
||||||
|
assert result["status"] == "completed"
|
||||||
|
call_kwargs = mock_memory.retain_batch_async.call_args.kwargs
|
||||||
|
assert call_kwargs["contents"][0]["tags"] == ["project:alpha"]
|
||||||
|
|
||||||
|
async def test_sync_retain_error(self, mock_memory):
|
||||||
|
mock_memory.retain_batch_async.side_effect = Exception("DB error")
|
||||||
|
mcp = _make_mcp_server(mock_memory, {"sync_retain"}, include_bank_id=True)
|
||||||
|
result = await _tools(mcp)["sync_retain"].fn(content="test")
|
||||||
|
assert result["status"] == "error"
|
||||||
|
assert "DB error" in result["message"]
|
||||||
|
|
||||||
|
|
||||||
# =========================================================================
|
# =========================================================================
|
||||||
# Document Tool Tests
|
# Document Tool Tests
|
||||||
# =========================================================================
|
# =========================================================================
|
||||||
|
|
|
||||||
|
|
@ -112,7 +112,7 @@ const DEFAULT_GEMINI_SAFETY_SETTINGS: GeminiSafetySetting[] = GEMINI_HARM_CATEGO
|
||||||
// ─── MCP tool catalogue ───────────────────────────────────────────────────────
|
// ─── MCP tool catalogue ───────────────────────────────────────────────────────
|
||||||
|
|
||||||
const MCP_TOOL_GROUPS: { label: string; tools: string[] }[] = [
|
const MCP_TOOL_GROUPS: { label: string; tools: string[] }[] = [
|
||||||
{ label: "Core", tools: ["retain", "recall", "reflect"] },
|
{ label: "Core", tools: ["retain", "sync_retain", "recall", "reflect"] },
|
||||||
{
|
{
|
||||||
label: "Bank management",
|
label: "Bank management",
|
||||||
tools: [
|
tools: [
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue