Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 5 additions & 3 deletions src/code_index_mcp/search/ag.py
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,8 @@ def search(
context_lines: int = 0,
file_pattern: Optional[str] = None,
fuzzy: bool = False,
regex: bool = False
regex: bool = False,
max_line_length: Optional[int] = None
) -> Dict[str, List[Tuple[int, str]]]:
"""
Execute a search using The Silver Searcher (ag).
Expand All @@ -40,6 +41,7 @@ def search(
file_pattern: File pattern to filter
fuzzy: Enable word boundary matching (not true fuzzy search)
regex: Enable regex pattern matching
max_line_length: Optional. Limit the length of lines when context_lines is used
"""
# ag prints line numbers and groups by file by default, which is good.
# --noheading is used to be consistent with other tools' output format.
Expand Down Expand Up @@ -116,10 +118,10 @@ def search(
if process.returncode > 1:
raise RuntimeError(f"ag failed with exit code {process.returncode}: {process.stderr}")

return parse_search_output(process.stdout, base_path)
return parse_search_output(process.stdout, base_path, max_line_length)

except FileNotFoundError:
raise RuntimeError("'ag' (The Silver Searcher) not found. Please install it and ensure it's in your PATH.")
except Exception as e:
# Re-raise other potential exceptions like permission errors
raise RuntimeError(f"An error occurred while running ag: {e}")
raise RuntimeError(f"An error occurred while running ag: {e}")
15 changes: 12 additions & 3 deletions src/code_index_mcp/search/base.py
Original file line number Diff line number Diff line change
Expand Up @@ -14,13 +14,18 @@

from ..indexing.qualified_names import normalize_file_path

def parse_search_output(output: str, base_path: str) -> Dict[str, List[Tuple[int, str]]]:
def parse_search_output(
output: str,
base_path: str,
max_line_length: Optional[int] = None
) -> Dict[str, List[Tuple[int, str]]]:
"""
Parse the output of command-line search tools (grep, ag, rg).

Args:
output: The raw output from the command-line tool.
base_path: The base path of the project to make file paths relative.
max_line_length: Optional maximum line length to truncate long lines.

Returns:
A dictionary where keys are file paths and values are lists of (line_number, line_content) tuples.
Expand Down Expand Up @@ -53,6 +58,10 @@ def parse_search_output(output: str, base_path: str) -> Dict[str, List[Tuple[int
# Normalize path separators for consistency
relative_path = normalize_file_path(relative_path)

# Truncate content if it exceeds max_line_length
if max_line_length and len(content) > max_line_length:
content = content[:max_line_length] + '... (truncated)'

if relative_path not in results:
results[relative_path] = []
results[relative_path].append((line_number, content))
Expand Down Expand Up @@ -175,7 +184,8 @@ def search(
context_lines: int = 0,
file_pattern: Optional[str] = None,
fuzzy: bool = False,
regex: bool = False
regex: bool = False,
max_line_length: Optional[int] = None
) -> Dict[str, List[Tuple[int, str]]]:
"""
Execute a search using the specific strategy.
Expand All @@ -193,4 +203,3 @@ def search(
A dictionary mapping filenames to lists of (line_number, line_content) tuples.
"""
pass

13 changes: 10 additions & 3 deletions src/code_index_mcp/search/basic.py
Original file line number Diff line number Diff line change
Expand Up @@ -46,7 +46,8 @@ def search(
context_lines: int = 0,
file_pattern: Optional[str] = None,
fuzzy: bool = False,
regex: bool = False
regex: bool = False,
max_line_length: Optional[int] = None
) -> Dict[str, List[Tuple[int, str]]]:
"""
Execute a basic, line-by-line search.
Expand All @@ -60,6 +61,7 @@ def search(
file_pattern: File pattern to filter
fuzzy: Enable word boundary matching
regex: Enable regex pattern matching
max_line_length: Optional. Limit the length of lines when context_lines is used
"""
results: Dict[str, List[Tuple[int, str]]] = {}

Expand Down Expand Up @@ -94,15 +96,20 @@ def search(
with open(file_path, 'r', encoding='utf-8', errors='ignore') as f:
for line_num, line in enumerate(f, 1):
if search_regex.search(line):
content = line.rstrip('\n')
# Truncate content if it exceeds max_line_length
if max_line_length and len(content) > max_line_length:
content = content[:max_line_length] + '... (truncated)'

if rel_path not in results:
results[rel_path] = []
# Strip newline for consistent output
results[rel_path].append((line_num, line.rstrip('\n')))
results[rel_path].append((line_num, content))
except (UnicodeDecodeError, PermissionError, OSError):
# Ignore files that can't be opened or read due to encoding/permission issues
continue
except Exception:
# Ignore any other unexpected exceptions to maintain robustness
continue

return results
return results
8 changes: 5 additions & 3 deletions src/code_index_mcp/search/grep.py
Original file line number Diff line number Diff line change
Expand Up @@ -32,7 +32,8 @@ def search(
context_lines: int = 0,
file_pattern: Optional[str] = None,
fuzzy: bool = False,
regex: bool = False
regex: bool = False,
max_line_length: Optional[int] = None
) -> Dict[str, List[Tuple[int, str]]]:
"""
Execute a search using standard grep.
Expand All @@ -45,6 +46,7 @@ def search(
file_pattern: File pattern to filter
fuzzy: Enable word boundary matching
regex: Enable regex pattern matching
max_line_length: Optional. Limit the length of lines when context_lines is used
"""
# -r: recursive, -n: line number
cmd = ['grep', '-r', '-n']
Expand Down Expand Up @@ -102,9 +104,9 @@ def search(
if process.returncode > 1:
raise RuntimeError(f"grep failed with exit code {process.returncode}: {process.stderr}")

return parse_search_output(process.stdout, base_path)
return parse_search_output(process.stdout, base_path, max_line_length)

except FileNotFoundError:
raise RuntimeError("'grep' not found. Please install it and ensure it's in your PATH.")
except Exception as e:
raise RuntimeError(f"An error occurred while running grep: {e}")
raise RuntimeError(f"An error occurred while running grep: {e}")
8 changes: 5 additions & 3 deletions src/code_index_mcp/search/ripgrep.py
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,8 @@ def search(
context_lines: int = 0,
file_pattern: Optional[str] = None,
fuzzy: bool = False,
regex: bool = False
regex: bool = False,
max_line_length: Optional[int] = None
) -> Dict[str, List[Tuple[int, str]]]:
"""
Execute a search using ripgrep.
Expand All @@ -40,6 +41,7 @@ def search(
file_pattern: File pattern to filter
fuzzy: Enable word boundary matching (not true fuzzy search)
regex: Enable regex pattern matching
max_line_length: Optional. Limit the length of lines when context_lines is used
"""
cmd = ['rg', '--line-number', '--no-heading', '--color=never', '--no-ignore']

Expand Down Expand Up @@ -87,10 +89,10 @@ def search(
if process.returncode > 1:
raise RuntimeError(f"ripgrep failed with exit code {process.returncode}: {process.stderr}")

return parse_search_output(process.stdout, base_path)
return parse_search_output(process.stdout, base_path, max_line_length)

except FileNotFoundError:
raise RuntimeError("ripgrep (rg) not found. Please install it and ensure it's in your PATH.")
except Exception as e:
# Re-raise other potential exceptions like permission errors
raise RuntimeError(f"An error occurred while running ripgrep: {e}")
raise RuntimeError(f"An error occurred while running ripgrep: {e}")
6 changes: 4 additions & 2 deletions src/code_index_mcp/search/ugrep.py
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,8 @@ def search(
context_lines: int = 0,
file_pattern: Optional[str] = None,
fuzzy: bool = False,
regex: bool = False
regex: bool = False,
max_line_length: Optional[int] = None
) -> Dict[str, List[Tuple[int, str]]]:
"""
Execute a search using the 'ug' command-line tool.
Expand All @@ -40,6 +41,7 @@ def search(
file_pattern: File pattern to filter
fuzzy: Enable true fuzzy search (ugrep native support)
regex: Enable regex pattern matching
max_line_length: Optional. Limit the length of lines when context_lines is used
"""
if not self.is_available():
return {"error": "ugrep (ug) command not found."}
Expand Down Expand Up @@ -89,7 +91,7 @@ def search(
error_output = process.stderr.strip()
return {"error": f"ugrep execution failed with code {process.returncode}", "details": error_output}

return parse_search_output(process.stdout, base_path)
return parse_search_output(process.stdout, base_path, max_line_length)

except FileNotFoundError:
return {"error": "ugrep (ug) command not found. Please ensure it's installed and in your PATH."}
Expand Down
7 changes: 5 additions & 2 deletions src/code_index_mcp/server.py
Original file line number Diff line number Diff line change
Expand Up @@ -122,7 +122,8 @@ def search_code_advanced(
context_lines: int = 0,
file_pattern: str = None,
fuzzy: bool = False,
regex: bool = None
regex: bool = None,
max_line_length: int = 200
) -> Dict[str, Any]:
"""
Search for a code pattern in the project using an advanced, fast tool.
Expand All @@ -136,6 +137,7 @@ def search_code_advanced(
context_lines: Number of lines to show before and after the match.
file_pattern: A glob pattern to filter files to search in
(e.g., "*.py", "*.js", "test_*.py").
max_line_length: Optional. Default 200. Limits the length of lines when context_lines is used.
All search tools now handle glob patterns consistently:
- ugrep: Uses glob patterns (*.py, *.{js,ts})
- ripgrep: Uses glob patterns (*.py, *.{js,ts})
Expand Down Expand Up @@ -164,7 +166,8 @@ def search_code_advanced(
context_lines=context_lines,
file_pattern=file_pattern,
fuzzy=fuzzy,
regex=regex
regex=regex,
max_line_length=max_line_length
)

@mcp.tool()
Expand Down
9 changes: 6 additions & 3 deletions src/code_index_mcp/services/search_service.py
Original file line number Diff line number Diff line change
Expand Up @@ -31,7 +31,8 @@ def search_code( # pylint: disable=too-many-arguments
context_lines: int = 0,
file_pattern: Optional[str] = None,
fuzzy: bool = False,
regex: Optional[bool] = None
regex: Optional[bool] = None,
max_line_length: Optional[int] = 200
) -> Dict[str, Any]:
"""
Search for code patterns in the project.
Expand All @@ -45,6 +46,7 @@ def search_code( # pylint: disable=too-many-arguments
file_pattern: Glob pattern to filter files
fuzzy: Whether to enable fuzzy matching
regex: Regex mode - True/False to force, None for auto-detection
max_line_length: Optional. Default 200. Limits the length of lines when context_lines is used.

Returns:
Dictionary with search results or error information
Expand Down Expand Up @@ -89,7 +91,8 @@ def search_code( # pylint: disable=too-many-arguments
context_lines=context_lines,
file_pattern=file_pattern,
fuzzy=fuzzy,
regex=regex
regex=regex,
max_line_length=max_line_length
)
return ResponseFormatter.search_results_response(results)
except Exception as e:
Expand Down Expand Up @@ -141,4 +144,4 @@ def get_search_capabilities(self) -> Dict[str, Any]:
"supports_file_patterns": True
}

return capabilities
return capabilities