fix: parse tag: prefix at MCP tool level to avoid hybrid search failure (#30)

When semantic search is enabled (default), `search_notes(query="tag:security")`
failed because the HYBRID retrieval mode requires non-empty text, but the
service-layer tag: parser clears the text after the mode is already set.

Parse tag: prefix at the tool level before search mode selection, converting it
to a tags filter. This works with all search modes (text, hybrid, vector).

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Signed-off-by: phernandez <paul@basicmachines.co>
This commit is contained in:
phernandez
2026-02-28 11:53:05 -06:00
parent 2c5b63c4a2
commit 4f12182e28
2 changed files with 97 additions and 3 deletions
+14 -3
View File
@@ -1,5 +1,6 @@
"""Search tools for Basic Memory MCP server."""
import re
from textwrap import dedent
from typing import List, Optional, Dict, Any, Literal
@@ -302,9 +303,9 @@ async def search_notes(
- `search_notes("work-project", "category:observation")` - Filter by observation categories
- `search_notes("team-docs", "author:username")` - Find content by author (if metadata available)
**Note:** `tag:` shorthand requires `search_type="text"` when semantic search is enabled
(the default is hybrid). Alternatively, use the `tags` parameter for tag filtering with
any search type: `search_notes("project", "query", tags=["my-tag"])`
**Note:** `tag:` shorthand is automatically converted to a `tags` filter, so it works
with any search type (text, hybrid, vector). You can also use the `tags` parameter
directly: `search_notes("project", "query", tags=["my-tag"])`
### Search Type Examples
- `search_notes("my-project", "Meeting", search_type="title")` - Search only in titles
@@ -438,6 +439,16 @@ async def search_notes(
note_types = note_types or []
entity_types = entity_types or []
# Parse tag:<value> shorthand at tool level so it works with all search modes.
# Without this, hybrid/vector modes fail because they require non-empty text,
# but the service-layer tag: parser clears the text after the mode is set.
if query and query.strip().lower().startswith("tag:"):
tag_values = [t for t in re.split(r"[,\s]+", query.strip()[4:].strip()) if t]
if tag_values:
# Merge with any explicitly provided tags
tags = list(set((tags or []) + tag_values))
query = None
# Detect project from memory URL prefix before routing
if project is None and query is not None:
detected = detect_project_from_url_prefix(query, ConfigManager().config)