Skip to content

Commit 4777fa1

Browse files
committed
feat: implement automatic file watcher system for real-time index updates
Add comprehensive file monitoring capabilities using watchdog library to automatically rebuild project index when files change, eliminating need for manual refresh operations. Key features: - Real-time file system monitoring with cross-platform support - Intelligent debouncing to batch rapid file changes (configurable) - Background async index rebuilding without blocking search operations - Configurable exclude patterns and file type filtering - Graceful fallback to manual refresh if watcher unavailable - New MCP tools for file watcher status and configuration management Technical improvements: - Add FileWatcherService with Observer pattern for file monitoring - Implement background rebuild system in IndexService using asyncio - Enhanced ProjectService to automatically start file watcher on project setup - Remove auto-refresh rate limiting (replaced by proactive monitoring) - Add watchdog>=3.0.0 dependency for cross-platform file monitoring This eliminates the previous 5-second rate limit and provides near-instantaneous index updates when files change, greatly improving developer experience.
1 parent 3cbff23 commit 4777fa1

File tree

8 files changed

+701
-96
lines changed

8 files changed

+701
-96
lines changed

pyproject.toml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@ authors = [
1414
]
1515
dependencies = [
1616
"mcp>=0.3.0",
17+
"watchdog>=3.0.0",
1718
]
1819

1920
[project.urls]

src/code_index_mcp/project_settings.py

Lines changed: 45 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -568,3 +568,48 @@ def refresh_available_strategies(self):
568568
print("Refreshing available search strategies...")
569569
self.available_strategies = _get_available_strategies()
570570
print(f"Available strategies found: {[s.name for s in self.available_strategies]}")
571+
572+
def get_file_watcher_config(self) -> dict:
573+
"""
574+
Get file watcher specific configuration.
575+
576+
Returns:
577+
dict: File watcher configuration with defaults
578+
"""
579+
config = self.load_config()
580+
default_config = {
581+
"enabled": True,
582+
"debounce_seconds": 3.0,
583+
"additional_exclude_patterns": [],
584+
"monitored_extensions": [], # Empty = use all supported extensions
585+
"exclude_patterns": [
586+
".git", ".svn", ".hg",
587+
"node_modules", "__pycache__", ".venv", "venv",
588+
".DS_Store", "Thumbs.db",
589+
"dist", "build", "target", ".idea", ".vscode",
590+
".pytest_cache", ".coverage", ".tox",
591+
"bin", "obj"
592+
]
593+
}
594+
595+
# Merge with loaded config
596+
file_watcher_config = config.get("file_watcher", {})
597+
for key, default_value in default_config.items():
598+
if key not in file_watcher_config:
599+
file_watcher_config[key] = default_value
600+
601+
return file_watcher_config
602+
603+
def update_file_watcher_config(self, updates: dict) -> None:
604+
"""
605+
Update file watcher configuration.
606+
607+
Args:
608+
updates: Dictionary of configuration updates
609+
"""
610+
config = self.load_config()
611+
if "file_watcher" not in config:
612+
config["file_watcher"] = self.get_file_watcher_config()
613+
614+
config["file_watcher"].update(updates)
615+
self.save_config(config)

src/code_index_mcp/server.py

Lines changed: 86 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -22,7 +22,7 @@
2222
from .project_settings import ProjectSettings
2323
from .services import (
2424
ProjectService, IndexService, SearchService,
25-
FileService, SettingsService
25+
FileService, SettingsService, FileWatcherService
2626
)
2727
from .services.settings_service import manage_temp_directory
2828
from .utils import (
@@ -37,6 +37,7 @@ class CodeIndexerContext:
3737
file_count: int = 0
3838
file_index: dict = field(default_factory=dict)
3939
index_cache: dict = field(default_factory=dict)
40+
file_watcher_service: FileWatcherService = None
4041

4142
@asynccontextmanager
4243
async def indexer_lifespan(_server: FastMCP) -> AsyncIterator[CodeIndexerContext]:
@@ -49,17 +50,23 @@ async def indexer_lifespan(_server: FastMCP) -> AsyncIterator[CodeIndexerContext
4950
# Initialize settings manager with skip_load=True to skip loading files
5051
settings = ProjectSettings(base_path, skip_load=True)
5152

52-
# Initialize context
53+
# Initialize context - file watcher will be initialized later when project path is set
5354
context = CodeIndexerContext(
5455
base_path=base_path,
55-
settings=settings
56+
settings=settings,
57+
file_watcher_service=None
5658
)
5759

5860
try:
5961
print("Server ready. Waiting for user to set project path...")
6062
# Provide context to the server
6163
yield context
6264
finally:
65+
# Stop file watcher if it was started
66+
if context.file_watcher_service:
67+
print("Stopping file watcher service...")
68+
await context.file_watcher_service.stop_monitoring()
69+
6370
# Only save index if project path has been set
6471
if context.base_path and context.index_cache:
6572
print(f"Saving index for project: {context.base_path}")
@@ -203,18 +210,18 @@ def refresh_index(ctx: Context) -> str:
203210
Manually refresh the project index when files have been added/removed/moved.
204211
205212
Use when:
206-
- After AI/LLM has created, modified, or deleted files
207-
- After git operations (checkout, merge, pull) that change files
213+
- File watcher is disabled or unavailable
214+
- After large-scale operations (git checkout, merge, pull) that change many files
215+
- When you want immediate index rebuild without waiting for file watcher debounce
208216
- When find_files results seem incomplete or outdated
209-
- For immediate refresh without waiting for auto-refresh rate limits
217+
- For troubleshooting suspected index synchronization issues
210218
211219
Important notes for LLMs:
212-
- This tool bypasses the 5-second rate limit that applies to auto-refresh
213-
- Always available for immediate use when you know files have changed
220+
- Always available as backup when file watcher is not working
214221
- Performs full project re-indexing for complete accuracy
215222
- Use when you suspect the index is stale after file system changes
216-
- **Always call this after modifying files programmatically**
217-
- Essential for find_files tool to see new/changed files
223+
- **Call this after programmatic file modifications if file watcher seems unresponsive**
224+
- Complements the automatic file watcher system
218225
219226
Returns:
220227
Success message with total file count
@@ -254,6 +261,75 @@ def refresh_search_tools(ctx: Context) -> str:
254261
"""
255262
return SearchService(ctx).refresh_search_tools()
256263

264+
@mcp.tool()
265+
@handle_mcp_tool_errors(return_type='dict')
266+
def get_file_watcher_status(ctx: Context) -> Dict[str, Any]:
267+
"""Get file watcher service status and statistics."""
268+
try:
269+
# Get file watcher service from context
270+
file_watcher_service = None
271+
if hasattr(ctx.request_context.lifespan_context, 'file_watcher_service'):
272+
file_watcher_service = ctx.request_context.lifespan_context.file_watcher_service
273+
274+
if not file_watcher_service:
275+
return {"status": "not_initialized", "message": "File watcher service not initialized"}
276+
277+
# Get status from file watcher service
278+
status = file_watcher_service.get_status()
279+
280+
# Add index service status
281+
index_service = IndexService(ctx)
282+
rebuild_status = index_service.get_rebuild_status()
283+
status["rebuild_status"] = rebuild_status
284+
285+
# Add configuration
286+
if hasattr(ctx.request_context.lifespan_context, 'settings') and ctx.request_context.lifespan_context.settings:
287+
file_watcher_config = ctx.request_context.lifespan_context.settings.get_file_watcher_config()
288+
status["configuration"] = file_watcher_config
289+
290+
return status
291+
292+
except Exception as e:
293+
return {"status": "error", "message": f"Failed to get file watcher status: {e}"}
294+
295+
@mcp.tool()
296+
@handle_mcp_tool_errors(return_type='str')
297+
def configure_file_watcher(
298+
ctx: Context,
299+
enabled: bool = None,
300+
debounce_seconds: float = None,
301+
additional_exclude_patterns: list = None
302+
) -> str:
303+
"""Configure file watcher service settings."""
304+
try:
305+
# Get settings from context
306+
if not hasattr(ctx.request_context.lifespan_context, 'settings') or not ctx.request_context.lifespan_context.settings:
307+
return "Settings not available - project path not set"
308+
309+
settings = ctx.request_context.lifespan_context.settings
310+
311+
# Build updates dictionary
312+
updates = {}
313+
if enabled is not None:
314+
updates["enabled"] = enabled
315+
if debounce_seconds is not None:
316+
updates["debounce_seconds"] = debounce_seconds
317+
if additional_exclude_patterns is not None:
318+
updates["additional_exclude_patterns"] = additional_exclude_patterns
319+
320+
if not updates:
321+
return "No configuration changes specified"
322+
323+
# Update configuration
324+
settings.update_file_watcher_config(updates)
325+
326+
# If file watcher is running, we would need to restart it for changes to take effect
327+
# For now, just return success message with note about restart
328+
return f"File watcher configuration updated: {updates}. Restart may be required for changes to take effect."
329+
330+
except Exception as e:
331+
return f"Failed to update file watcher configuration: {e}"
332+
257333
# ----- PROMPTS -----
258334

259335
@mcp.prompt()

src/code_index_mcp/services/__init__.py

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -24,12 +24,14 @@
2424
from .search_service import SearchService
2525
from .file_service import FileService
2626
from .settings_service import SettingsService
27+
from .file_watcher_service import FileWatcherService
2728

2829
__all__ = [
2930
'BaseService',
3031
'ProjectService',
3132
'IndexService',
3233
'SearchService',
3334
'FileService',
34-
'SettingsService'
35+
'SettingsService',
36+
'FileWatcherService'
3537
]

0 commit comments

Comments
 (0)