Skip to content

Commit d15ccc1

Browse files
committed
Add auto-refresh mechanism with 30-second rate limiting
- Add automatic index refresh for find_files tool when files are added/removed - Implement 30-second rate limiting to prevent excessive refreshes during active development - Use persistent configuration storage to support stateless MCP clients - Add in-memory caching to reduce IO overhead - Simplify tool outputs to reduce LLM token usage while maintaining necessary guidance - Manual refresh_index tool respects the same rate limiting mechanism Design decisions: - Added comprehensive LLM guidance comments for manual refresh scenarios - Balanced implementation complexity vs practical benefits - Auto-refresh handles common cases (new files) while edge cases (deleted/moved files) are handled through LLM-initiated manual refresh with clear tool documentation
1 parent 4567dc9 commit d15ccc1

File tree

2 files changed

+135
-7
lines changed

2 files changed

+135
-7
lines changed

.gitignore

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -48,4 +48,5 @@ CLAUDE.md
4848
claude_*
4949
COMMIT_MESSAGE.txt
5050

51-
.llm-context/
51+
.llm-context/
52+
.kiro/

src/code_index_mcp/server.py

Lines changed: 133 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@
1010
import os
1111
import sys
1212
import tempfile
13+
import time
1314
from contextlib import asynccontextmanager
1415
from dataclasses import dataclass
1516
from 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()
403510
def 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()
470577
def 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

Comments
 (0)