66"""
77
88import json
9- from typing import Any , Dict , List , Optional
9+ from typing import Any , Dict , List , Optional , cast
1010
1111from fastmcp import Context
1212from loguru import logger
1717from basic_memory .schemas .search import SearchResponse , SearchResult
1818
1919
20+ def _identifier_for_read_note (identifier : str ) -> str :
21+ """Convert ChatGPT result ids into routable Basic Memory identifiers."""
22+ stripped = identifier .strip ()
23+ if stripped .startswith ("memory://" ) or "/" not in stripped :
24+ return identifier
25+ return f"memory://{ stripped } "
26+
27+
2028def _format_search_results_for_chatgpt (
21- results : SearchResponse | list [SearchResult ] | list [ dict [str , Any ]] | dict [str , Any ],
29+ results : SearchResponse | list [SearchResult | dict [str , Any ]] | dict [str , Any ],
2230) -> List [Dict [str , Any ]]:
2331 """Format search results according to ChatGPT's expected schema.
2432
2533 Returns a list of result objects with id, title, and url fields.
2634 """
2735 if isinstance (results , SearchResponse ):
28- raw_results : list [SearchResult ] | list [ dict [str , Any ]] = results .results
36+ raw_results : list [SearchResult | dict [str , Any ]] = list ( results .results )
2937 elif isinstance (results , dict ):
3038 nested_results = results .get ("results" )
31- raw_results = nested_results if isinstance (nested_results , list ) else []
39+ raw_results = (
40+ cast (list [SearchResult | dict [str , Any ]], nested_results )
41+ if isinstance (nested_results , list )
42+ else []
43+ )
3244 else :
3345 raw_results = results
3446
@@ -113,8 +125,7 @@ async def search(
113125 logger .info (f"ChatGPT search request: query='{ query } '" )
114126
115127 try :
116- # Let search_notes resolve the default project via get_project_client(),
117- # which works in both local mode (ConfigManager) and cloud mode (database).
128+ # Keep this adapter tiny: the real search behavior lives in search_notes.
118129 results = await search_notes (
119130 query = query ,
120131 page = 1 ,
@@ -123,24 +134,24 @@ async def search(
123134 context = context ,
124135 )
125136
126- # Handle string error responses from search_notes
127137 if isinstance (results , str ):
128138 logger .warning (f"Search failed with error: { results [:100 ]} ..." )
129139 search_results = {
130140 "results" : [],
131141 "error" : "Search failed" ,
132142 "error_details" : results [:500 ], # Truncate long error messages
133143 }
134- else :
135- # Format successful results for ChatGPT
136- raw_results = results .get ("results" , []) if isinstance (results , dict ) else []
137- formatted_results = _format_search_results_for_chatgpt (raw_results )
138- search_results = {
139- "results" : formatted_results ,
140- "total_count" : len (raw_results ), # Use actual count from results
141- "query" : query ,
142- }
143- logger .info (f"Search completed: { len (formatted_results )} results returned" )
144+ return [{"type" : "text" , "text" : json .dumps (search_results , ensure_ascii = False )}]
145+
146+ raw_results = results .get ("results" , []) if isinstance (results , dict ) else []
147+
148+ formatted_results = _format_search_results_for_chatgpt (raw_results )
149+ search_results = {
150+ "results" : formatted_results ,
151+ "total_count" : len (raw_results ), # Use actual count from results
152+ "query" : query ,
153+ }
154+ logger .info (f"Search completed: { len (formatted_results )} results returned" )
144155
145156 # Return in MCP content array format as required by OpenAI
146157 return [{"type" : "text" , "text" : json .dumps (search_results , ensure_ascii = False )}]
@@ -180,7 +191,7 @@ async def fetch(
180191 # which works in both local mode (ConfigManager) and cloud mode (database).
181192 content = str (
182193 await read_note (
183- identifier = id ,
194+ identifier = _identifier_for_read_note ( id ) ,
184195 context = context ,
185196 )
186197 )
0 commit comments