Skip to content

Commit 58c6532

Browse files
committed
feat: enhance file_summary functionality with comprehensive index-based analysis
This commit significantly improves the file_summary MCP tool by refactoring it to provide comprehensive analysis data directly from the project index, eliminating real-time file analysis overhead. **Core File Summary Improvements:** - Refactor FileService.analyze_file() to rely exclusively on indexed data for faster, consistent results - Add comprehensive index entry validation with _validate_index_entry() method - Return complete relationship data including function calls, class inheritance, and import dependencies - Improve error handling with detailed validation of index data structure integrity - Remove dependency on AnalyzerFactory for better separation of concerns and performance **Enhanced Data Quality and Validation:** - Add robust validation for functions, classes, and imports data structures in index entries - Support both legacy string format and enhanced object format for backward compatibility - Validate function metadata including parameters, decorators, async status, and call relationships - Validate class inheritance chains and instantiation relationships - Ensure import statements include complete module and type information **Path Handling Improvements:** - Enhance normalize_file_path() function to handle edge cases and provide consistent path normalization - Update scanner.py and search/base.py to use centralized path normalization - Improve cross-platform path handling with os.path.normpath() integration - Fix path separator consistency throughout the indexing and search systems **Fallback Strategy Enhancements:** - Implement intelligent project-aware fallback directories (project → home → system temp) - Replace hardcoded current directory fallbacks with context-aware alternatives - Improve error handling for directory access issues in restricted environments - Enhanced ProjectSettings robustness for various deployment scenarios **Response Format Improvements:** - Enhanced ResponseFormatter to handle complex index data structures - Better error reporting and validation feedback for malformed index entries - Improved utilities for file operation validation and path handling The file_summary tool now provides rich, structured analysis data including complete function signatures, class relationships, import dependencies, and language-specific metadata, all sourced from the optimized project index for maximum performance and consistency.
1 parent 4777fa1 commit 58c6532

File tree

8 files changed

+519
-140
lines changed

8 files changed

+519
-140
lines changed

src/code_index_mcp/indexing/qualified_names.py

Lines changed: 11 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -90,15 +90,24 @@ def validate_qualified_name(qualified_name: str) -> bool:
9090

9191
def normalize_file_path(file_path: str) -> str:
9292
"""
93-
Normalize file path for consistent qualified name generation.
93+
Normalize file path for consistent use throughout the codebase.
94+
95+
This function provides a unified way to normalize file paths by:
96+
1. Converting all path separators to forward slashes
97+
2. Normalizing the path structure (removing redundant separators, etc.)
9498
9599
Args:
96100
file_path: File path to normalize
97101
98102
Returns:
99103
Normalized file path with forward slashes
100104
"""
101-
return file_path.replace(os.sep, '/')
105+
if not file_path:
106+
return file_path
107+
108+
# First normalize the path structure, then convert separators
109+
normalized = os.path.normpath(file_path)
110+
return normalized.replace(os.sep, '/')
102111

103112

104113
def get_file_path_from_qualified_name(qualified_name: str) -> str:

src/code_index_mcp/indexing/scanner.py

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@
1111
from datetime import datetime
1212
from typing import Dict, List, Any
1313
from .models import FileInfo, ProjectScanResult, SpecialFiles
14+
from .qualified_names import normalize_file_path
1415
from code_index_mcp.constants import SUPPORTED_EXTENSIONS
1516

1617

@@ -115,7 +116,7 @@ def _discover_files(self) -> List[str]:
115116
file_path = os.path.join(root, filename)
116117
# Convert to relative path from base_path
117118
rel_path = os.path.relpath(file_path, self.base_path)
118-
files.append(rel_path.replace('\\', '/')) # Normalize path separators
119+
files.append(normalize_file_path(rel_path)) # Normalize path separators
119120

120121
return files
121122

src/code_index_mcp/project_settings.py

Lines changed: 95 additions & 36 deletions
Original file line numberDiff line numberDiff line change
@@ -74,15 +74,25 @@ def __init__(self, base_path, skip_load=False):
7474
# Check if the system temporary directory exists and is writable
7575
if not os.path.exists(system_temp):
7676
print(f"Warning: System temporary directory does not exist: {system_temp}")
77-
# Try using current directory as fallback
78-
system_temp = os.getcwd()
79-
print(f"Using current directory as fallback: {system_temp}")
77+
# Try using project directory as fallback if available
78+
if base_path and os.path.exists(base_path):
79+
system_temp = base_path
80+
print(f"Using project directory as fallback: {system_temp}")
81+
else:
82+
# Use user's home directory as last resort
83+
system_temp = os.path.expanduser("~")
84+
print(f"Using home directory as fallback: {system_temp}")
8085

8186
if not os.access(system_temp, os.W_OK):
8287
print(f"Warning: No write access to system temporary directory: {system_temp}")
83-
# Try using current directory as fallback
84-
system_temp = os.getcwd()
85-
print(f"Using current directory as fallback: {system_temp}")
88+
# Try using project directory as fallback if available
89+
if base_path and os.path.exists(base_path) and os.access(base_path, os.W_OK):
90+
system_temp = base_path
91+
print(f"Using project directory as fallback: {system_temp}")
92+
else:
93+
# Use user's home directory as last resort
94+
system_temp = os.path.expanduser("~")
95+
print(f"Using home directory as fallback: {system_temp}")
8696

8797
# Create code_indexer directory
8898
temp_base_dir = os.path.join(system_temp, SETTINGS_DIR)
@@ -96,9 +106,14 @@ def __init__(self, base_path, skip_load=False):
96106
print(f"Code indexer directory already exists: {temp_base_dir}")
97107
except Exception as e:
98108
print(f"Error setting up temporary directory: {e}")
99-
# If unable to create temporary directory, use .code_indexer in current directory
100-
temp_base_dir = os.path.join(os.getcwd(), ".code_indexer")
101-
print(f"Using fallback directory: {temp_base_dir}")
109+
# If unable to create temporary directory, use .code_indexer in project directory if available
110+
if base_path and os.path.exists(base_path):
111+
temp_base_dir = os.path.join(base_path, ".code_indexer")
112+
print(f"Using project fallback directory: {temp_base_dir}")
113+
else:
114+
# Use home directory as last resort
115+
temp_base_dir = os.path.join(os.path.expanduser("~"), ".code_indexer")
116+
print(f"Using home fallback directory: {temp_base_dir}")
102117
if not os.path.exists(temp_base_dir):
103118
os.makedirs(temp_base_dir, exist_ok=True)
104119

@@ -117,9 +132,13 @@ def __init__(self, base_path, skip_load=False):
117132
self.ensure_settings_dir()
118133
except Exception as e:
119134
print(f"Error setting up project settings: {e}")
120-
# If error occurs, use .code_indexer in current directory as fallback
121-
fallback_dir = os.path.join(os.getcwd(), ".code_indexer",
122-
"default" if not base_path else hashlib.md5(base_path.encode()).hexdigest())
135+
# If error occurs, use .code_indexer in project or home directory as fallback
136+
if base_path and os.path.exists(base_path):
137+
fallback_dir = os.path.join(base_path, ".code_indexer",
138+
hashlib.md5(base_path.encode()).hexdigest())
139+
else:
140+
fallback_dir = os.path.join(os.path.expanduser("~"), ".code_indexer",
141+
"default" if not base_path else hashlib.md5(base_path.encode()).hexdigest())
123142
print(f"Using fallback directory: {fallback_dir}")
124143
self.settings_path = fallback_dir
125144
if not os.path.exists(fallback_dir):
@@ -141,18 +160,26 @@ def ensure_settings_dir(self):
141160
# Check if directory is writable
142161
if not os.access(self.settings_path, os.W_OK):
143162
print(f"Warning: No write access to project settings directory: {self.settings_path}")
144-
# If directory is not writable, use .code_indexer in current directory as fallback
145-
fallback_dir = os.path.join(os.getcwd(), ".code_indexer",
146-
os.path.basename(self.settings_path))
163+
# If directory is not writable, use .code_indexer in project or home directory as fallback
164+
if self.base_path and os.path.exists(self.base_path) and os.access(self.base_path, os.W_OK):
165+
fallback_dir = os.path.join(self.base_path, ".code_indexer",
166+
os.path.basename(self.settings_path))
167+
else:
168+
fallback_dir = os.path.join(os.path.expanduser("~"), ".code_indexer",
169+
os.path.basename(self.settings_path))
147170
print(f"Using fallback directory: {fallback_dir}")
148171
self.settings_path = fallback_dir
149172
if not os.path.exists(fallback_dir):
150173
os.makedirs(fallback_dir, exist_ok=True)
151174
except Exception as e:
152175
print(f"Error ensuring settings directory: {e}")
153-
# If unable to create settings directory, use .code_indexer in current directory
154-
fallback_dir = os.path.join(os.getcwd(), ".code_indexer",
155-
"default" if not self.base_path else hashlib.md5(self.base_path.encode()).hexdigest())
176+
# If unable to create settings directory, use .code_indexer in project or home directory
177+
if self.base_path and os.path.exists(self.base_path):
178+
fallback_dir = os.path.join(self.base_path, ".code_indexer",
179+
hashlib.md5(self.base_path.encode()).hexdigest())
180+
else:
181+
fallback_dir = os.path.join(os.path.expanduser("~"), ".code_indexer",
182+
"default" if not self.base_path else hashlib.md5(self.base_path.encode()).hexdigest())
156183
print(f"Using fallback directory: {fallback_dir}")
157184
self.settings_path = fallback_dir
158185
if not os.path.exists(fallback_dir):
@@ -167,8 +194,11 @@ def get_config_path(self):
167194
return path
168195
except Exception as e:
169196
print(f"Error getting config path: {e}")
170-
# If error occurs, use file in current directory as fallback
171-
return os.path.join(os.getcwd(), CONFIG_FILE)
197+
# If error occurs, use file in project or home directory as fallback
198+
if self.base_path and os.path.exists(self.base_path):
199+
return os.path.join(self.base_path, CONFIG_FILE)
200+
else:
201+
return os.path.join(os.path.expanduser("~"), CONFIG_FILE)
172202

173203
def get_index_path(self):
174204
"""Get the path to the index file"""
@@ -179,8 +209,11 @@ def get_index_path(self):
179209
return path
180210
except Exception as e:
181211
print(f"Error getting index path: {e}")
182-
# If error occurs, use file in current directory as fallback
183-
return os.path.join(os.getcwd(), INDEX_FILE)
212+
# If error occurs, use file in project or home directory as fallback
213+
if self.base_path and os.path.exists(self.base_path):
214+
return os.path.join(self.base_path, INDEX_FILE)
215+
else:
216+
return os.path.join(os.path.expanduser("~"), INDEX_FILE)
184217

185218
# get_cache_path method removed - no longer needed with new indexing system
186219

@@ -259,8 +292,11 @@ def save_index(self, index_data):
259292
# Check if directory is writable
260293
if not os.access(dir_path, os.W_OK):
261294
print(f"Warning: Directory is not writable: {dir_path}")
262-
# Use current directory as fallback
263-
index_path = os.path.join(os.getcwd(), INDEX_FILE)
295+
# Use project or home directory as fallback
296+
if self.base_path and os.path.exists(self.base_path):
297+
index_path = os.path.join(self.base_path, INDEX_FILE)
298+
else:
299+
index_path = os.path.join(os.path.expanduser("~"), INDEX_FILE)
264300
print(f"Using fallback path: {index_path}")
265301

266302
# Convert to JSON string if it's a CodeIndex object
@@ -278,9 +314,12 @@ def save_index(self, index_data):
278314
print(f"Index saved successfully to: {index_path}")
279315
except Exception as e:
280316
print(f"Error saving index: {e}")
281-
# Try saving to current directory
317+
# Try saving to project or home directory
282318
try:
283-
fallback_path = os.path.join(os.getcwd(), INDEX_FILE)
319+
if self.base_path and os.path.exists(self.base_path):
320+
fallback_path = os.path.join(self.base_path, INDEX_FILE)
321+
else:
322+
fallback_path = os.path.join(os.path.expanduser("~"), INDEX_FILE)
284323
print(f"Trying fallback path: {fallback_path}")
285324

286325
# Convert to JSON string if it's a CodeIndex object
@@ -324,8 +363,11 @@ def load_index(self):
324363
print(f"Unexpected error loading index: {e}")
325364
return None
326365
else:
327-
# Try loading from current directory
328-
fallback_path = os.path.join(os.getcwd(), INDEX_FILE)
366+
# Try loading from project or home directory
367+
if self.base_path and os.path.exists(self.base_path):
368+
fallback_path = os.path.join(self.base_path, INDEX_FILE)
369+
else:
370+
fallback_path = os.path.join(os.path.expanduser("~"), INDEX_FILE)
329371
if os.path.exists(fallback_path):
330372
print(f"Trying fallback path: {fallback_path}")
331373
try:
@@ -376,8 +418,12 @@ def detect_index_version(self):
376418
return 'legacy'
377419

378420
# Check fallback locations
379-
fallback_json = os.path.join(os.getcwd(), INDEX_FILE)
380-
fallback_pickle = os.path.join(os.getcwd(), "file_index.pickle")
421+
if self.base_path and os.path.exists(self.base_path):
422+
fallback_json = os.path.join(self.base_path, INDEX_FILE)
423+
fallback_pickle = os.path.join(self.base_path, "file_index.pickle")
424+
else:
425+
fallback_json = os.path.join(os.path.expanduser("~"), INDEX_FILE)
426+
fallback_pickle = os.path.join(os.path.expanduser("~"), "file_index.pickle")
381427

382428
if os.path.exists(fallback_json):
383429
try:
@@ -427,11 +473,21 @@ def migrate_legacy_index(self):
427473
# Clean up legacy files
428474
legacy_files = [
429475
os.path.join(self.settings_path, "file_index.pickle"),
430-
os.path.join(self.settings_path, "content_cache.pickle"),
431-
os.path.join(os.getcwd(), "file_index.pickle"),
432-
os.path.join(os.getcwd(), "content_cache.pickle")
476+
os.path.join(self.settings_path, "content_cache.pickle")
433477
]
434478

479+
# Add fallback locations
480+
if self.base_path and os.path.exists(self.base_path):
481+
legacy_files.extend([
482+
os.path.join(self.base_path, "file_index.pickle"),
483+
os.path.join(self.base_path, "content_cache.pickle")
484+
])
485+
else:
486+
legacy_files.extend([
487+
os.path.join(os.path.expanduser("~"), "file_index.pickle"),
488+
os.path.join(os.path.expanduser("~"), "content_cache.pickle")
489+
])
490+
435491
for legacy_file in legacy_files:
436492
if os.path.exists(legacy_file):
437493
try:
@@ -493,7 +549,7 @@ def get_stats(self):
493549
'writable': os.access(self.settings_path, os.W_OK) if os.path.exists(self.settings_path) else False,
494550
'files': {},
495551
'temp_dir': tempfile.gettempdir(),
496-
'current_dir': os.getcwd()
552+
'base_path': self.base_path
497553
}
498554

499555
if stats['exists'] and stats['is_directory']:
@@ -524,7 +580,10 @@ def get_stats(self):
524580
stats['list_error'] = str(e)
525581

526582
# Check fallback path
527-
fallback_dir = os.path.join(os.getcwd(), ".code_indexer")
583+
if self.base_path and os.path.exists(self.base_path):
584+
fallback_dir = os.path.join(self.base_path, ".code_indexer")
585+
else:
586+
fallback_dir = os.path.join(os.path.expanduser("~"), ".code_indexer")
528587
stats['fallback_path'] = fallback_dir
529588
stats['fallback_exists'] = os.path.exists(fallback_dir)
530589
stats['fallback_is_directory'] = os.path.isdir(fallback_dir) if os.path.exists(fallback_dir) else False
@@ -536,7 +595,7 @@ def get_stats(self):
536595
'error': str(e),
537596
'settings_path': self.settings_path,
538597
'temp_dir': tempfile.gettempdir(),
539-
'current_dir': os.getcwd()
598+
'base_path': self.base_path
540599
}
541600

542601
def get_search_tools_config(self):

src/code_index_mcp/search/base.py

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,8 @@
1212
from abc import ABC, abstractmethod
1313
from typing import Dict, List, Optional, Tuple, Any
1414

15+
from ..indexing.qualified_names import normalize_file_path
16+
1517
def parse_search_output(output: str, base_path: str) -> Dict[str, List[Tuple[int, str]]]:
1618
"""
1719
Parse the output of command-line search tools (grep, ag, rg).
@@ -49,7 +51,7 @@ def parse_search_output(output: str, base_path: str) -> Dict[str, List[Tuple[int
4951
relative_path = os.path.relpath(file_path_abs, normalized_base_path)
5052

5153
# Normalize path separators for consistency
52-
relative_path = relative_path.replace('\\', '/')
54+
relative_path = normalize_file_path(relative_path)
5355

5456
if relative_path not in results:
5557
results[relative_path] = []

0 commit comments

Comments
 (0)