1010import os
1111import sys
1212import tempfile
13+ import time
1314from contextlib import asynccontextmanager
1415from dataclasses import dataclass
1516from typing import AsyncIterator , Dict , List , Optional , Tuple , Any
@@ -217,6 +218,51 @@ def get_settings_stats() -> str:
217218
218219 return json .dumps (stats , indent = 2 )
219220
221+ # ----- AUTO-REFRESH HELPERS -----
222+
223+ REFRESH_RATE_LIMIT_SECONDS = 30
224+
225+ # Memory cache for refresh time (loaded once per server session)
226+ _cached_last_refresh_time = None
227+
228+ def _get_last_refresh_time (ctx : Context ) -> float :
229+ """Get last refresh time, with memory cache for performance."""
230+ global _cached_last_refresh_time
231+
232+ # Load from config only once per server session
233+ if _cached_last_refresh_time is None :
234+ config = ctx .request_context .lifespan_context .settings .load_config ()
235+ _cached_last_refresh_time = config .get ('last_auto_refresh_time' , 0.0 )
236+
237+ return _cached_last_refresh_time
238+
239+ def _should_auto_refresh (ctx : Context ) -> bool :
240+ """Check if auto-refresh is allowed based on 30-second rate limit."""
241+ last_refresh_time = _get_last_refresh_time (ctx )
242+ current_time = time .time ()
243+ return (current_time - last_refresh_time ) >= REFRESH_RATE_LIMIT_SECONDS
244+
245+ def _update_last_refresh_time (ctx : Context ) -> None :
246+ """Update refresh time in both memory cache and persistent config."""
247+ global _cached_last_refresh_time
248+ current_time = time .time ()
249+
250+ # Update memory cache immediately for performance
251+ _cached_last_refresh_time = current_time
252+
253+ # Persist to config for stateless client support
254+ config = ctx .request_context .lifespan_context .settings .load_config ()
255+ config ['last_auto_refresh_time' ] = current_time
256+ ctx .request_context .lifespan_context .settings .save_config (config )
257+
258+ def _get_remaining_refresh_time (ctx : Context ) -> int :
259+ """Get remaining seconds until next refresh is allowed."""
260+ last_refresh_time = _get_last_refresh_time (ctx )
261+ current_time = time .time ()
262+ elapsed = current_time - last_refresh_time
263+ remaining = max (0 , REFRESH_RATE_LIMIT_SECONDS - elapsed )
264+ return int (remaining )
265+
220266# ----- TOOLS -----
221267
222268@mcp .tool ()
@@ -378,26 +424,87 @@ def search_code_advanced(
378424 return {"error" : f"Search failed using '{ strategy .name } ': { e } " }
379425
380426@mcp .tool ()
381- def find_files (pattern : str , ctx : Context ) -> List [str ]:
382- """Find files in the project matching a specific glob pattern."""
427+ def find_files (pattern : str , ctx : Context ) -> Dict [str , Any ]:
428+ """
429+ Find files matching a glob pattern. Auto-refreshes index if no results found.
430+
431+ Use when:
432+ - Looking for files by pattern (e.g., "*.py", "test_*.js", "src/**/*.ts")
433+ - Checking if specific files exist in the project
434+ - Getting file lists for further analysis
435+
436+ Auto-refresh behavior:
437+ - If no files found, automatically refreshes index once and retries
438+ - Rate limited to once every 30 seconds to avoid excessive refreshes
439+ - Manual refresh_index tool is always available without rate limits
440+
441+ Args:
442+ pattern: Glob pattern to match files (e.g., "*.py", "test_*.js")
443+
444+ Returns:
445+ Dictionary with files list and status information
446+ """
383447 base_path = ctx .request_context .lifespan_context .base_path
384448
385449 # Check if base_path is set
386450 if not base_path :
387- return ["Error: Project path not set. Please use set_project_path to set a project directory first." ]
451+ return {
452+ "error" : "Project path not set. Please use set_project_path to set a project directory first." ,
453+ "files" : []
454+ }
388455
389- # Check if we need to index the project
456+ # Check if we need to index the project initially
390457 if not file_index :
391458 _index_project (base_path )
392459 ctx .request_context .lifespan_context .file_count = _count_files (file_index )
393460 ctx .request_context .lifespan_context .settings .save_index (file_index )
394461
462+ # First search attempt
395463 matching_files = []
396464 for file_path , _ in _get_all_files (file_index ):
397465 if fnmatch .fnmatch (file_path , pattern ):
398466 matching_files .append (file_path )
399467
400- return matching_files
468+ # If no results found, try auto-refresh once (with rate limiting)
469+ if not matching_files :
470+ if _should_auto_refresh (ctx ):
471+ # Perform full re-index
472+ file_index .clear ()
473+ _index_project (base_path )
474+ ctx .request_context .lifespan_context .file_count = _count_files (file_index )
475+ ctx .request_context .lifespan_context .settings .save_index (file_index )
476+
477+ # Update last refresh time
478+ _update_last_refresh_time (ctx )
479+
480+ # Search again after refresh
481+ for file_path , _ in _get_all_files (file_index ):
482+ if fnmatch .fnmatch (file_path , pattern ):
483+ matching_files .append (file_path )
484+
485+ if matching_files :
486+ return {
487+ "files" : matching_files ,
488+ "status" : f"✅ Found { len (matching_files )} files after refresh"
489+ }
490+ else :
491+ return {
492+ "files" : [],
493+ "status" : "⚠️ No files found even after refresh"
494+ }
495+ else :
496+ # Rate limited
497+ remaining_time = _get_remaining_refresh_time (ctx )
498+ return {
499+ "files" : [],
500+ "status" : f"⚠️ No files found - Rate limited. Try again in { remaining_time } seconds"
501+ }
502+
503+ # Return successful results
504+ return {
505+ "files" : matching_files ,
506+ "status" : f"✅ Found { len (matching_files )} files"
507+ }
401508
402509@mcp .tool ()
403510def get_file_summary (file_path : str , ctx : Context ) -> Dict [str , Any ]:
@@ -468,7 +575,24 @@ def get_file_summary(file_path: str, ctx: Context) -> Dict[str, Any]:
468575
469576@mcp .tool ()
470577def refresh_index (ctx : Context ) -> str :
471- """Refresh the project index."""
578+ """
579+ Manually refresh the project index when files have been added/removed/moved.
580+
581+ Use when:
582+ - Files were added, deleted, or moved outside the editor
583+ - After git operations (checkout, merge, pull) that change files
584+ - When find_files results seem incomplete or outdated
585+ - For immediate refresh without waiting for auto-refresh rate limits
586+
587+ Important notes for LLMs:
588+ - This tool bypasses the 30-second rate limit that applies to auto-refresh
589+ - Always available for immediate use when you know files have changed
590+ - Performs full project re-indexing for complete accuracy
591+ - Use when you suspect the index is stale after file system changes
592+
593+ Returns:
594+ Success message with total file count
595+ """
472596 base_path = ctx .request_context .lifespan_context .base_path
473597
474598 # Check if base_path is set
@@ -492,6 +616,9 @@ def refresh_index(ctx: Context) -> str:
492616 ** config ,
493617 'last_indexed' : ctx .request_context .lifespan_context .settings ._get_timestamp ()
494618 })
619+
620+ # Update auto-refresh timer to prevent immediate auto-refresh after manual refresh
621+ _update_last_refresh_time (ctx )
495622
496623 return f"Project re-indexed. Found { file_count } files."
497624
0 commit comments