feat: expand MCP tool surface area with 18 new tools and enhanced parameters (#435)
Add directives, memory browsing, documents, operations, tags, and bank management tools to the MCP server. Expose previously hardcoded parameters (budget, types, tags, response_schema, trigger) on retain, recall, reflect, and mental model tools. Update docs for all new tools and parameters. Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
parent
0aa7c2b3a1
commit
3ffec65090
6 changed files with 2367 additions and 71 deletions
|
|
@ -101,7 +101,24 @@ def create_mcp_server(memory: MemoryEngine, multi_bank: bool = True) -> FastMCP:
|
|||
"update_mental_model",
|
||||
"delete_mental_model",
|
||||
"refresh_mental_model",
|
||||
}, # Scoped tools for single-bank mode (excludes bank management: list_banks, create_bank)
|
||||
"list_directives",
|
||||
"create_directive",
|
||||
"delete_directive",
|
||||
"list_memories",
|
||||
"get_memory",
|
||||
"delete_memory",
|
||||
"list_documents",
|
||||
"get_document",
|
||||
"delete_document",
|
||||
"list_operations",
|
||||
"get_operation",
|
||||
"cancel_operation",
|
||||
"list_tags",
|
||||
"get_bank",
|
||||
"update_bank",
|
||||
"delete_bank",
|
||||
"clear_memories",
|
||||
}, # Scoped tools for single-bank mode (excludes multi-bank management: list_banks, create_bank, get_bank_stats)
|
||||
retain_fire_and_forget=False, # HTTP MCP supports sync/async modes
|
||||
)
|
||||
|
||||
|
|
|
|||
File diff suppressed because it is too large
Load diff
|
|
@ -165,5 +165,5 @@ class TestMCPExtensionIntegration:
|
|||
assert "create_bank" in tools
|
||||
# Extension tool also present
|
||||
assert "test_extension_tool" in tools
|
||||
# At least 11 core + 1 extension = 12 tools (may grow as new tools are added)
|
||||
assert len(tools) >= 12
|
||||
# At least 29 core + 1 extension = 30 tools (may grow as new tools are added)
|
||||
assert len(tools) >= 30
|
||||
|
|
|
|||
|
|
@ -77,8 +77,9 @@ class TestBuildContentDict:
|
|||
|
||||
@pytest.fixture
|
||||
def mock_memory():
|
||||
"""Create a mock MemoryEngine with mental model methods."""
|
||||
"""Create a mock MemoryEngine with all MCP tool methods."""
|
||||
memory = MagicMock()
|
||||
# Mental model methods
|
||||
memory.list_mental_models = AsyncMock(
|
||||
return_value=[
|
||||
{"id": "mm-1", "name": "Coding Prefs", "source_query": "coding preferences?", "content": "Prefers Python"},
|
||||
|
|
@ -104,6 +105,41 @@ def mock_memory():
|
|||
}
|
||||
)
|
||||
memory.delete_mental_model = AsyncMock(return_value=True)
|
||||
|
||||
# Retain/recall/reflect
|
||||
memory.retain_batch_async = AsyncMock()
|
||||
memory.submit_async_retain = AsyncMock(return_value={"operation_id": "op-retain"})
|
||||
memory.recall_async = AsyncMock(return_value=MagicMock(model_dump_json=lambda indent=None: '{"results": []}', model_dump=lambda: {"results": []}))
|
||||
memory.reflect_async = AsyncMock(return_value=MagicMock(model_dump_json=lambda indent=None: '{"text": "reflection"}', model_dump=lambda: {"text": "reflection"}, structured_output=None))
|
||||
|
||||
# Directive methods
|
||||
memory.list_directives = AsyncMock(return_value=[{"id": "dir-1", "name": "Be concise", "content": "Keep responses short"}])
|
||||
memory.create_directive = AsyncMock(return_value={"id": "dir-new", "name": "Test", "content": "Test content"})
|
||||
memory.delete_directive = AsyncMock(return_value=True)
|
||||
|
||||
# Memory browsing methods
|
||||
memory.list_memory_units = AsyncMock(return_value={"items": [{"id": "mem-1", "content": "Test"}], "total": 1})
|
||||
memory.get_memory_unit = AsyncMock(return_value={"id": "mem-1", "content": "Test memory"})
|
||||
memory.delete_memory_unit = AsyncMock(return_value={"deleted_count": 1})
|
||||
|
||||
# Document methods
|
||||
memory.list_documents = AsyncMock(return_value={"items": [{"id": "doc-1", "name": "Test Doc"}], "total": 1})
|
||||
memory.get_document = AsyncMock(return_value={"id": "doc-1", "name": "Test Doc"})
|
||||
memory.delete_document = AsyncMock(return_value={"deleted_memories": 5})
|
||||
|
||||
# Operation methods
|
||||
memory.list_operations = AsyncMock(return_value={"items": [{"id": "op-1", "status": "completed"}]})
|
||||
memory.get_operation_status = AsyncMock(return_value={"id": "op-1", "status": "completed", "progress": 100})
|
||||
memory.cancel_operation = AsyncMock(return_value={"id": "op-1", "status": "cancelled"})
|
||||
|
||||
# Tags & bank methods
|
||||
memory.list_tags = AsyncMock(return_value={"items": ["tag1", "tag2"], "total": 2})
|
||||
memory.get_bank_profile = AsyncMock(return_value={"id": "test-bank", "name": "Test Bank", "mission": "Testing"})
|
||||
memory.get_bank_stats = AsyncMock(return_value={"nodes": 100, "links": 50})
|
||||
memory.update_bank = AsyncMock(return_value={"id": "test-bank", "name": "Updated"})
|
||||
memory.delete_bank = AsyncMock(return_value={"deleted_memories": 10, "deleted_entities": 5})
|
||||
memory.list_banks = AsyncMock(return_value=[])
|
||||
|
||||
return memory
|
||||
|
||||
|
||||
|
|
@ -211,7 +247,7 @@ class TestMentalModelToolRegistration:
|
|||
assert request_context.api_key == "test-api-key"
|
||||
|
||||
def test_mental_model_tools_in_default_set(self):
|
||||
"""Mental model tools should be in the default tools set when config.tools is None."""
|
||||
"""All tools should be in the default tools set when config.tools is None."""
|
||||
from fastmcp import FastMCP
|
||||
|
||||
memory = MagicMock()
|
||||
|
|
@ -229,6 +265,21 @@ class TestMentalModelToolRegistration:
|
|||
memory.submit_async_refresh_mental_model = AsyncMock()
|
||||
memory.update_mental_model = AsyncMock()
|
||||
memory.delete_mental_model = AsyncMock()
|
||||
memory.list_directives = AsyncMock(return_value=[])
|
||||
memory.create_directive = AsyncMock()
|
||||
memory.delete_directive = AsyncMock()
|
||||
memory.list_memory_units = AsyncMock(return_value={})
|
||||
memory.get_memory_unit = AsyncMock()
|
||||
memory.delete_memory_unit = AsyncMock()
|
||||
memory.list_documents = AsyncMock(return_value={})
|
||||
memory.get_document = AsyncMock()
|
||||
memory.delete_document = AsyncMock()
|
||||
memory.list_operations = AsyncMock(return_value={})
|
||||
memory.get_operation_status = AsyncMock()
|
||||
memory.cancel_operation = AsyncMock()
|
||||
memory.list_tags = AsyncMock(return_value={})
|
||||
memory.get_bank_stats = AsyncMock(return_value={})
|
||||
memory.delete_bank = AsyncMock(return_value={})
|
||||
|
||||
mcp = FastMCP("test", stateless_http=True)
|
||||
config = MCPToolsConfig(
|
||||
|
|
@ -241,6 +292,18 @@ class TestMentalModelToolRegistration:
|
|||
assert "list_mental_models" in tools
|
||||
assert "create_mental_model" in tools
|
||||
assert "refresh_mental_model" in tools
|
||||
# New tools
|
||||
assert "list_directives" in tools
|
||||
assert "list_memories" in tools
|
||||
assert "list_documents" in tools
|
||||
assert "list_operations" in tools
|
||||
assert "list_tags" in tools
|
||||
assert "get_bank" in tools
|
||||
assert "get_bank_stats" in tools
|
||||
assert "update_bank" in tools
|
||||
assert "delete_bank" in tools
|
||||
assert "clear_memories" in tools
|
||||
assert len(tools) == 29
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
|
|
@ -644,3 +707,542 @@ class TestMentalModelInputValidation:
|
|||
result = await _tools(mcp_server_single_bank)["get_mental_model"].fn(mental_model_id="missing")
|
||||
assert isinstance(result, dict)
|
||||
assert "fixed-bank" in result["error"]
|
||||
|
||||
|
||||
# =========================================================================
|
||||
# New Parameter Tests for Existing Tools
|
||||
# =========================================================================
|
||||
|
||||
|
||||
def _make_mcp_server(mock_memory, tools, include_bank_id=True):
|
||||
"""Helper to create an MCP server with specific tools."""
|
||||
from fastmcp import FastMCP
|
||||
|
||||
mcp = FastMCP("test", stateless_http=True)
|
||||
config = MCPToolsConfig(
|
||||
bank_id_resolver=lambda: "test-bank",
|
||||
include_bank_id_param=include_bank_id,
|
||||
tools=tools,
|
||||
)
|
||||
register_mcp_tools(mcp, mock_memory, config)
|
||||
return mcp
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
class TestRetainNewParams:
|
||||
"""Tests for new retain parameters: tags, metadata, document_id."""
|
||||
|
||||
async def test_retain_with_tags(self, mock_memory):
|
||||
mcp = _make_mcp_server(mock_memory, {"retain"})
|
||||
await _tools(mcp)["retain"].fn(content="test", tags=["user:123", "project:alpha"])
|
||||
call_args = mock_memory.submit_async_retain.call_args
|
||||
contents = call_args.kwargs["contents"]
|
||||
assert contents[0]["tags"] == ["user:123", "project:alpha"]
|
||||
|
||||
async def test_retain_with_metadata(self, mock_memory):
|
||||
mcp = _make_mcp_server(mock_memory, {"retain"})
|
||||
await _tools(mcp)["retain"].fn(content="test", metadata={"source": "slack"})
|
||||
call_args = mock_memory.submit_async_retain.call_args
|
||||
contents = call_args.kwargs["contents"]
|
||||
assert contents[0]["metadata"] == {"source": "slack"}
|
||||
|
||||
async def test_retain_with_document_id(self, mock_memory):
|
||||
mcp = _make_mcp_server(mock_memory, {"retain"})
|
||||
await _tools(mcp)["retain"].fn(content="test", document_id="doc-1")
|
||||
call_args = mock_memory.submit_async_retain.call_args
|
||||
contents = call_args.kwargs["contents"]
|
||||
assert contents[0]["document_id"] == "doc-1"
|
||||
|
||||
async def test_retain_without_new_params_backward_compat(self, mock_memory):
|
||||
"""Existing behavior preserved when new params not provided."""
|
||||
mcp = _make_mcp_server(mock_memory, {"retain"})
|
||||
await _tools(mcp)["retain"].fn(content="test")
|
||||
call_args = mock_memory.submit_async_retain.call_args
|
||||
contents = call_args.kwargs["contents"]
|
||||
assert "tags" not in contents[0]
|
||||
assert "metadata" not in contents[0]
|
||||
assert "document_id" not in contents[0]
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
class TestRecallNewParams:
|
||||
"""Tests for new recall parameters: budget, types, tags, tags_match, query_timestamp."""
|
||||
|
||||
async def test_recall_default_budget_high(self, mock_memory):
|
||||
"""Default budget should be HIGH (backward compat)."""
|
||||
from hindsight_api.engine.memory_engine import Budget
|
||||
|
||||
mcp = _make_mcp_server(mock_memory, {"recall"})
|
||||
await _tools(mcp)["recall"].fn(query="test")
|
||||
call_kwargs = mock_memory.recall_async.call_args.kwargs
|
||||
assert call_kwargs["budget"] == Budget.HIGH
|
||||
|
||||
async def test_recall_budget_low(self, mock_memory):
|
||||
from hindsight_api.engine.memory_engine import Budget
|
||||
|
||||
mcp = _make_mcp_server(mock_memory, {"recall"})
|
||||
await _tools(mcp)["recall"].fn(query="test", budget="low")
|
||||
call_kwargs = mock_memory.recall_async.call_args.kwargs
|
||||
assert call_kwargs["budget"] == Budget.LOW
|
||||
|
||||
async def test_recall_with_types(self, mock_memory):
|
||||
mcp = _make_mcp_server(mock_memory, {"recall"})
|
||||
await _tools(mcp)["recall"].fn(query="test", types=["world"])
|
||||
call_kwargs = mock_memory.recall_async.call_args.kwargs
|
||||
assert call_kwargs["fact_type"] == ["world"]
|
||||
|
||||
async def test_recall_default_types_all(self, mock_memory):
|
||||
from hindsight_api.engine.response_models import VALID_RECALL_FACT_TYPES
|
||||
|
||||
mcp = _make_mcp_server(mock_memory, {"recall"})
|
||||
await _tools(mcp)["recall"].fn(query="test")
|
||||
call_kwargs = mock_memory.recall_async.call_args.kwargs
|
||||
assert call_kwargs["fact_type"] == list(VALID_RECALL_FACT_TYPES)
|
||||
|
||||
async def test_recall_with_tags(self, mock_memory):
|
||||
mcp = _make_mcp_server(mock_memory, {"recall"})
|
||||
await _tools(mcp)["recall"].fn(query="test", tags=["project:x"])
|
||||
call_kwargs = mock_memory.recall_async.call_args.kwargs
|
||||
assert call_kwargs["tags"] == ["project:x"]
|
||||
assert call_kwargs["tags_match"] == "any"
|
||||
|
||||
async def test_recall_with_query_timestamp(self, mock_memory):
|
||||
mcp = _make_mcp_server(mock_memory, {"recall"})
|
||||
await _tools(mcp)["recall"].fn(query="test", query_timestamp="2024-01-01T00:00:00Z")
|
||||
call_kwargs = mock_memory.recall_async.call_args.kwargs
|
||||
assert call_kwargs["question_date"] == datetime(2024, 1, 1, 0, 0, 0, tzinfo=timezone.utc)
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
class TestReflectNewParams:
|
||||
"""Tests for new reflect parameters: max_tokens, response_schema, tags, tags_match."""
|
||||
|
||||
async def test_reflect_with_max_tokens(self, mock_memory):
|
||||
mcp = _make_mcp_server(mock_memory, {"reflect"})
|
||||
await _tools(mcp)["reflect"].fn(query="test", max_tokens=2048)
|
||||
call_kwargs = mock_memory.reflect_async.call_args.kwargs
|
||||
assert call_kwargs["max_tokens"] == 2048
|
||||
|
||||
async def test_reflect_with_response_schema(self, mock_memory):
|
||||
schema = {"type": "object", "properties": {"answer": {"type": "string"}}}
|
||||
mock_memory.reflect_async = AsyncMock(
|
||||
return_value=MagicMock(
|
||||
model_dump_json=lambda indent=None: '{"text": "reflection"}',
|
||||
model_dump=lambda: {"text": "reflection"},
|
||||
structured_output={"answer": "yes"},
|
||||
)
|
||||
)
|
||||
mcp = _make_mcp_server(mock_memory, {"reflect"})
|
||||
result = await _tools(mcp)["reflect"].fn(query="test", response_schema=schema)
|
||||
call_kwargs = mock_memory.reflect_async.call_args.kwargs
|
||||
assert call_kwargs["response_schema"] == schema
|
||||
# Multi-bank returns JSON string
|
||||
import json
|
||||
|
||||
parsed = json.loads(result)
|
||||
assert parsed["structured_output"] == {"answer": "yes"}
|
||||
|
||||
async def test_reflect_with_tags(self, mock_memory):
|
||||
mcp = _make_mcp_server(mock_memory, {"reflect"})
|
||||
await _tools(mcp)["reflect"].fn(query="test", tags=["scope:work"], tags_match="all")
|
||||
call_kwargs = mock_memory.reflect_async.call_args.kwargs
|
||||
assert call_kwargs["tags"] == ["scope:work"]
|
||||
assert call_kwargs["tags_match"] == "all"
|
||||
|
||||
async def test_reflect_without_tags_no_tags_in_kwargs(self, mock_memory):
|
||||
"""When tags not provided, they should not be passed to engine."""
|
||||
mcp = _make_mcp_server(mock_memory, {"reflect"})
|
||||
await _tools(mcp)["reflect"].fn(query="test")
|
||||
call_kwargs = mock_memory.reflect_async.call_args.kwargs
|
||||
assert "tags" not in call_kwargs
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
class TestMentalModelTrigger:
|
||||
"""Tests for trigger_refresh_after_consolidation on create/update mental model."""
|
||||
|
||||
async def test_create_with_trigger(self, mock_memory):
|
||||
mcp = _make_mcp_server(mock_memory, {"create_mental_model"})
|
||||
await _tools(mcp)["create_mental_model"].fn(
|
||||
name="Test", source_query="query", trigger_refresh_after_consolidation=True
|
||||
)
|
||||
call_kwargs = mock_memory.create_mental_model.call_args.kwargs
|
||||
assert call_kwargs["trigger"] == {"refresh_after_consolidation": True}
|
||||
|
||||
async def test_create_default_trigger_false(self, mock_memory):
|
||||
mcp = _make_mcp_server(mock_memory, {"create_mental_model"})
|
||||
await _tools(mcp)["create_mental_model"].fn(name="Test", source_query="query")
|
||||
call_kwargs = mock_memory.create_mental_model.call_args.kwargs
|
||||
assert call_kwargs["trigger"] == {"refresh_after_consolidation": False}
|
||||
|
||||
async def test_update_with_trigger(self, mock_memory):
|
||||
mcp = _make_mcp_server(mock_memory, {"update_mental_model"})
|
||||
await _tools(mcp)["update_mental_model"].fn(
|
||||
mental_model_id="mm-1", trigger_refresh_after_consolidation=True
|
||||
)
|
||||
call_kwargs = mock_memory.update_mental_model.call_args.kwargs
|
||||
assert call_kwargs["trigger"] == {"refresh_after_consolidation": True}
|
||||
|
||||
async def test_update_without_trigger_no_trigger_in_kwargs(self, mock_memory):
|
||||
mcp = _make_mcp_server(mock_memory, {"update_mental_model"})
|
||||
await _tools(mcp)["update_mental_model"].fn(mental_model_id="mm-1", name="New Name")
|
||||
call_kwargs = mock_memory.update_mental_model.call_args.kwargs
|
||||
assert "trigger" not in call_kwargs
|
||||
|
||||
|
||||
# =========================================================================
|
||||
# Directive Tool Tests
|
||||
# =========================================================================
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
class TestDirectiveTools:
|
||||
async def test_list_directives_multi_bank(self, mock_memory):
|
||||
mcp = _make_mcp_server(mock_memory, {"list_directives"}, include_bank_id=True)
|
||||
result = await _tools(mcp)["list_directives"].fn()
|
||||
assert '"dir-1"' in result
|
||||
mock_memory.list_directives.assert_called_once()
|
||||
assert mock_memory.list_directives.call_args[0][0] == "test-bank"
|
||||
|
||||
async def test_list_directives_single_bank(self, mock_memory):
|
||||
mcp = _make_mcp_server(mock_memory, {"list_directives"}, include_bank_id=False)
|
||||
result = await _tools(mcp)["list_directives"].fn()
|
||||
assert isinstance(result, dict)
|
||||
assert len(result["items"]) == 1
|
||||
|
||||
async def test_create_directive(self, mock_memory):
|
||||
mcp = _make_mcp_server(mock_memory, {"create_directive"}, include_bank_id=True)
|
||||
result = await _tools(mcp)["create_directive"].fn(name="Test", content="Be concise", priority=5)
|
||||
assert '"dir-new"' in result
|
||||
call_args = mock_memory.create_directive.call_args
|
||||
assert call_args[0][0] == "test-bank"
|
||||
assert call_args.kwargs["name"] == "Test"
|
||||
assert call_args.kwargs["content"] == "Be concise"
|
||||
assert call_args.kwargs["priority"] == 5
|
||||
|
||||
async def test_delete_directive(self, mock_memory):
|
||||
mcp = _make_mcp_server(mock_memory, {"delete_directive"}, include_bank_id=True)
|
||||
result = await _tools(mcp)["delete_directive"].fn(directive_id="dir-1")
|
||||
assert '"deleted"' in result
|
||||
assert mock_memory.delete_directive.call_args[0][1] == "dir-1"
|
||||
|
||||
async def test_delete_directive_not_found(self, mock_memory):
|
||||
mock_memory.delete_directive.return_value = False
|
||||
mcp = _make_mcp_server(mock_memory, {"delete_directive"}, include_bank_id=True)
|
||||
result = await _tools(mcp)["delete_directive"].fn(directive_id="missing")
|
||||
assert "not found" in result
|
||||
|
||||
|
||||
# =========================================================================
|
||||
# Memory Browsing Tool Tests
|
||||
# =========================================================================
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
class TestMemoryBrowsingTools:
|
||||
async def test_list_memories_default(self, mock_memory):
|
||||
mcp = _make_mcp_server(mock_memory, {"list_memories"}, include_bank_id=True)
|
||||
result = await _tools(mcp)["list_memories"].fn()
|
||||
assert '"mem-1"' in result
|
||||
call_kwargs = mock_memory.list_memory_units.call_args.kwargs
|
||||
assert call_kwargs["limit"] == 100
|
||||
assert call_kwargs["offset"] == 0
|
||||
|
||||
async def test_list_memories_with_filters(self, mock_memory):
|
||||
mcp = _make_mcp_server(mock_memory, {"list_memories"}, include_bank_id=True)
|
||||
await _tools(mcp)["list_memories"].fn(type="world", q="test query", limit=50)
|
||||
call_kwargs = mock_memory.list_memory_units.call_args.kwargs
|
||||
assert call_kwargs["fact_type"] == "world"
|
||||
assert call_kwargs["search_query"] == "test query"
|
||||
assert call_kwargs["limit"] == 50
|
||||
|
||||
async def test_get_memory(self, mock_memory):
|
||||
mcp = _make_mcp_server(mock_memory, {"get_memory"}, include_bank_id=True)
|
||||
result = await _tools(mcp)["get_memory"].fn(memory_id="mem-1")
|
||||
assert '"mem-1"' in result
|
||||
|
||||
async def test_get_memory_not_found(self, mock_memory):
|
||||
mock_memory.get_memory_unit.return_value = None
|
||||
mcp = _make_mcp_server(mock_memory, {"get_memory"}, include_bank_id=True)
|
||||
result = await _tools(mcp)["get_memory"].fn(memory_id="missing")
|
||||
assert "not found" in result
|
||||
|
||||
async def test_delete_memory(self, mock_memory):
|
||||
mcp = _make_mcp_server(mock_memory, {"delete_memory"}, include_bank_id=True)
|
||||
result = await _tools(mcp)["delete_memory"].fn(memory_id="mem-1")
|
||||
assert '"deleted"' in result
|
||||
assert mock_memory.delete_memory_unit.call_args.kwargs["unit_id"] == "mem-1"
|
||||
|
||||
async def test_list_memories_single_bank(self, mock_memory):
|
||||
mcp = _make_mcp_server(mock_memory, {"list_memories"}, include_bank_id=False)
|
||||
result = await _tools(mcp)["list_memories"].fn()
|
||||
assert isinstance(result, dict)
|
||||
|
||||
|
||||
# =========================================================================
|
||||
# Document Tool Tests
|
||||
# =========================================================================
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
class TestDocumentTools:
|
||||
async def test_list_documents(self, mock_memory):
|
||||
mcp = _make_mcp_server(mock_memory, {"list_documents"}, include_bank_id=True)
|
||||
result = await _tools(mcp)["list_documents"].fn()
|
||||
assert '"doc-1"' in result
|
||||
|
||||
async def test_get_document(self, mock_memory):
|
||||
mcp = _make_mcp_server(mock_memory, {"get_document"}, include_bank_id=True)
|
||||
result = await _tools(mcp)["get_document"].fn(document_id="doc-1")
|
||||
assert '"doc-1"' in result
|
||||
|
||||
async def test_get_document_not_found(self, mock_memory):
|
||||
mock_memory.get_document.return_value = None
|
||||
mcp = _make_mcp_server(mock_memory, {"get_document"}, include_bank_id=True)
|
||||
result = await _tools(mcp)["get_document"].fn(document_id="missing")
|
||||
assert "not found" in result
|
||||
|
||||
async def test_delete_document(self, mock_memory):
|
||||
mcp = _make_mcp_server(mock_memory, {"delete_document"}, include_bank_id=True)
|
||||
result = await _tools(mcp)["delete_document"].fn(document_id="doc-1")
|
||||
assert '"deleted"' in result
|
||||
|
||||
async def test_list_documents_single_bank(self, mock_memory):
|
||||
mcp = _make_mcp_server(mock_memory, {"list_documents"}, include_bank_id=False)
|
||||
result = await _tools(mcp)["list_documents"].fn()
|
||||
assert isinstance(result, dict)
|
||||
|
||||
|
||||
# =========================================================================
|
||||
# Operation Tool Tests
|
||||
# =========================================================================
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
class TestOperationTools:
|
||||
async def test_list_operations(self, mock_memory):
|
||||
mcp = _make_mcp_server(mock_memory, {"list_operations"}, include_bank_id=True)
|
||||
result = await _tools(mcp)["list_operations"].fn()
|
||||
assert '"op-1"' in result
|
||||
|
||||
async def test_list_operations_with_status(self, mock_memory):
|
||||
mcp = _make_mcp_server(mock_memory, {"list_operations"}, include_bank_id=True)
|
||||
await _tools(mcp)["list_operations"].fn(status="completed", limit=10)
|
||||
call_kwargs = mock_memory.list_operations.call_args.kwargs
|
||||
assert call_kwargs["status"] == "completed"
|
||||
assert call_kwargs["limit"] == 10
|
||||
|
||||
async def test_get_operation(self, mock_memory):
|
||||
mcp = _make_mcp_server(mock_memory, {"get_operation"}, include_bank_id=True)
|
||||
result = await _tools(mcp)["get_operation"].fn(operation_id="op-1")
|
||||
assert '"op-1"' in result
|
||||
|
||||
async def test_cancel_operation(self, mock_memory):
|
||||
mcp = _make_mcp_server(mock_memory, {"cancel_operation"}, include_bank_id=True)
|
||||
result = await _tools(mcp)["cancel_operation"].fn(operation_id="op-1")
|
||||
assert '"cancelled"' in result
|
||||
|
||||
async def test_list_operations_single_bank(self, mock_memory):
|
||||
mcp = _make_mcp_server(mock_memory, {"list_operations"}, include_bank_id=False)
|
||||
result = await _tools(mcp)["list_operations"].fn()
|
||||
assert isinstance(result, dict)
|
||||
|
||||
|
||||
# =========================================================================
|
||||
# Tags & Bank Tool Tests
|
||||
# =========================================================================
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
class TestTagsAndBankTools:
|
||||
async def test_list_tags(self, mock_memory):
|
||||
mcp = _make_mcp_server(mock_memory, {"list_tags"}, include_bank_id=True)
|
||||
result = await _tools(mcp)["list_tags"].fn(q="project:*", limit=50)
|
||||
call_kwargs = mock_memory.list_tags.call_args.kwargs
|
||||
assert call_kwargs["pattern"] == "project:*"
|
||||
assert call_kwargs["limit"] == 50
|
||||
|
||||
async def test_get_bank(self, mock_memory):
|
||||
mcp = _make_mcp_server(mock_memory, {"get_bank"}, include_bank_id=True)
|
||||
result = await _tools(mcp)["get_bank"].fn()
|
||||
assert '"test-bank"' in result or "test-bank" in result
|
||||
|
||||
async def test_get_bank_stats(self, mock_memory):
|
||||
mcp = _make_mcp_server(mock_memory, {"get_bank_stats"}, include_bank_id=True)
|
||||
result = await _tools(mcp)["get_bank_stats"].fn()
|
||||
assert "100" in result # nodes count
|
||||
|
||||
async def test_update_bank(self, mock_memory):
|
||||
mcp = _make_mcp_server(mock_memory, {"update_bank"}, include_bank_id=True)
|
||||
result = await _tools(mcp)["update_bank"].fn(name="New Name", mission="New Mission")
|
||||
call_kwargs = mock_memory.update_bank.call_args.kwargs
|
||||
assert call_kwargs["name"] == "New Name"
|
||||
assert call_kwargs["mission"] == "New Mission"
|
||||
|
||||
async def test_delete_bank(self, mock_memory):
|
||||
mcp = _make_mcp_server(mock_memory, {"delete_bank"}, include_bank_id=True)
|
||||
result = await _tools(mcp)["delete_bank"].fn()
|
||||
assert '"deleted"' in result
|
||||
mock_memory.delete_bank.assert_called_once()
|
||||
|
||||
async def test_clear_memories(self, mock_memory):
|
||||
mcp = _make_mcp_server(mock_memory, {"clear_memories"}, include_bank_id=True)
|
||||
result = await _tools(mcp)["clear_memories"].fn()
|
||||
assert '"cleared"' in result
|
||||
mock_memory.delete_bank.assert_called_once()
|
||||
|
||||
async def test_clear_memories_with_type_filter(self, mock_memory):
|
||||
mcp = _make_mcp_server(mock_memory, {"clear_memories"}, include_bank_id=True)
|
||||
await _tools(mcp)["clear_memories"].fn(type="world")
|
||||
call_kwargs = mock_memory.delete_bank.call_args.kwargs
|
||||
assert call_kwargs["fact_type"] == "world"
|
||||
|
||||
async def test_list_tags_single_bank(self, mock_memory):
|
||||
mcp = _make_mcp_server(mock_memory, {"list_tags"}, include_bank_id=False)
|
||||
result = await _tools(mcp)["list_tags"].fn()
|
||||
assert isinstance(result, dict)
|
||||
|
||||
async def test_get_bank_single_bank(self, mock_memory):
|
||||
mcp = _make_mcp_server(mock_memory, {"get_bank"}, include_bank_id=False)
|
||||
result = await _tools(mcp)["get_bank"].fn()
|
||||
assert isinstance(result, dict)
|
||||
|
||||
async def test_delete_bank_single_bank(self, mock_memory):
|
||||
mcp = _make_mcp_server(mock_memory, {"delete_bank"}, include_bank_id=False)
|
||||
result = await _tools(mcp)["delete_bank"].fn()
|
||||
assert isinstance(result, dict)
|
||||
assert result["status"] == "deleted"
|
||||
|
||||
async def test_clear_memories_single_bank(self, mock_memory):
|
||||
mcp = _make_mcp_server(mock_memory, {"clear_memories"}, include_bank_id=False)
|
||||
result = await _tools(mcp)["clear_memories"].fn()
|
||||
assert isinstance(result, dict)
|
||||
assert result["status"] == "cleared"
|
||||
|
||||
|
||||
# =========================================================================
|
||||
# Additional Error Handling & Edge Case Tests
|
||||
# =========================================================================
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
class TestOperationErrorHandling:
|
||||
"""Error handling tests for operation tools."""
|
||||
|
||||
async def test_get_operation_engine_error(self, mock_memory):
|
||||
mock_memory.get_operation_status.side_effect = RuntimeError("Operation not found")
|
||||
mcp = _make_mcp_server(mock_memory, {"get_operation"}, include_bank_id=True)
|
||||
result = await _tools(mcp)["get_operation"].fn(operation_id="missing")
|
||||
assert "error" in result
|
||||
assert "Operation not found" in result
|
||||
|
||||
async def test_get_operation_engine_error_single_bank(self, mock_memory):
|
||||
mock_memory.get_operation_status.side_effect = RuntimeError("Operation not found")
|
||||
mcp = _make_mcp_server(mock_memory, {"get_operation"}, include_bank_id=False)
|
||||
result = await _tools(mcp)["get_operation"].fn(operation_id="missing")
|
||||
assert isinstance(result, dict)
|
||||
assert "Operation not found" in result["error"]
|
||||
|
||||
async def test_cancel_operation_engine_error(self, mock_memory):
|
||||
mock_memory.cancel_operation.side_effect = RuntimeError("Cannot cancel completed operation")
|
||||
mcp = _make_mcp_server(mock_memory, {"cancel_operation"}, include_bank_id=True)
|
||||
result = await _tools(mcp)["cancel_operation"].fn(operation_id="op-done")
|
||||
assert "error" in result
|
||||
assert "Cannot cancel" in result
|
||||
|
||||
async def test_cancel_operation_engine_error_single_bank(self, mock_memory):
|
||||
mock_memory.cancel_operation.side_effect = RuntimeError("Cannot cancel")
|
||||
mcp = _make_mcp_server(mock_memory, {"cancel_operation"}, include_bank_id=False)
|
||||
result = await _tools(mcp)["cancel_operation"].fn(operation_id="op-done")
|
||||
assert isinstance(result, dict)
|
||||
assert "Cannot cancel" in result["error"]
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
class TestDeleteErrorHandling:
|
||||
"""Error handling tests for delete operations."""
|
||||
|
||||
async def test_delete_memory_engine_error(self, mock_memory):
|
||||
mock_memory.delete_memory_unit.side_effect = RuntimeError("DB error")
|
||||
mcp = _make_mcp_server(mock_memory, {"delete_memory"}, include_bank_id=True)
|
||||
result = await _tools(mcp)["delete_memory"].fn(memory_id="mem-1")
|
||||
assert "error" in result
|
||||
assert "DB error" in result
|
||||
|
||||
async def test_delete_memory_single_bank(self, mock_memory):
|
||||
mcp = _make_mcp_server(mock_memory, {"delete_memory"}, include_bank_id=False)
|
||||
result = await _tools(mcp)["delete_memory"].fn(memory_id="mem-1")
|
||||
assert isinstance(result, dict)
|
||||
assert result["status"] == "deleted"
|
||||
|
||||
async def test_delete_document_engine_error(self, mock_memory):
|
||||
mock_memory.delete_document.side_effect = RuntimeError("DB error")
|
||||
mcp = _make_mcp_server(mock_memory, {"delete_document"}, include_bank_id=True)
|
||||
result = await _tools(mcp)["delete_document"].fn(document_id="doc-1")
|
||||
assert "error" in result
|
||||
assert "DB error" in result
|
||||
|
||||
async def test_delete_document_single_bank(self, mock_memory):
|
||||
mcp = _make_mcp_server(mock_memory, {"delete_document"}, include_bank_id=False)
|
||||
result = await _tools(mcp)["delete_document"].fn(document_id="doc-1")
|
||||
assert isinstance(result, dict)
|
||||
assert result["status"] == "deleted"
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
class TestUpdateBankVariants:
|
||||
"""Additional tests for update_bank tool."""
|
||||
|
||||
async def test_update_bank_single_bank(self, mock_memory):
|
||||
mcp = _make_mcp_server(mock_memory, {"update_bank"}, include_bank_id=False)
|
||||
result = await _tools(mcp)["update_bank"].fn(name="New Name")
|
||||
assert isinstance(result, dict)
|
||||
call_kwargs = mock_memory.update_bank.call_args.kwargs
|
||||
assert call_kwargs["name"] == "New Name"
|
||||
|
||||
async def test_update_bank_engine_error(self, mock_memory):
|
||||
mock_memory.update_bank.side_effect = RuntimeError("DB error")
|
||||
mcp = _make_mcp_server(mock_memory, {"update_bank"}, include_bank_id=True)
|
||||
result = await _tools(mcp)["update_bank"].fn(name="X")
|
||||
assert "error" in result
|
||||
|
||||
async def test_get_bank_stats_engine_error(self, mock_memory):
|
||||
mock_memory.get_bank_stats.side_effect = RuntimeError("DB error")
|
||||
mcp = _make_mcp_server(mock_memory, {"get_bank_stats"}, include_bank_id=True)
|
||||
result = await _tools(mcp)["get_bank_stats"].fn()
|
||||
assert "error" in result
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
class TestEmptyListReturns:
|
||||
"""Tests that empty lists are handled gracefully."""
|
||||
|
||||
async def test_list_memories_empty(self, mock_memory):
|
||||
mock_memory.list_memory_units.return_value = {"items": [], "total": 0}
|
||||
mcp = _make_mcp_server(mock_memory, {"list_memories"}, include_bank_id=True)
|
||||
result = await _tools(mcp)["list_memories"].fn()
|
||||
assert '"items": []' in result or "[]" in result
|
||||
|
||||
async def test_list_documents_empty(self, mock_memory):
|
||||
mock_memory.list_documents.return_value = {"items": [], "total": 0}
|
||||
mcp = _make_mcp_server(mock_memory, {"list_documents"}, include_bank_id=True)
|
||||
result = await _tools(mcp)["list_documents"].fn()
|
||||
assert '"items": []' in result or "[]" in result
|
||||
|
||||
async def test_list_operations_empty(self, mock_memory):
|
||||
mock_memory.list_operations.return_value = {"items": []}
|
||||
mcp = _make_mcp_server(mock_memory, {"list_operations"}, include_bank_id=True)
|
||||
result = await _tools(mcp)["list_operations"].fn()
|
||||
assert '"items": []' in result or "[]" in result
|
||||
|
||||
async def test_list_directives_empty(self, mock_memory):
|
||||
mock_memory.list_directives.return_value = []
|
||||
mcp = _make_mcp_server(mock_memory, {"list_directives"}, include_bank_id=True)
|
||||
result = await _tools(mcp)["list_directives"].fn()
|
||||
assert "[]" in result
|
||||
|
||||
async def test_list_tags_empty(self, mock_memory):
|
||||
mock_memory.list_tags.return_value = {"items": [], "total": 0}
|
||||
mcp = _make_mcp_server(mock_memory, {"list_tags"}, include_bank_id=True)
|
||||
result = await _tools(mcp)["list_tags"].fn()
|
||||
assert '"items": []' in result or "[]" in result
|
||||
|
|
|
|||
|
|
@ -100,12 +100,12 @@ The MCP server operates in two modes depending on the URL:
|
|||
|
||||
| Mode | URL | Tools | bank_id |
|
||||
|------|-----|-------|---------|
|
||||
| **Single-bank** | `/mcp/{bank_id}/` | Memory + mental model tools | Implicit from URL |
|
||||
| **Multi-bank** | `/mcp/` | All tools including bank management | Explicit `bank_id` parameter on each tool |
|
||||
| **Single-bank** | `/mcp/{bank_id}/` | 26 tools (memory, mental models, directives, documents, operations, tags, bank management) | Implicit from URL |
|
||||
| **Multi-bank** | `/mcp/` | All 29 tools including `list_banks`, `create_bank`, `get_bank_stats` | Explicit `bank_id` parameter on each tool |
|
||||
|
||||
**Single-bank mode** (recommended) scopes all operations to the bank in the URL. Tools don't expose a `bank_id` parameter.
|
||||
|
||||
**Multi-bank mode** exposes all tools with an optional `bank_id` parameter, plus bank management tools (`list_banks`, `create_bank`).
|
||||
**Multi-bank mode** exposes all tools with an optional `bank_id` parameter, plus bank management tools (`list_banks`, `create_bank`, `get_bank_stats`).
|
||||
|
||||
---
|
||||
|
||||
|
|
@ -120,6 +120,9 @@ Store information to long-term memory.
|
|||
| `content` | string | Yes | The fact or memory to store |
|
||||
| `context` | string | No | Category for the memory (default: `general`) |
|
||||
| `timestamp` | string | No | ISO 8601 timestamp for when the event occurred |
|
||||
| `tags` | list[string] | No | Tags for organizing and filtering this memory |
|
||||
| `metadata` | object | No | Key-value metadata to attach (e.g., `{"source": "slack"}`) |
|
||||
| `document_id` | string | No | Associate this memory with an existing document |
|
||||
|
||||
**Example:**
|
||||
```json
|
||||
|
|
@ -127,7 +130,8 @@ Store information to long-term memory.
|
|||
"name": "retain",
|
||||
"arguments": {
|
||||
"content": "User prefers Python over JavaScript for backend development",
|
||||
"context": "programming_preferences"
|
||||
"context": "programming_preferences",
|
||||
"tags": ["user:alice", "preferences"]
|
||||
}
|
||||
}
|
||||
```
|
||||
|
|
@ -148,13 +152,20 @@ Search memories to provide personalized responses.
|
|||
|-----------|------|----------|-------------|
|
||||
| `query` | string | Yes | Natural language search query |
|
||||
| `max_tokens` | integer | No | Maximum tokens to return (default: 4096) |
|
||||
| `budget` | string | No | Search thoroughness: `low`, `mid`, or `high` (default: `high`) |
|
||||
| `types` | list[string] | No | Filter by fact type: `world`, `experience`, `opinion`. Defaults to all |
|
||||
| `tags` | list[string] | No | Filter memories by tags |
|
||||
| `tags_match` | string | No | Tag matching mode: `any` (default) or `all` |
|
||||
| `query_timestamp` | string | No | ISO 8601 timestamp — recall as if asking at this point in time |
|
||||
|
||||
**Example:**
|
||||
```json
|
||||
{
|
||||
"name": "recall",
|
||||
"arguments": {
|
||||
"query": "What are the user's programming language preferences?"
|
||||
"query": "What are the user's programming language preferences?",
|
||||
"tags": ["preferences"],
|
||||
"budget": "high"
|
||||
}
|
||||
}
|
||||
```
|
||||
|
|
@ -176,6 +187,10 @@ Generate thoughtful analysis by synthesizing stored memories with the bank's per
|
|||
| `query` | string | Yes | The question or topic to reflect on |
|
||||
| `context` | string | No | Optional context about why this reflection is needed |
|
||||
| `budget` | string | No | Search budget: `low`, `mid`, or `high` (default: `low`) |
|
||||
| `max_tokens` | integer | No | Maximum tokens in the response (default: 4096) |
|
||||
| `response_schema` | object | No | JSON Schema for structured output. When provided, the response includes a `structured_output` field |
|
||||
| `tags` | list[string] | No | Filter memories by tags before reflecting |
|
||||
| `tags_match` | string | No | Tag matching mode: `any` (default) or `all` |
|
||||
|
||||
**Example:**
|
||||
```json
|
||||
|
|
@ -183,7 +198,8 @@ Generate thoughtful analysis by synthesizing stored memories with the bank's per
|
|||
"name": "reflect",
|
||||
"arguments": {
|
||||
"query": "Based on my past decisions, what architectural style do I prefer?",
|
||||
"budget": "mid"
|
||||
"budget": "mid",
|
||||
"tags": ["architecture"]
|
||||
}
|
||||
}
|
||||
```
|
||||
|
|
@ -206,6 +222,7 @@ Create a mental model — a living document that stays current with your memorie
|
|||
| `mental_model_id` | string | No | Custom ID (alphanumeric lowercase with hyphens). Auto-generated if not provided |
|
||||
| `tags` | list[string] | No | Tags for organizing and filtering models |
|
||||
| `max_tokens` | integer | No | Maximum tokens for model content (default: 2048) |
|
||||
| `trigger_refresh_after_consolidation` | boolean | No | Auto-refresh this model after memory consolidation (default: `false`) |
|
||||
|
||||
**Example:**
|
||||
```json
|
||||
|
|
@ -254,6 +271,7 @@ Update a mental model's metadata or settings.
|
|||
| `source_query` | string | No | New source query |
|
||||
| `tags` | list[string] | No | New tags |
|
||||
| `max_tokens` | integer | No | New max tokens |
|
||||
| `trigger_refresh_after_consolidation` | boolean | No | Auto-refresh after consolidation. Only set when you want to change this setting |
|
||||
|
||||
---
|
||||
|
||||
|
|
@ -295,6 +313,186 @@ Create a new memory bank or retrieve an existing one.
|
|||
|
||||
---
|
||||
|
||||
### list_directives
|
||||
|
||||
List all directives in a bank. Directives are instructions that guide how the memory system processes and responds to queries.
|
||||
|
||||
| Parameter | Type | Required | Description |
|
||||
|-----------|------|----------|-------------|
|
||||
| `tags` | list[string] | No | Filter directives by tags |
|
||||
| `active_only` | boolean | No | Only return active directives (default: `true`) |
|
||||
|
||||
---
|
||||
|
||||
### create_directive
|
||||
|
||||
Create a new directive in a bank.
|
||||
|
||||
| Parameter | Type | Required | Description |
|
||||
|-----------|------|----------|-------------|
|
||||
| `name` | string | Yes | Human-readable name for the directive |
|
||||
| `content` | string | Yes | The directive content/instruction |
|
||||
| `priority` | integer | No | Priority level (higher = more important) |
|
||||
| `is_active` | boolean | No | Whether the directive is active (default: `true`) |
|
||||
| `tags` | list[string] | No | Tags for organizing directives |
|
||||
|
||||
---
|
||||
|
||||
### delete_directive
|
||||
|
||||
Delete a directive by ID.
|
||||
|
||||
| Parameter | Type | Required | Description |
|
||||
|-----------|------|----------|-------------|
|
||||
| `directive_id` | string | Yes | The ID of the directive to delete |
|
||||
|
||||
---
|
||||
|
||||
### list_memories
|
||||
|
||||
Browse stored memories with optional filtering and pagination.
|
||||
|
||||
| Parameter | Type | Required | Description |
|
||||
|-----------|------|----------|-------------|
|
||||
| `type` | string | No | Filter by fact type: `world`, `experience`, or `opinion` |
|
||||
| `q` | string | No | Search query to filter memories |
|
||||
| `limit` | integer | No | Maximum number of results (default: 100) |
|
||||
| `offset` | integer | No | Number of results to skip for pagination (default: 0) |
|
||||
|
||||
---
|
||||
|
||||
### get_memory
|
||||
|
||||
Retrieve a specific memory by ID.
|
||||
|
||||
| Parameter | Type | Required | Description |
|
||||
|-----------|------|----------|-------------|
|
||||
| `memory_id` | string | Yes | The ID of the memory to retrieve |
|
||||
|
||||
---
|
||||
|
||||
### delete_memory
|
||||
|
||||
Permanently delete a specific memory.
|
||||
|
||||
| Parameter | Type | Required | Description |
|
||||
|-----------|------|----------|-------------|
|
||||
| `memory_id` | string | Yes | The ID of the memory to delete |
|
||||
|
||||
---
|
||||
|
||||
### list_documents
|
||||
|
||||
List documents that have been ingested into the memory bank.
|
||||
|
||||
| Parameter | Type | Required | Description |
|
||||
|-----------|------|----------|-------------|
|
||||
| `q` | string | No | Search query to filter documents |
|
||||
| `limit` | integer | No | Maximum number of results (default: 100) |
|
||||
|
||||
---
|
||||
|
||||
### get_document
|
||||
|
||||
Retrieve a specific document by ID, including its metadata.
|
||||
|
||||
| Parameter | Type | Required | Description |
|
||||
|-----------|------|----------|-------------|
|
||||
| `document_id` | string | Yes | The ID of the document to retrieve |
|
||||
|
||||
---
|
||||
|
||||
### delete_document
|
||||
|
||||
Delete a document and all memories linked to it.
|
||||
|
||||
| Parameter | Type | Required | Description |
|
||||
|-----------|------|----------|-------------|
|
||||
| `document_id` | string | Yes | The ID of the document to delete |
|
||||
|
||||
---
|
||||
|
||||
### list_operations
|
||||
|
||||
List async operations (retain processing, mental model refresh, etc.) with optional status filtering.
|
||||
|
||||
| Parameter | Type | Required | Description |
|
||||
|-----------|------|----------|-------------|
|
||||
| `status` | string | No | Filter by status: `pending`, `running`, `completed`, `failed`, `cancelled` |
|
||||
| `limit` | integer | No | Maximum number of results (default: 100) |
|
||||
|
||||
---
|
||||
|
||||
### get_operation
|
||||
|
||||
Get the status and details of an async operation.
|
||||
|
||||
| Parameter | Type | Required | Description |
|
||||
|-----------|------|----------|-------------|
|
||||
| `operation_id` | string | Yes | The ID of the operation to check |
|
||||
|
||||
---
|
||||
|
||||
### cancel_operation
|
||||
|
||||
Cancel a pending or running async operation.
|
||||
|
||||
| Parameter | Type | Required | Description |
|
||||
|-----------|------|----------|-------------|
|
||||
| `operation_id` | string | Yes | The ID of the operation to cancel |
|
||||
|
||||
---
|
||||
|
||||
### list_tags
|
||||
|
||||
List all unique tags used in a bank, optionally filtered by pattern.
|
||||
|
||||
| Parameter | Type | Required | Description |
|
||||
|-----------|------|----------|-------------|
|
||||
| `q` | string | No | Glob pattern to filter tags (e.g., `project:*`) |
|
||||
| `limit` | integer | No | Maximum number of results (default: 100) |
|
||||
|
||||
---
|
||||
|
||||
### get_bank
|
||||
|
||||
Get information about a memory bank, including its name, mission, and disposition.
|
||||
|
||||
---
|
||||
|
||||
### get_bank_stats (multi-bank mode only)
|
||||
|
||||
Get statistics for a memory bank (node/link counts).
|
||||
|
||||
---
|
||||
|
||||
### update_bank
|
||||
|
||||
Update a memory bank's metadata.
|
||||
|
||||
| Parameter | Type | Required | Description |
|
||||
|-----------|------|----------|-------------|
|
||||
| `name` | string | No | New human-friendly name for the bank |
|
||||
| `mission` | string | No | New mission describing who the agent is and what they're trying to accomplish |
|
||||
|
||||
---
|
||||
|
||||
### delete_bank
|
||||
|
||||
Permanently delete a memory bank and all its data (memories, documents, entities, mental models).
|
||||
|
||||
---
|
||||
|
||||
### clear_memories
|
||||
|
||||
Clear all memories from a bank without deleting the bank itself. Optionally filter by fact type to only clear specific kinds of memories.
|
||||
|
||||
| Parameter | Type | Required | Description |
|
||||
|-----------|------|----------|-------------|
|
||||
| `type` | string | No | Fact type to clear: `world`, `experience`, or `opinion`. If not specified, clears all |
|
||||
|
||||
---
|
||||
|
||||
## Integration with AI Assistants
|
||||
|
||||
The MCP server can be used with any MCP-compatible AI assistant. See the [Authentication](#authentication) section above for Claude Code and Claude Desktop configuration examples.
|
||||
|
|
|
|||
|
|
@ -66,22 +66,74 @@ claude mcp add --transport http hindsight http://localhost:8888/mcp/my-bank/
|
|||
|
||||
## Available Tools
|
||||
|
||||
The local server exposes the full tool set:
|
||||
The local server exposes the full tool set (29 tools in multi-bank mode, 26 in single-bank mode):
|
||||
|
||||
**Core Memory**
|
||||
|
||||
| Tool | Description |
|
||||
|------|-------------|
|
||||
| `retain` | Store information to long-term memory with optional tags, metadata, and document association |
|
||||
| `recall` | Search memories with natural language, configurable budget, type filters, and tag filters |
|
||||
| `reflect` | Synthesize memories into a reasoned answer with optional structured output |
|
||||
|
||||
**Mental Models**
|
||||
|
||||
| Tool | Description |
|
||||
|------|-------------|
|
||||
| `retain` | Store information to long-term memory (fire-and-forget) |
|
||||
| `recall` | Search memories with natural language |
|
||||
| `reflect` | Synthesize memories into a reasoned answer |
|
||||
| `list_banks` | List all memory banks |
|
||||
| `create_bank` | Create or configure a memory bank |
|
||||
| `list_mental_models` | List pinned reflections for a bank |
|
||||
| `get_mental_model` | Get a specific mental model |
|
||||
| `create_mental_model` | Create a new mental model |
|
||||
| `create_mental_model` | Create a new mental model with optional auto-refresh trigger |
|
||||
| `update_mental_model` | Update a mental model's metadata |
|
||||
| `delete_mental_model` | Delete a mental model |
|
||||
| `refresh_mental_model` | Regenerate a mental model's content |
|
||||
|
||||
**Directives**
|
||||
|
||||
| Tool | Description |
|
||||
|------|-------------|
|
||||
| `list_directives` | List directives that guide memory processing |
|
||||
| `create_directive` | Create a new directive |
|
||||
| `delete_directive` | Delete a directive |
|
||||
|
||||
**Memory Browsing**
|
||||
|
||||
| Tool | Description |
|
||||
|------|-------------|
|
||||
| `list_memories` | Browse memories with filtering and pagination |
|
||||
| `get_memory` | Get a specific memory by ID |
|
||||
| `delete_memory` | Delete a specific memory |
|
||||
|
||||
**Documents**
|
||||
|
||||
| Tool | Description |
|
||||
|------|-------------|
|
||||
| `list_documents` | List ingested documents |
|
||||
| `get_document` | Get a specific document |
|
||||
| `delete_document` | Delete a document and its linked memories |
|
||||
|
||||
**Operations**
|
||||
|
||||
| Tool | Description |
|
||||
|------|-------------|
|
||||
| `list_operations` | List async operations with status filtering |
|
||||
| `get_operation` | Check operation status and progress |
|
||||
| `cancel_operation` | Cancel a pending or running operation |
|
||||
|
||||
**Tags & Bank Management**
|
||||
|
||||
| Tool | Description |
|
||||
|------|-------------|
|
||||
| `list_tags` | List unique tags used in a bank |
|
||||
| `get_bank` | Get bank profile (name, mission, disposition) |
|
||||
| `get_bank_stats` | Get bank statistics (multi-bank only) |
|
||||
| `update_bank` | Update bank name or mission |
|
||||
| `delete_bank` | Delete an entire bank and all its data |
|
||||
| `clear_memories` | Clear memories without deleting the bank |
|
||||
| `list_banks` | List all memory banks (multi-bank only) |
|
||||
| `create_bank` | Create or configure a memory bank (multi-bank only) |
|
||||
|
||||
For detailed parameter documentation, see the [MCP Server reference](/developer/mcp-server#available-tools).
|
||||
|
||||
## Environment Variables
|
||||
|
||||
All standard [Hindsight configuration variables](/developer/configuration) are supported. Key ones for local use:
|
||||
|
|
|
|||
Loading…
Reference in a new issue