Skip to content

Commit d4025ba

Browse files
committed
Fix issue #75: [BUG] Cursor has errors calling search tool
Automated fix generated by Claude Code based on issue analysis. --- 🤖 Generated with Claude Code GitHub Action
1 parent f588529 commit d4025ba

9 files changed

Lines changed: 166 additions & 41 deletions

File tree

claude-output/claude_error_75.log

Whitespace-only changes.
Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
Starting issue-fix mode at Sat Apr 5 18:02:54 UTC 2025
2+
Fetching issue #75 details
3+
Using repository: basicmachines-co/basic-memory
4+
Checking if phernandez is a member of organization basicmachines-co
5+
User phernandez is a member of organization basicmachines-co. Proceeding with Claude fix.
6+
Creating a new branch: fix-issue-75-20250405180254
7+
From https://github.com/basicmachines-co/basic-memory
8+
* branch main -> FETCH_HEAD
9+
Switched to a new branch 'fix-issue-75-20250405180254'
10+
branch 'fix-issue-75-20250405180254' set up to track 'origin/main'.
11+
Prompt saved to ./claude-output/claude_prompt_75.txt for debugging
12+
Running Claude to fix the issue...
13+
Committing changes...

claude-output/claude_output_75.txt

Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,35 @@
1+
Let's summarize the changes we've made to fix issue #75:
2+
3+
1. We updated the `search_notes` tool in `/src/basic_memory/mcp/tools/search.py` to accept primitive types as parameters instead of a complex Pydantic `SearchQuery` object. This makes it easier for LLMs like Cursor to make proper tool calls.
4+
5+
2. We converted the internal implementation to create a SearchQuery object from the primitive parameters, maintaining backward compatibility with the existing API.
6+
7+
3. We updated tests in `/tests/mcp/test_tool_search.py` to use the new function signature with primitive parameters.
8+
9+
4. We updated code in `/src/basic_memory/mcp/tools/read_note.py` to use the new function signature when making calls to `search_notes`.
10+
11+
5. We updated code in `/src/basic_memory/mcp/prompts/search.py` to use the new function signature when making calls to `search_notes`.
12+
13+
These changes should make it easier for Cursor and other LLMs to use the search_notes tool by eliminating the complex Pydantic object parameter in favor of simple primitive parameters.
14+
15+
---SUMMARY---
16+
Fixed issue #75 where Cursor was having errors calling the search_notes tool. The problem was that the search_notes tool was expecting a complex Pydantic object (SearchQuery) as input, which was confusing Cursor.
17+
18+
Changes:
19+
1. Modified the search_notes tool to accept primitive types (strings, lists, etc.) as parameters instead of a complex Pydantic object
20+
2. Updated the implementation to create a SearchQuery object internally from these primitive parameters
21+
3. Updated all call sites in the codebase that were using the old function signature
22+
4. Updated tests to use the new function signature
23+
24+
The fix makes it easier for LLMs like Cursor to make proper calls to the search_notes tool, which will resolve the reported error messages:
25+
- "Parameter 'query' must be of type undefined, got object"
26+
- "Parameter 'query' must be of type undefined, got string"
27+
- "Invalid type for parameter 'query' in tool search_notes"
28+
29+
Files modified:
30+
- src/basic_memory/mcp/tools/search.py
31+
- src/basic_memory/mcp/tools/read_note.py
32+
- src/basic_memory/mcp/prompts/search.py
33+
- tests/mcp/test_tool_search.py
34+
- tests/mcp/test_tool_read_note.py
35+
---END SUMMARY---

claude-output/claude_prompt_75.txt

Lines changed: 49 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,49 @@
1+
You are Claude, an AI assistant tasked with fixing issues in a GitHub repository.
2+
3+
Issue #75: [BUG] Cursor has errors calling search tool
4+
5+
Issue Description:
6+
## Bug Description
7+
8+
9+
10+
> Cursor cannot figure out how to structure the parameters for that tool call. No matter what Cursor seems to try it gets the errors.
11+
>
12+
> ```Looking at the error messages more carefully:
13+
> - When I pass an object: "Parameter 'query' must be of type undefined, got object"
14+
> - When I pass a string: "Parameter 'query' must be of type undefined, got string"
15+
>
16+
>
17+
>
18+
> and then it reports: "Invalid type for parameter 'query' in tool search_notes"
19+
> Any chance you can give me some guidance with this?
20+
>
21+
22+
## Steps To Reproduce
23+
Steps to reproduce the behavior:
24+
25+
try using search tool in Cursor.
26+
27+
## Possible Solution
28+
29+
The tool args should probably be plain text and not json to make it easier to call.
30+
Additional Instructions from User Comment:
31+
let make a PR to implement option #1.
32+
Your task is to:
33+
1. Analyze the issue carefully to understand the problem
34+
2. Look through the repository to identify the relevant files that need to be modified
35+
3. Make precise changes to fix the issue
36+
4. Use the Edit tool to modify files directly when needed
37+
5. Be minimal in your changes - only modify what's necessary to fix the issue
38+
39+
After making changes, provide a summary of what you did in this format:
40+
41+
---SUMMARY---
42+
[Your detailed summary of changes, including which files were modified and how]
43+
---END SUMMARY---
44+
45+
Remember:
46+
- Be specific in your changes
47+
- Only modify files that are necessary to fix the issue
48+
- Follow existing code style and conventions
49+
- Make the minimal changes needed to resolve the issue

src/basic_memory/mcp/prompts/search.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -40,7 +40,7 @@ async def search_prompt(
4040
"""
4141
logger.info(f"Searching knowledge base, query: {query}, timeframe: {timeframe}")
4242

43-
search_results = await search_tool(SearchQuery(text=query, after_date=timeframe))
43+
search_results = await search_tool(query=query, after_date=timeframe)
4444
return format_search_results(query, search_results, timeframe)
4545

4646

src/basic_memory/mcp/tools/read_note.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -63,7 +63,7 @@ async def read_note(identifier: str, page: int = 1, page_size: int = 10) -> str:
6363

6464
# Fallback 1: Try title search via API
6565
logger.info(f"Search title for: {identifier}")
66-
title_results = await search_notes(SearchQuery(title=identifier))
66+
title_results = await search_notes(query=identifier, search_type="title")
6767

6868
if title_results and title_results.results:
6969
result = title_results.results[0] # Get the first/best match
@@ -87,7 +87,7 @@ async def read_note(identifier: str, page: int = 1, page_size: int = 10) -> str:
8787

8888
# Fallback 2: Text search as a last resort
8989
logger.info(f"Title search failed, trying text search for: {identifier}")
90-
text_results = await search_notes(SearchQuery(text=identifier))
90+
text_results = await search_notes(query=identifier, search_type="text")
9191

9292
# We didn't find a direct match, construct a helpful error message
9393
if not text_results or not text_results.results:
Lines changed: 56 additions & 27 deletions
Original file line numberDiff line numberDiff line change
@@ -1,77 +1,106 @@
11
"""Search tools for Basic Memory MCP server."""
22

3+
from typing import Optional, List
34
from loguru import logger
45

56
from basic_memory.mcp.server import mcp
67
from basic_memory.mcp.tools.utils import call_post
7-
from basic_memory.schemas.search import SearchQuery, SearchResponse
8+
from basic_memory.schemas.search import SearchQuery, SearchResponse, SearchItemType
89
from basic_memory.mcp.async_client import client
910

1011

1112
@mcp.tool(
1213
description="Search across all content in the knowledge base.",
1314
)
14-
async def search_notes(query: SearchQuery, page: int = 1, page_size: int = 10) -> SearchResponse:
15+
async def search_notes(
16+
query: str,
17+
page: int = 1,
18+
page_size: int = 10,
19+
search_type: str = "text",
20+
types: Optional[List[str]] = None,
21+
entity_types: Optional[List[str]] = None,
22+
after_date: Optional[str] = None,
23+
) -> SearchResponse:
1524
"""Search across all content in the knowledge base.
1625
1726
This tool searches the knowledge base using full-text search, pattern matching,
1827
or exact permalink lookup. It supports filtering by content type, entity type,
1928
and date.
2029
2130
Args:
22-
query: SearchQuery object with search parameters including:
23-
- text: Full-text search (e.g., "project planning")
24-
Supports boolean operators: AND, OR, NOT and parentheses for grouping
25-
- title: Search only in titles (e.g., "Meeting notes")
26-
- permalink: Exact permalink match (e.g., "docs/meeting-notes")
27-
- permalink_match: Pattern matching for permalinks (e.g., "docs/*-notes")
28-
- types: Optional list of content types to search (e.g., ["entity", "observation"])
29-
- entity_types: Optional list of entity types to filter by (e.g., ["note", "person"])
30-
- after_date: Optional date filter for recent content (e.g., "1 week", "2d")
31+
query: The search query string
3132
page: The page number of results to return (default 1)
3233
page_size: The number of results to return per page (default 10)
34+
search_type: Type of search to perform, one of: "text", "title", "permalink", "permalink_match" (default: "text")
35+
types: Optional list of content types to search (e.g., ["entity", "observation"])
36+
entity_types: Optional list of entity types to filter by (e.g., ["note", "person"])
37+
after_date: Optional date filter for recent content (e.g., "1 week", "2d")
3338
3439
Returns:
3540
SearchResponse with results and pagination info
3641
3742
Examples:
3843
# Basic text search
39-
results = await search_notes(SearchQuery(text="project planning"))
44+
results = await search_notes("project planning")
4045
4146
# Boolean AND search (both terms must be present)
42-
results = await search_notes(SearchQuery(text="project AND planning"))
47+
results = await search_notes("project AND planning")
4348
4449
# Boolean OR search (either term can be present)
45-
results = await search_notes(SearchQuery(text="project OR meeting"))
50+
results = await search_notes("project OR meeting")
4651
4752
# Boolean NOT search (exclude terms)
48-
results = await search_notes(SearchQuery(text="project NOT meeting"))
53+
results = await search_notes("project NOT meeting")
4954
5055
# Boolean search with grouping
51-
results = await search_notes(SearchQuery(text="(project OR planning) AND notes"))
56+
results = await search_notes("(project OR planning) AND notes")
5257
5358
# Search with type filter
54-
results = await search_notes(SearchQuery(
55-
text="meeting notes",
59+
results = await search_notes(
60+
query="meeting notes",
5661
types=["entity"],
57-
))
62+
)
5863
5964
# Search for recent content
60-
results = await search_notes(SearchQuery(
61-
text="bug report",
65+
results = await search_notes(
66+
query="bug report",
6267
after_date="1 week"
63-
))
68+
)
6469
6570
# Pattern matching on permalinks
66-
results = await search_notes(SearchQuery(
67-
permalink_match="docs/meeting-*"
68-
))
71+
results = await search_notes(
72+
query="docs/meeting-*",
73+
search_type="permalink_match"
74+
)
6975
"""
70-
logger.info(f"Searching for {query}")
76+
# Create a SearchQuery object based on the parameters
77+
search_query = SearchQuery()
78+
79+
# Set the appropriate search field based on search_type
80+
if search_type == "text":
81+
search_query.text = query
82+
elif search_type == "title":
83+
search_query.title = query
84+
elif search_type == "permalink":
85+
search_query.permalink = query
86+
elif search_type == "permalink_match":
87+
search_query.permalink_match = query
88+
else:
89+
search_query.text = query # Default to text search
90+
91+
# Add optional filters if provided
92+
if types:
93+
search_query.types = [SearchItemType(t) for t in types]
94+
if entity_types:
95+
search_query.entity_types = entity_types
96+
if after_date:
97+
search_query.after_date = after_date
98+
99+
logger.info(f"Searching for {search_query}")
71100
response = await call_post(
72101
client,
73102
"/search/",
74-
json=query.model_dump(),
103+
json=search_query.model_dump(),
75104
params={"page": page, "page_size": page_size},
76105
)
77106
return SearchResponse.model_validate(response.json())

tests/mcp/test_tool_read_note.py

Lines changed: 6 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -201,7 +201,8 @@ async def test_read_note_title_search_fallback(mock_call_get, mock_search):
201201

202202
# Verify title search was used
203203
mock_search.assert_called_once()
204-
assert mock_search.call_args[0][0].title == "Test Note"
204+
assert mock_search.call_args[1]["query"] == "Test Note"
205+
assert mock_search.call_args[1]["search_type"] == "title"
205206

206207
# Verify second lookup was used
207208
assert mock_call_get.call_count == 2
@@ -254,8 +255,10 @@ async def test_read_note_text_search_fallback(mock_call_get, mock_search):
254255

255256
# Verify both search types were used
256257
assert mock_search.call_count == 2
257-
assert mock_search.call_args_list[0][0][0].title == "some query" # Title search
258-
assert mock_search.call_args_list[1][0][0].text == "some query" # Text search
258+
assert mock_search.call_args_list[0][1]["query"] == "some query" # Title search
259+
assert mock_search.call_args_list[0][1]["search_type"] == "title"
260+
assert mock_search.call_args_list[1][1]["query"] == "some query" # Text search
261+
assert mock_search.call_args_list[1][1]["search_type"] == "text"
259262

260263
# Verify result contains helpful information
261264
assert "Note Not Found" in result

tests/mcp/test_tool_search.py

Lines changed: 4 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -21,8 +21,7 @@ async def test_search_basic(client):
2121
assert result
2222

2323
# Search for it
24-
query = SearchQuery(text="searchable")
25-
response = await search_notes(query)
24+
response = await search_notes(query="searchable")
2625

2726
# Verify results
2827
assert len(response.results) > 0
@@ -42,8 +41,7 @@ async def test_search_pagination(client):
4241
assert result
4342

4443
# Search for it
45-
query = SearchQuery(text="searchable")
46-
response = await search_notes(query, page=1, page_size=1)
44+
response = await search_notes(query="searchable", page=1, page_size=1)
4745

4846
# Verify results
4947
assert len(response.results) == 1
@@ -61,8 +59,7 @@ async def test_search_with_type_filter(client):
6159
)
6260

6361
# Search with type filter
64-
query = SearchQuery(text="type", types=[SearchItemType.ENTITY])
65-
response = await search_notes(query)
62+
response = await search_notes(query="type", types=["entity"])
6663

6764
# Verify all results are entities
6865
assert all(r.type == "entity" for r in response.results)
@@ -80,8 +77,7 @@ async def test_search_with_date_filter(client):
8077

8178
# Search with date filter
8279
one_hour_ago = datetime.now() - timedelta(hours=1)
83-
query = SearchQuery(text="recent", after_date=one_hour_ago)
84-
response = await search_notes(query)
80+
response = await search_notes(query="recent", after_date=one_hour_ago.isoformat())
8581

8682
# Verify we get results within timeframe
8783
assert len(response.results) > 0

0 commit comments

Comments
 (0)