Skip to content

Commit 6dd61e5

Browse files
phernandezclaude
andcommitted
fix(mcp): default category-only search to observations, not entities
search_notes defaults entity_types to 'entity' when unset (issue #31), but categories only exist on observations — so search_notes(categories=[...]) without entity_types ANDed the category against entity rows (NULL category) and returned nothing. When categories is supplied without an explicit entity_types, default to observations instead. Adds a regression test for the categories-only path (Codex review on #908). Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> Signed-off-by: phernandez <paul@basicmachines.co>
1 parent 2df4e73 commit 6dd61e5

2 files changed

Lines changed: 47 additions & 1 deletion

File tree

src/basic_memory/mcp/tools/search.py

Lines changed: 11 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1004,7 +1004,17 @@ async def search_notes(
10041004
# Applied after no_criteria() so that the implicit default doesn't
10051005
# mask a truly empty search request.
10061006
if not search_query.entity_types:
1007-
search_query.entity_types = [SearchItemType("entity")]
1007+
# Trigger: a category filter was supplied without an explicit
1008+
# entity_types.
1009+
# Why: categories only exist on observations — defaulting to "entity"
1010+
# (whose rows have NULL category) would AND a category filter against
1011+
# entity rows and return nothing, defeating a category-only search.
1012+
# Outcome: scope the implicit default to observations so
1013+
# search_notes(categories=[...]) returns the matching bullets.
1014+
if search_query.categories:
1015+
search_query.entity_types = [SearchItemType("observation")]
1016+
else:
1017+
search_query.entity_types = [SearchItemType("entity")]
10081018

10091019
logger.debug(
10101020
f"Search request: project={active_project.name} "

tests/mcp/test_tool_search.py

Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -421,6 +421,42 @@ async def test_search_with_categories_filter(client, test_project):
421421
assert all("requirement" != r.get("category") for r in decision["results"])
422422

423423

424+
@pytest.mark.asyncio
425+
async def test_search_categories_without_entity_types_returns_observations(client, test_project):
426+
"""categories=[...] WITHOUT entity_types must return the matching observations (#908).
427+
428+
search_notes defaults entity_types to "entity" when unset, but categories only exist on
429+
observations — so a category filter without an explicit entity_types would AND the
430+
category against entity rows (which have NULL category) and return nothing. The implicit
431+
default must scope to observations when categories is supplied.
432+
"""
433+
await write_note(
434+
project=test_project.name,
435+
title="Category Default Note",
436+
directory="test",
437+
content=(
438+
"# Category Default Note\n"
439+
"- [requirement] Auth tokens must rotate every 24 hours\n"
440+
"- [decision] We chose JWT for the auth token format\n"
441+
),
442+
)
443+
444+
# Note: no entity_types passed — exercises the implicit default.
445+
response = await search_notes(
446+
project=test_project.name,
447+
query="auth",
448+
search_type="text",
449+
categories=["requirement"],
450+
output_format="json",
451+
)
452+
453+
assert isinstance(response, dict), f"Search failed with error: {response}"
454+
results = response["results"]
455+
assert len(results) > 0, "category-only search must return matching observations"
456+
assert all(r["type"] == "observation" for r in results)
457+
assert all(r["category"] == "requirement" for r in results)
458+
459+
424460
@pytest.mark.asyncio
425461
async def test_search_with_date_filter(client, test_project):
426462
"""Test search with date filter."""

0 commit comments

Comments
 (0)