Skip to content

Commit f21e2ee

Browse files
committed
fix(mcp): preserve empty all-project search scope
Signed-off-by: phernandez <paul@basicmachines.co>
1 parent e2a830b commit f21e2ee

2 files changed

Lines changed: 48 additions & 6 deletions

File tree

src/basic_memory/mcp/tools/search.py

Lines changed: 14 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -451,14 +451,23 @@ async def _search_all_projects(
451451
status: str | None,
452452
min_similarity: float | None,
453453
context: Context | None,
454-
) -> dict | str | None:
454+
) -> dict | str:
455455
"""Search every accessible project when the caller explicitly opts in."""
456+
requested_page = max(page, 1)
457+
requested_page_size = max(page_size, 1)
456458
project_refs = await _load_search_project_refs(context=context)
457459
if not project_refs:
458-
return None
460+
response = SearchResponse(
461+
results=[],
462+
current_page=requested_page,
463+
page_size=requested_page_size,
464+
total=0,
465+
has_more=False,
466+
)
467+
if output_format == "json":
468+
return response.model_dump(mode="json", exclude_none=True)
469+
return _format_search_markdown(response, "all projects", query)
459470

460-
requested_page = max(page, 1)
461-
requested_page_size = max(page_size, 1)
462471
per_project_page_size = requested_page * requested_page_size
463472
merged_results: list[dict[str, Any]] = []
464473
total = 0
@@ -810,8 +819,7 @@ async def search_notes(
810819
min_similarity=min_similarity,
811820
context=context,
812821
)
813-
if all_projects_result is not None:
814-
return all_projects_result
822+
return all_projects_result
815823

816824
with logfire.span(
817825
"mcp.tool.search_notes",

tests/mcp/tools/test_search_notes_multi_project.py

Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -131,3 +131,37 @@ async def search(self, payload, page, page_size):
131131
assert isinstance(result, dict)
132132
assert result["results"] == []
133133
assert searched_projects == [(None, None)]
134+
135+
136+
@pytest.mark.asyncio
137+
async def test_search_notes_search_all_projects_with_no_refs_returns_empty_all_projects(
138+
monkeypatch,
139+
):
140+
"""Explicit all-project search must not silently fall back to one project."""
141+
search_mod = importlib.import_module("basic_memory.mcp.tools.search")
142+
143+
async def fake_load_search_project_refs(context=None):
144+
return []
145+
146+
@asynccontextmanager
147+
async def fail_get_project_client(*args, **kwargs):
148+
raise AssertionError("search_all_projects=True should not fall back to scoped search")
149+
yield
150+
151+
monkeypatch.setattr(search_mod, "_load_search_project_refs", fake_load_search_project_refs)
152+
monkeypatch.setattr(search_mod, "get_project_client", fail_get_project_client)
153+
154+
result = await search_mod.search_notes(
155+
query="MCP Test Note",
156+
search_all_projects=True,
157+
output_format="json",
158+
)
159+
160+
assert isinstance(result, dict)
161+
assert result == {
162+
"results": [],
163+
"current_page": 1,
164+
"page_size": 10,
165+
"total": 0,
166+
"has_more": False,
167+
}

0 commit comments

Comments
 (0)