diff --git a/README.md b/README.md
index e737580..d8ad972 100644
--- a/README.md
+++ b/README.md
@@ -1,203 +1,273 @@
-# Code Index MCP
-
-
-
-[](https://modelcontextprotocol.io)
-[](https://www.python.org/)
-[](LICENSE)
-
-A Model Context Protocol server for code indexing, searching, and analysis.
-
-
-
-
-
-
-
-## What is Code Index MCP?
-
-Code Index MCP is a specialized MCP server that provides intelligent code indexing and analysis capabilities. It enables Large Language Models to interact with your code repositories, offering real-time insights and navigation through complex codebases.
-
-This server integrates with the [Model Context Protocol](https://modelcontextprotocol.io) (MCP), a standardized way for AI models to interact with external tools and data sources.
-
-## Key Features
-
-- **Project Indexing**: Recursively scans directories to build a searchable index of code files
-- **Advanced Search**: Intelligent search with automatic detection of ugrep, ripgrep, ag, or grep for enhanced performance
-- **Fuzzy Search**: Native fuzzy matching with ugrep, or safe fuzzy patterns for other tools
-- **File Analysis**: Get detailed insights about file structure, imports, and complexity
-- **Smart Filtering**: Automatically ignores build directories, dependencies, and non-code files
-- **Persistent Storage**: Caches indexes for improved performance across sessions
-- **Lazy Loading**: Search tools are detected only when needed for optimal startup performance
-
-## Supported File Types
-
-The server supports multiple programming languages and file extensions including:
-
-- Python (.py)
-- JavaScript/TypeScript (.js, .ts, .jsx, .tsx, .mjs, .cjs)
-- Frontend Frameworks (.vue, .svelte, .astro)
-- Java (.java)
-- C/C++ (.c, .cpp, .h, .hpp)
-- C# (.cs)
-- Go (.go)
-- Ruby (.rb)
-- PHP (.php)
-- Swift (.swift)
-- Kotlin (.kt)
-- Rust (.rs)
-- Scala (.scala)
-- Shell scripts (.sh, .bash)
-- Zig (.zig)
-- Web files (.html, .css, .scss, .less, .sass, .stylus, .styl)
-- Template engines (.hbs, .handlebars, .ejs, .pug)
-- **Database & SQL**:
- - SQL files (.sql, .ddl, .dml)
- - Database-specific (.mysql, .postgresql, .psql, .sqlite, .mssql, .oracle, .ora, .db2)
- - Database objects (.proc, .procedure, .func, .function, .view, .trigger, .index)
- - Migration & tools (.migration, .seed, .fixture, .schema, .liquibase, .flyway)
- - NoSQL & modern (.cql, .cypher, .sparql, .gql)
-- Documentation/Config (.md, .mdx, .json, .xml, .yml, .yaml)
-
-## Installation
-
-### Prerequisites
-
-- Python 3.8 or higher
-- [uv](https://github.com/astral-sh/uv) package manager (recommended)
-
-### Using uvx (recommended)
-
-The easiest way to install and use code-index-mcp is with uvx:
-
-```bash
-uvx code-index-mcp
-```
-
-### Using pip
-
-Alternatively, you can install via pip:
-
-```bash
-pip install code-index-mcp
-```
-
-After installation, you can run it as a module:
-
-```bash
-python -m code_index_mcp
-```
-
-## Integration with Claude Desktop
-
-Add this to your Claude settings (`~/Library/Application Support/Claude/claude_desktop_config.json`):
-
-```json
-{
- "mcpServers": {
- "code-index": {
- "command": "uvx",
- "args": [
- "code-index-mcp"
- ]
- }
- }
-}
-```
-
-After adding the configuration, restart Claude Desktop and the Code Index MCP tools will be available.
-
-## Available Tools
-
-### Core Tools
-
-- **set_project_path**: Sets the base project path for indexing.
-- **search_code**: Basic search for code matches within the indexed files.
-- **search_code_advanced**: Enhanced search using external tools (ugrep/ripgrep/ag/grep) with fuzzy matching support.
-- **find_files**: Finds files in the project matching a given pattern.
-- **get_file_summary**: Gets a summary of a specific file, including line count, functions, imports, etc.
-- **refresh_index**: Refreshes the project index.
-- **get_settings_info**: Gets information about the project settings.
-
-### Utility Tools
-
-- **create_temp_directory**: Creates the temporary directory used for storing index data.
-- **check_temp_directory**: Checks the temporary directory used for storing index data.
-- **clear_settings**: Clears all settings and cached data.
-
-## Example Usage with Claude
-
-Here are some examples of how to use Code Index MCP with Claude:
-
-### Setting a Project Path
-
-```
-Please set the project path to C:\Users\username\projects\my-python-project
-```
-
-### Searching for Code Patterns
-
-```
-Search the code for all occurrences of "def process_data" in Python files
-```
-
-### Advanced Search with Fuzzy Matching
-
-```
-Use advanced search to find "process" with fuzzy matching enabled
-```
-
-### Getting a File Summary
-
-```
-Give me a summary of the main.py file in the project
-```
-
-### Finding All Files of a Certain Type
-
-```
-Find all JavaScript files in the project
-```
-
-## Development
-
-### Building from Source
-
-1. Clone the repository:
-
-```bash
-git clone https://github.com/username/code-index-mcp.git
-cd code-index-mcp
-```
-
-2. Install dependencies:
-
-```bash
-uv sync
-```
-
-3. Run the server locally:
-
-```bash
-uv run code_index_mcp
-```
-
-## Debugging
-
-You can use the MCP inspector to debug the server:
-
-```bash
-npx @modelcontextprotocol/inspector uvx code-index-mcp
-```
-
-## License
-
-[MIT License](LICENSE)
-
-## Contributing
-
-Contributions are welcome! Please feel free to submit a Pull Request.
-
-## Languages
-
-- [繁體中文](README_zh.md)
+# Code Index MCP
+
+
+
+[](https://modelcontextprotocol.io)
+[](https://www.python.org/)
+[](LICENSE)
+
+A Model Context Protocol server for code indexing, searching, and analysis.
+
+
+
+
+
+
+
+## What is Code Index MCP?
+
+Code Index MCP is a specialized MCP server that provides intelligent code indexing and analysis capabilities. It enables Large Language Models to interact with your code repositories, offering real-time insights and navigation through complex codebases.
+
+This server integrates with the [Model Context Protocol](https://modelcontextprotocol.io) (MCP), a standardized way for AI models to interact with external tools and data sources.
+
+## Key Features
+
+- **Project Indexing**: Recursively scans directories to build a searchable index of code files
+- **Advanced Search**: Intelligent search with automatic detection of ugrep, ripgrep, ag, or grep for enhanced performance
+- **Fuzzy Search**: Native fuzzy matching with ugrep, or safe fuzzy patterns for other tools
+- **File Analysis**: Get detailed insights about file structure, imports, and complexity
+- **Smart Filtering**: Automatically ignores build directories, dependencies, and non-code files
+- **Persistent Storage**: Caches indexes for improved performance across sessions
+- **Lazy Loading**: Search tools are detected only when needed for optimal startup performance
+
+## Supported File Types
+
+The server supports multiple programming languages and file extensions including:
+
+- Python (.py)
+- JavaScript/TypeScript (.js, .ts, .jsx, .tsx, .mjs, .cjs)
+- Frontend Frameworks (.vue, .svelte, .astro)
+- Java (.java)
+- C/C++ (.c, .cpp, .h, .hpp)
+- C# (.cs)
+- Go (.go)
+- Ruby (.rb)
+- PHP (.php)
+- Swift (.swift)
+- Kotlin (.kt)
+- Rust (.rs)
+- Scala (.scala)
+- Shell scripts (.sh, .bash)
+- Zig (.zig)
+- Web files (.html, .css, .scss, .less, .sass, .stylus, .styl)
+- Template engines (.hbs, .handlebars, .ejs, .pug)
+- **Database & SQL**:
+ - SQL files (.sql, .ddl, .dml)
+ - Database-specific (.mysql, .postgresql, .psql, .sqlite, .mssql, .oracle, .ora, .db2)
+ - Database objects (.proc, .procedure, .func, .function, .view, .trigger, .index)
+ - Migration & tools (.migration, .seed, .fixture, .schema, .liquibase, .flyway)
+ - NoSQL & modern (.cql, .cypher, .sparql, .gql)
+- Documentation/Config (.md, .mdx, .json, .xml, .yml, .yaml)
+
+## Setup and Integration
+
+There are several ways to set up and use Code Index MCP, depending on your needs.
+
+### For General Use with Host Applications (Recommended)
+
+This is the easiest and most common way to use the server. It's designed for users who want to use Code Index MCP within an AI application like Claude Desktop.
+
+1. **Prerequisite**: Make sure you have Python 3.8+ and [uv](https://github.com/astral-sh/uv) installed.
+
+2. **Configure the Host App**: Add the following to your host application's MCP configuration file.
+
+ *Claude Desktop* -> `claude_desktop_config.json`
+
+ *Claude Code* -> `~/.claude.json`. There is one `mcpServers` for each project and one global
+
+ ```json
+ {
+ "mcpServers": {
+ "code-index": {
+ "command": "uvx",
+ "args": [
+ "code-index-mcp"
+ ]
+ }
+ }
+ }
+ ```
+
+4. **Restart the Host App**: After adding the configuration, restart the application. The `uvx` command will automatically handle the installation and execution of the `code-index-mcp` server in the background.
+
+### For Local Development
+
+If you want to contribute to the development of this project, follow these steps:
+
+1. **Clone the repository**:
+ ```bash
+ git clone https://github.com/johnhuang316/code-index-mcp.git
+ cd code-index-mcp
+ ```
+
+2. **Install dependencies** using `uv`:
+ ```bash
+ uv sync
+ ```
+
+3. **Configure Your Host App for Local Development**: To make your host application (e.g., Claude Desktop) use your local source code, update its configuration file to execute the server via `uv run`. This ensures any changes you make to the code are reflected immediately when the host app starts the server.
+
+ ```json
+ {
+ "mcpServers": {
+ "code-index": {
+ "command": "uv",
+ "args": [
+ "run",
+ "code_index_mcp"
+ ]
+ }
+ }
+ }
+ ```
+
+4. **Debug with the MCP Inspector**: To debug your local server, you also need to tell the inspector to use `uv run`.
+ ```bash
+ npx @modelcontextprotocol/inspector uv run code_index_mcp
+ ```
+
+### Manual Installation via pip (Alternative)
+
+If you prefer to manage your Python packages manually with `pip`, you can install the server directly.
+
+1. **Install the package**:
+ ```bash
+ pip install code-index-mcp
+ ```
+
+2. **Configure the Host App**: You will need to manually update your host application's MCP configuration to point to the installed script. Replace `"command": "uvx"` with `"command": "code-index-mcp"`.
+
+ ```json
+ {
+ "mcpServers": {
+ "code-index": {
+ "command": "code-index-mcp",
+ "args": []
+ }
+ }
+ }
+ ```
+
+## Available Tools
+
+### Core Tools
+
+- **set_project_path**: Sets the base project path for indexing.
+- **search_code**: Enhanced search using external tools (ugrep/ripgrep/ag/grep) with fuzzy matching support.
+- **find_files**: Finds files in the project matching a given pattern.
+- **get_file_summary**: Gets a summary of a specific file, including line count, functions, imports, etc.
+- **refresh_index**: Refreshes the project index.
+- **get_settings_info**: Gets information about the project settings.
+
+### Utility Tools
+
+- **create_temp_directory**: Creates the temporary directory used for storing index data.
+- **check_temp_directory**: Checks the temporary directory used for storing index data.
+- **clear_settings**: Clears all settings and cached data.
+- **refresh_search_tools**: Manually re-detect available command-line search tools (e.g., ripgrep).
+
+## Common Workflows and Examples
+
+Here’s a typical workflow for using Code Index MCP with an AI assistant like Claude.
+
+### 1. Set Project Path & Initial Indexing
+
+This is the first and most important step. When you set the project path, the server automatically creates a file index for the first time or loads a previously cached one.
+
+**Example Prompt:**
+```
+Please set the project path to C:\Users\username\projects\my-react-app
+```
+
+### 2. Refresh the Index (When Needed)
+
+If you make significant changes to your project files after the initial setup, you can manually refresh the index to ensure all tools are working with the latest information.
+
+**Example Prompt:**
+```
+I've just added a few new components, please refresh the project index.
+```
+*(The assistant would use the `refresh_index` tool)*
+
+### 3. Explore the Project Structure
+
+Once the index is ready, you can find files using patterns (globs) to understand the codebase and locate relevant files.
+
+**Example Prompt:**
+```
+Find all TypeScript component files in the 'src/components' directory.
+```
+*(The assistant would use the `find_files` tool with a pattern like `src/components/**/*.tsx`)*
+
+### 4. Analyze a Specific File
+
+Before diving into the full content of a file, you can get a quick summary of its structure, including functions, classes, and imports.
+
+**Example Prompt:**
+```
+Can you give me a summary of the 'src/api/userService.ts' file?
+```
+*(The assistant would use the `get_file_summary` tool)*
+
+### 5. Search for Code
+
+With an up-to-date index, you can search for code snippets, function names, or any text pattern to find where specific logic is implemented.
+
+**Example: Simple Search**
+```
+Search for all occurrences of the "processData" function.
+```
+
+**Example: Search with Fuzzy Matching**
+```
+I'm looking for a function related to user authentication, it might be named 'authUser', 'authenticateUser', or something similar. Can you do a fuzzy search for 'authUser'?
+```
+
+**Example: Search within Specific Files**
+```
+Search for the string "API_ENDPOINT" only in Python files.
+```
+*(The assistant would use the `search_code` tool with the `file_pattern` parameter set to `*.py`)*
+
+## Development
+
+### Building from Source
+
+1. Clone the repository:
+
+```bash
+git clone https://github.com/username/code-index-mcp.git
+cd code-index-mcp
+```
+
+2. Install dependencies:
+
+```bash
+uv sync
+```
+
+3. Run the server locally:
+
+```bash
+uv run code_index_mcp
+```
+
+## Debugging
+
+You can use the MCP inspector to debug the server:
+
+```bash
+npx @modelcontextprotocol/inspector uvx code-index-mcp
+```
+
+## License
+
+[MIT License](LICENSE)
+
+## Contributing
+
+Contributions are welcome! Please feel free to submit a Pull Request.
+
+## Languages
+
+- [繁體中文](README_zh.md)
diff --git a/README_zh.md b/README_zh.md
index 98b93ab..8b75193 100644
--- a/README_zh.md
+++ b/README_zh.md
@@ -55,61 +55,97 @@
- NoSQL 與現代資料庫 (.cql, .cypher, .sparql, .gql)
- 文件/配置 (.md, .mdx, .json, .xml, .yml, .yaml)
-## 安裝
+## 設定與整合
-### 先決條件
+您可以根據不同的需求,透過以下幾種方式來設定和使用 Code Index MCP。
-- Python 3.8 或更高版本
-- [uv](https://github.com/astral-sh/uv) 套件管理器(建議)
+### 一般用途:與宿主應用整合(建議)
-### 使用 uvx(建議)
+這是最簡單也最常見的使用方式,專為希望在 AI 應用程式(如 Claude Desktop)中使用 Code Index MCP 的使用者設計。
-使用 uvx 安裝和使用 code-index-mcp 是最簡單的方法:
+1. **先決條件**:請確保您已安裝 Python 3.8+ 和 [uv](https://github.com/astral-sh/uv)。
-```bash
-uvx code-index-mcp
-```
+2. **設定宿主應用**:將以下設定新增到您宿主應用的 MCP 設定檔中(例如,Claude Desktop 的設定檔是 `claude_desktop_config.json`):
-### 使用 pip
+ ```json
+ {
+ "mcpServers": {
+ "code-index": {
+ "command": "uvx",
+ "args": [
+ "code-index-mcp"
+ ]
+ }
+ }
+ }
+ ```
+
+3. **重新啟動宿主應用**:新增設定後,請重新啟動您的應用程式。`uvx` 命令會在背景自動處理 `code-index-mcp` 伺服器的安裝與執行。
+
+### 本地開發
+
+如果您想為這個專案的開發做出貢獻,請遵循以下步驟:
+
+1. **複製儲存庫**:
+ ```bash
+ git clone https://github.com/johnhuang316/code-index-mcp.git
+ cd code-index-mcp
+ ```
+
+2. **安裝相依套件**:使用 `uv` 安裝所需套件。
+ ```bash
+ uv sync
+ ```
+
+3. **設定您的宿主應用以進行本地開發**:為了讓您的宿主應用程式(例如 Claude Desktop)使用您本地的原始碼,請更新其設定檔,讓它透過 `uv run` 來執行伺服器。這能確保您對程式碼所做的任何變更,在宿主應用啟動伺服器時能立即生效。
+
+ ```json
+ {
+ "mcpServers": {
+ "code-index": {
+ "command": "uv",
+ "args": [
+ "run",
+ "code_index_mcp"
+ ]
+ }
+ }
+ }
+ ```
-或者,您可以透過 pip 安裝:
+4. **使用 MCP Inspector 進行偵錯**:要對您的本地伺服器進行偵錯,您同樣需要告訴 Inspector 使用 `uv run`。
+ ```bash
+ npx @modelcontextprotocol/inspector uv run code_index_mcp
+ ```
-```bash
-pip install code-index-mcp
-```
+### 手動安裝:使用 pip(替代方案)
-安裝後,您可以以模組方式執行:
+如果您習慣使用 `pip` 來手動管理您的 Python 套件,可以透過以下方式安裝。
-```bash
-python -m code_index_mcp
-```
-
-## 與 Claude Desktop 整合
+1. **安裝套件**:
+ ```bash
+ pip install code-index-mcp
+ ```
-將以下內容新增到您的 Claude 設定(`~/Library/Application Support/Claude/claude_desktop_config.json`):
+2. **設定宿主應用**:您需要手動更新宿主應用的 MCP 設定,將命令從 `"command": "uvx"` 修改為 `"command": "code-index-mcp"`。
-```json
-{
- "mcpServers": {
- "code-index": {
- "command": "uvx",
- "args": [
- "code-index-mcp"
- ]
+ ```json
+ {
+ "mcpServers": {
+ "code-index": {
+ "command": "code-index-mcp",
+ "args": []
+ }
+ }
}
- }
-}
-```
-
-新增設定後,重新啟動 Claude Desktop,程式碼索引 MCP 工具將可使用。
+ ```
## 可用工具
### 核心工具
- **set_project_path**:設定索引的基本專案路徑。
-- **search_code**:在已索引檔案中進行基本程式碼搜尋。
-- **search_code_advanced**:使用外部工具 (ugrep/ripgrep/ag/grep) 的增強搜尋,支援模糊匹配。
+- **search_code**:使用外部工具 (ugrep/ripgrep/ag/grep) 的增強搜尋,支援模糊匹配。
- **find_files**:尋找專案中符合給定模式的檔案。
- **get_file_summary**:取得特定檔案的摘要,包括行數、函式、匯入等。
- **refresh_index**:重新整理專案索引。
@@ -120,40 +156,70 @@ python -m code_index_mcp
- **create_temp_directory**:建立用於儲存索引資料的臨時目錄。
- **check_temp_directory**:檢查用於儲存索引資料的臨時目錄。
- **clear_settings**:清除所有設定和快取資料。
+- **refresh_search_tools**:手動重新偵測可用的命令列搜尋工具(例如 ripgrep)。
-## Claude 使用範例
+## 常見工作流程與範例
-以下是如何與 Claude 一起使用 Code Index MCP 的一些範例:
+這是一個典型的使用場景,展示如何透過像 Claude 這樣的 AI 助理來使用 Code Index MCP。
-### 設定專案路徑
+### 1. 設定專案路徑與首次索引
+這是第一步,也是最重要的一步。當您設定專案路徑時,伺服器會自動進行首次的檔案索引,或載入先前快取的索引。
+
+**範例提示:**
```
-請將專案路徑設定為 C:\Users\username\projects\my-python-project
+請將專案路徑設定為 C:\Users\username\projects\my-react-app
```
-### 搜尋程式碼模式
+### 2. 更新索引(需要時)
+
+如果您在初次設定後對專案檔案做了較大的變更,可以手動重新整理索引,以確保所有工具都能基於最新的資訊運作。
+**範例提示:**
```
-在 Python 檔案中搜尋所有出現 "def process_data" 的地方
+我剛新增了幾個新的元件,請幫我重新整理專案索引。
```
+*(AI 助理會使用 `refresh_index` 工具)*
+
+### 3. 探索專案結構
-### 進階搜尋與模糊匹配
+索引準備就緒後,您可以使用模式(glob)來尋找檔案,以了解程式碼庫的結構並找到相關檔案。
+**範例提示:**
```
-使用進階搜尋並啟用模糊匹配來尋找 "process"
+尋找 'src/components' 目錄中所有的 TypeScript 元件檔案。
```
+*(AI 助理會使用 `find_files` 工具,並搭配像 `src/components/**/*.tsx` 這樣的模式)*
-### 取得檔案摘要
+### 4. 分析特定檔案
+在深入研究一個檔案的完整內容之前,您可以先取得該檔案結構的快速摘要,包括函式、類別和匯入的模組。
+
+**範例提示:**
```
-給我專案中 main.py 檔案的摘要
+可以給我 'src/api/userService.ts' 這個檔案的摘要嗎?
```
+*(AI 助理會使用 `get_file_summary` 工具)*
+
+### 5. 搜尋程式碼
+
+有了最新的索引,您就可以搜尋程式碼片段、函式名稱或任何文字模式,以找到特定邏輯的實作位置。
-### 尋找特定類型的所有檔案
+**範例:簡單搜尋**
+```
+搜尋 "processData" 函式所有出現過的地方。
+```
+
+**範例:模糊匹配搜尋**
+```
+我正在尋找一個與使用者驗證相關的函式,它可能叫做 'authUser'、'authenticateUser' 或類似的名稱。你可以對 'authUser' 進行模糊搜尋嗎?
+```
+**範例:在特定檔案中搜尋**
```
-尋找專案中所有的 JavaScript 檔案
+只在 Python 檔案中搜尋字串 "API_ENDPOINT"。
```
+*(AI 助理會使用 `search_code` 工具,並將 `file_pattern` 參數設定為 `*.py`)*
## 開發
diff --git a/pyproject.toml b/pyproject.toml
index 94889d4..eac8bb1 100644
--- a/pyproject.toml
+++ b/pyproject.toml
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
[project]
name = "code-index-mcp"
-version = "0.3.0"
+version = "0.3.1"
description = "Code indexing and analysis tools for LLMs using MCP"
readme = "README.md"
requires-python = ">=3.10"
diff --git a/src/code_index_mcp/search/ag.py b/src/code_index_mcp/search/ag.py
index f809fa2..82cedb6 100644
--- a/src/code_index_mcp/search/ag.py
+++ b/src/code_index_mcp/search/ag.py
@@ -61,7 +61,7 @@ def search(
# Add -- to treat pattern as a literal argument, preventing injection
cmd.append('--')
cmd.append(search_pattern)
- cmd.append(base_path)
+ cmd.append('.') # Use current directory since we set cwd=base_path
try:
# ag exits with 1 if no matches are found, which is not an error.
@@ -72,7 +72,8 @@ def search(
text=True,
encoding='utf-8',
errors='replace',
- check=False # Do not raise CalledProcessError on non-zero exit
+ check=False, # Do not raise CalledProcessError on non-zero exit
+ cwd=base_path # Set working directory to project base path for proper pattern resolution
)
# We don't check returncode > 1 because ag's exit code behavior
# is less standardized than rg/ug. 0 for match, 1 for no match.
@@ -86,4 +87,4 @@ def search(
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}")
\ No newline at end of file
+ raise RuntimeError(f"An error occurred while running ag: {e}")
diff --git a/src/code_index_mcp/search/basic.py b/src/code_index_mcp/search/basic.py
index c247c2f..76d3607 100644
--- a/src/code_index_mcp/search/basic.py
+++ b/src/code_index_mcp/search/basic.py
@@ -3,6 +3,7 @@
"""
import os
import re
+import fnmatch
from typing import Dict, List, Optional, Tuple
from .base import SearchStrategy, create_safe_fuzzy_pattern
@@ -25,6 +26,18 @@ def is_available(self) -> bool:
"""This basic strategy is always available."""
return True
+ def _matches_pattern(self, filename: str, pattern: str) -> bool:
+ """Check if filename matches the glob pattern."""
+ if not pattern:
+ return True
+
+ # Handle simple cases efficiently
+ if pattern.startswith('*') and not any(c in pattern[1:] for c in '*?[]{}'):
+ return filename.endswith(pattern[1:])
+
+ # Use fnmatch for more complex patterns
+ return fnmatch.fnmatch(filename, pattern)
+
def search(
self,
pattern: str,
@@ -53,8 +66,8 @@ def search(
for root, _, files in os.walk(base_path):
for file in files:
- # Basic file pattern matching (not full glob support)
- if file_pattern and not file.endswith(file_pattern.replace('*', '')):
+ # Improved file pattern matching with glob support
+ if file_pattern and not self._matches_pattern(file, file_pattern):
continue
file_path = os.path.join(root, file)
@@ -67,9 +80,12 @@ def search(
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, line.rstrip('\n')))
+ except (UnicodeDecodeError, PermissionError, OSError):
+ # Ignore files that can't be opened or read due to encoding/permission issues
+ continue
except Exception:
- # Ignore files that can't be opened or read
+ # Ignore any other unexpected exceptions to maintain robustness
continue
return results
\ No newline at end of file
diff --git a/src/code_index_mcp/search/grep.py b/src/code_index_mcp/search/grep.py
index e8c9609..5e9897d 100644
--- a/src/code_index_mcp/search/grep.py
+++ b/src/code_index_mcp/search/grep.py
@@ -65,7 +65,7 @@ def search(
# Add -- to treat pattern as a literal argument, preventing injection
cmd.append('--')
cmd.append(search_pattern)
- cmd.append(base_path)
+ cmd.append('.') # Use current directory since we set cwd=base_path
try:
# grep exits with 1 if no matches are found, which is not an error.
@@ -76,7 +76,8 @@ def search(
text=True,
encoding='utf-8',
errors='replace',
- check=False
+ check=False,
+ cwd=base_path # Set working directory to project base path for proper pattern resolution
)
if process.returncode > 1:
@@ -87,4 +88,4 @@ def search(
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}")
\ No newline at end of file
+ raise RuntimeError(f"An error occurred while running grep: {e}")
diff --git a/src/code_index_mcp/search/ripgrep.py b/src/code_index_mcp/search/ripgrep.py
index 8cc6946..3f7aad7 100644
--- a/src/code_index_mcp/search/ripgrep.py
+++ b/src/code_index_mcp/search/ripgrep.py
@@ -57,7 +57,7 @@ def search(
# Add -- to treat pattern as a literal argument, preventing injection
cmd.append('--')
cmd.append(search_pattern)
- cmd.append(base_path)
+ cmd.append('.') # Use current directory since we set cwd=base_path
try:
# ripgrep exits with 1 if no matches are found, which is not an error.
@@ -68,7 +68,8 @@ def search(
text=True,
encoding='utf-8',
errors='replace',
- check=False # Do not raise CalledProcessError on non-zero exit
+ check=False, # Do not raise CalledProcessError on non-zero exit
+ cwd=base_path # Set working directory to project base path for proper glob resolution
)
if process.returncode > 1:
raise RuntimeError(f"ripgrep failed with exit code {process.returncode}: {process.stderr}")
@@ -79,4 +80,4 @@ def search(
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}")
\ No newline at end of file
+ raise RuntimeError(f"An error occurred while running ripgrep: {e}")
diff --git a/src/code_index_mcp/search/ugrep.py b/src/code_index_mcp/search/ugrep.py
index 4222602..5e64302 100644
--- a/src/code_index_mcp/search/ugrep.py
+++ b/src/code_index_mcp/search/ugrep.py
@@ -48,12 +48,12 @@ def search(
cmd.extend(['-A', str(context_lines), '-B', str(context_lines)])
if file_pattern:
- cmd.extend(['--include', file_pattern]) # Correct parameter for file patterns
+ cmd.extend(['-g', file_pattern]) # Correct parameter for file patterns
# Add '--' to treat pattern as a literal argument, preventing injection
cmd.append('--')
cmd.append(pattern)
- cmd.append(base_path)
+ cmd.append('.') # Use current directory since we set cwd=base_path
try:
process = subprocess.run(
@@ -62,7 +62,8 @@ def search(
text=True,
encoding='utf-8',
errors='ignore', # Ignore decoding errors for binary-like content
- check=False # Do not raise exception on non-zero exit codes
+ check=False, # Do not raise exception on non-zero exit codes
+ cwd=base_path # Set working directory to project base path for proper pattern resolution
)
# ugrep exits with 1 if no matches are found, which is not an error for us.
diff --git a/src/code_index_mcp/server.py b/src/code_index_mcp/server.py
index 56c9e1c..fd5e109 100644
--- a/src/code_index_mcp/server.py
+++ b/src/code_index_mcp/server.py
@@ -1,778 +1,795 @@
-"""
-Code Index MCP Server
-
-This MCP server allows LLMs to index, search, and analyze code from a project directory.
-It provides tools for file discovery, content retrieval, and code analysis.
-"""
-from contextlib import asynccontextmanager
-from dataclasses import dataclass
-from typing import AsyncIterator, Dict, List, Optional, Tuple, Any
-import os
-import pathlib
-import json
-import fnmatch
-import sys
-import tempfile
-import subprocess
-from mcp.server.fastmcp import FastMCP, Context, Image
-from mcp import types
-
-# Import the ProjectSettings class and constants - using relative import
-from .project_settings import ProjectSettings
-from .constants import SETTINGS_DIR
-
-# Create the MCP server
-mcp = FastMCP("CodeIndexer", dependencies=["pathlib"])
-
-# In-memory references (will be loaded from persistent storage)
-file_index = {}
-code_content_cache = {}
-supported_extensions = [
- '.py', '.js', '.ts', '.jsx', '.tsx', '.java', '.c', '.cpp', '.h', '.hpp',
- '.cs', '.go', '.rb', '.php', '.swift', '.kt', '.rs', '.scala', '.sh',
- '.bash', '.html', '.css', '.scss', '.md', '.json', '.xml', '.yml', '.yaml', '.zig',
- # Frontend frameworks
- '.vue', '.svelte', '.mjs', '.cjs',
- # Style languages
- '.less', '.sass', '.stylus', '.styl',
- # Template engines
- '.hbs', '.handlebars', '.ejs', '.pug',
- # Modern frontend
- '.astro', '.mdx',
- # Database and SQL
- '.sql', '.ddl', '.dml', '.mysql', '.postgresql', '.psql', '.sqlite',
- '.mssql', '.oracle', '.ora', '.db2',
- # Database objects
- '.proc', '.procedure', '.func', '.function', '.view', '.trigger', '.index',
- # Database frameworks and tools
- '.migration', '.seed', '.fixture', '.schema',
- # NoSQL and modern databases
- '.cql', '.cypher', '.sparql', '.gql',
- # Database migration tools
- '.liquibase', '.flyway'
-]
-
-@dataclass
-class CodeIndexerContext:
- """Context for the Code Indexer MCP server."""
- base_path: str
- settings: ProjectSettings
- file_count: int = 0
-
-@asynccontextmanager
-async def indexer_lifespan(server: FastMCP) -> AsyncIterator[CodeIndexerContext]:
- """Manage the lifecycle of the Code Indexer MCP server."""
- # Don't set a default path, user must explicitly set project path
- base_path = "" # Empty string to indicate no path is set
-
- print("Initializing Code Indexer MCP server...")
-
- # Initialize settings manager with skip_load=True to skip loading files
- settings = ProjectSettings(base_path, skip_load=True)
-
- # Initialize context
- context = CodeIndexerContext(
- base_path=base_path,
- settings=settings
- )
-
- # Initialize global variables
- global file_index, code_content_cache
-
- try:
- print("Server ready. Waiting for user to set project path...")
- # Provide context to the server
- yield context
- finally:
- # Only save index and cache if project path has been set
- if context.base_path and file_index:
- print(f"Saving index for project: {context.base_path}")
- settings.save_index(file_index)
-
- if context.base_path and code_content_cache:
- print(f"Saving cache for project: {context.base_path}")
- settings.save_cache(code_content_cache)
-
-# Initialize the server with our lifespan manager
-mcp = FastMCP("CodeIndexer", lifespan=indexer_lifespan)
-
-# ----- RESOURCES -----
-
-@mcp.resource("config://code-indexer")
-def get_config() -> str:
- """Get the current configuration of the Code Indexer."""
- ctx = mcp.get_context()
-
- # Get the base path from context
- base_path = ctx.request_context.lifespan_context.base_path
-
- # Check if base_path is set
- if not base_path:
- return json.dumps({
- "status": "not_configured",
- "message": "Project path not set. Please use set_project_path to set a project directory first.",
- "supported_extensions": supported_extensions
- }, indent=2)
-
- # Get file count
- file_count = ctx.request_context.lifespan_context.file_count
-
- # Get settings stats
- settings = ctx.request_context.lifespan_context.settings
- settings_stats = settings.get_stats()
-
- config = {
- "base_path": base_path,
- "supported_extensions": supported_extensions,
- "file_count": file_count,
- "settings_directory": settings.settings_path,
- "settings_stats": settings_stats
- }
-
- return json.dumps(config, indent=2)
-
-@mcp.resource("files://{file_path}")
-def get_file_content(file_path: str) -> str:
- """Get the content of a specific file."""
- ctx = mcp.get_context()
-
- # Get the base path from context
- base_path = ctx.request_context.lifespan_context.base_path
-
- # Check if base_path is set
- if not base_path:
- return "Error: Project path not set. Please use set_project_path to set a project directory first."
-
- # Handle absolute paths (especially Windows paths starting with drive letters)
- if os.path.isabs(file_path) or (len(file_path) > 1 and file_path[1] == ':'):
- # Absolute paths are not allowed via this endpoint
- return f"Error: Absolute file paths like '{file_path}' are not allowed. Please use paths relative to the project root."
-
- # Normalize the file path
- norm_path = os.path.normpath(file_path)
-
- # Check for path traversal attempts
- if "..\\" in norm_path or "../" in norm_path or norm_path.startswith(".."):
- return f"Error: Invalid file path: {file_path} (directory traversal not allowed)"
-
- # Construct the full path and verify it's within the project bounds
- full_path = os.path.join(base_path, norm_path)
- real_full_path = os.path.realpath(full_path)
- real_base_path = os.path.realpath(base_path)
-
- if not real_full_path.startswith(real_base_path):
- return f"Error: Access denied. File path must be within project directory."
-
- try:
- with open(full_path, 'r', encoding='utf-8') as f:
- content = f.read()
-
- # Cache the content for faster retrieval later
- code_content_cache[norm_path] = content
-
- return content
- except UnicodeDecodeError:
- return f"Error: File {file_path} appears to be a binary file or uses unsupported encoding."
- except Exception as e:
- return f"Error reading file: {e}"
-
-@mcp.resource("structure://project")
-def get_project_structure() -> str:
- """Get the structure of the project as a JSON tree."""
- ctx = mcp.get_context()
-
- # Get the base path from context
- base_path = ctx.request_context.lifespan_context.base_path
-
- # Check if base_path is set
- if not base_path:
- return json.dumps({
- "status": "not_configured",
- "message": "Project path not set. Please use set_project_path to set a project directory first."
- }, indent=2)
-
- # Check if we need to refresh the index
- if not file_index:
- _index_project(base_path)
- # Update file count in context
- ctx.request_context.lifespan_context.file_count = _count_files(file_index)
- # Save updated index
- ctx.request_context.lifespan_context.settings.save_index(file_index)
-
- return json.dumps(file_index, indent=2)
-
-@mcp.resource("settings://stats")
-def get_settings_stats() -> str:
- """Get statistics about the settings directory and files."""
- ctx = mcp.get_context()
-
- # Get settings manager from context
- settings = ctx.request_context.lifespan_context.settings
-
- # Get settings stats
- stats = settings.get_stats()
-
- return json.dumps(stats, indent=2)
-
-# ----- TOOLS -----
-
-@mcp.tool()
-def set_project_path(path: str, ctx: Context) -> str:
- """Set the base project path for indexing."""
- # Validate and normalize path
- try:
- norm_path = os.path.normpath(path)
- abs_path = os.path.abspath(norm_path)
-
- if not os.path.exists(abs_path):
- return f"Error: Path does not exist: {abs_path}"
-
- if not os.path.isdir(abs_path):
- return f"Error: Path is not a directory: {abs_path}"
-
- # Clear existing in-memory index and cache
- global file_index, code_content_cache
- file_index.clear()
- code_content_cache.clear()
-
- # Update the base path in context
- ctx.request_context.lifespan_context.base_path = abs_path
-
- # Create a new settings manager for the new path (don't skip loading files)
- ctx.request_context.lifespan_context.settings = ProjectSettings(abs_path, skip_load=False)
-
- # Print the settings path for debugging
- settings_path = ctx.request_context.lifespan_context.settings.settings_path
- print(f"Project settings path: {settings_path}")
-
- # Try to load existing index and cache
- print(f"Project path set to: {abs_path}")
- print(f"Attempting to load existing index and cache...")
-
- # Try to load index
- loaded_index = ctx.request_context.lifespan_context.settings.load_index()
- if loaded_index:
- print(f"Existing index found and loaded successfully")
- file_index = loaded_index
- file_count = _count_files(file_index)
- ctx.request_context.lifespan_context.file_count = file_count
-
- # Try to load cache
- loaded_cache = ctx.request_context.lifespan_context.settings.load_cache()
- if loaded_cache:
- print(f"Existing cache found and loaded successfully")
- code_content_cache.update(loaded_cache)
-
- # Get search capabilities info
- search_tool = ctx.request_context.lifespan_context.settings.get_preferred_search_tool()
-
- if search_tool is None:
- search_info = " Basic search available."
- else:
- search_info = f" Advanced search enabled ({search_tool.name})."
-
- return f"Project path set to: {abs_path}. Loaded existing index with {file_count} files.{search_info}"
- else:
- print(f"No existing index found, creating new index...")
-
- # If no existing index, create a new one
- file_count = _index_project(abs_path)
- ctx.request_context.lifespan_context.file_count = file_count
-
- # Save the new index
- ctx.request_context.lifespan_context.settings.save_index(file_index)
-
- # Save project config
- config = {
- "base_path": abs_path,
- "supported_extensions": supported_extensions,
- "last_indexed": ctx.request_context.lifespan_context.settings.load_config().get('last_indexed', None)
- }
- ctx.request_context.lifespan_context.settings.save_config(config)
-
- # Get search capabilities info (this will trigger lazy detection)
- search_tool = ctx.request_context.lifespan_context.settings.get_preferred_search_tool()
-
- if search_tool is None:
- search_info = " Basic search available."
- else:
- search_info = f" Advanced search enabled ({search_tool.name})."
-
- return f"Project path set to: {abs_path}. Indexed {file_count} files.{search_info}"
- except Exception as e:
- return f"Error setting project path: {e}"
-
-@mcp.tool()
-def search_code_advanced(
- pattern: str,
- ctx: Context,
- case_sensitive: bool = True,
- context_lines: int = 0,
- file_pattern: Optional[str] = None,
- fuzzy: bool = False
-) -> Dict[str, Any]:
- """
- Search for a code pattern in the project using an advanced, fast tool.
-
- This tool automatically selects the best available command-line search tool
- (like ugrep, ripgrep, ag, or grep) for maximum performance.
-
- Args:
- pattern: The search pattern (can be a regex if fuzzy=True).
- case_sensitive: Whether the search should be case-sensitive.
- 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").
- fuzzy: If True, treats the pattern as a regular expression.
- If False, performs a literal/fixed-string search.
- For 'ugrep', this enables fuzzy matching features.
-
- Returns:
- A dictionary containing the search results or an error message.
- """
- base_path = ctx.request_context.lifespan_context.base_path
- if not base_path:
- return {"error": "Project path not set. Please use set_project_path first."}
-
- settings = ctx.request_context.lifespan_context.settings
- strategy = settings.get_preferred_search_tool()
-
- if not strategy:
- return {"error": "No search strategies available. This is unexpected."}
-
- print(f"Using search strategy: {strategy.name}")
-
- try:
- results = strategy.search(
- pattern=pattern,
- base_path=base_path,
- case_sensitive=case_sensitive,
- context_lines=context_lines,
- file_pattern=file_pattern,
- fuzzy=fuzzy
- )
- return {"results": results}
- except Exception as e:
- return {"error": f"Search failed using '{strategy.name}': {e}"}
-
-@mcp.tool()
-def find_files(pattern: str, ctx: Context) -> List[str]:
- """Find files in the project matching a specific glob pattern."""
- base_path = ctx.request_context.lifespan_context.base_path
-
- # Check if base_path is set
- if not base_path:
- return ["Error: Project path not set. Please use set_project_path to set a project directory first."]
-
- # Check if we need to index the project
- if not file_index:
- _index_project(base_path)
- ctx.request_context.lifespan_context.file_count = _count_files(file_index)
- ctx.request_context.lifespan_context.settings.save_index(file_index)
-
- matching_files = []
- for file_path, _info in _get_all_files(file_index):
- if fnmatch.fnmatch(file_path, pattern):
- matching_files.append(file_path)
-
- return matching_files
-
-@mcp.tool()
-def get_file_summary(file_path: str, ctx: Context) -> Dict[str, Any]:
- """
- Get a summary of a specific file, including:
- - Line count
- - Function/class definitions (for supported languages)
- - Import statements
- - Basic complexity metrics
- """
- base_path = ctx.request_context.lifespan_context.base_path
-
- # Check if base_path is set
- if not base_path:
- return {"error": "Project path not set. Please use set_project_path to set a project directory first."}
-
- # Normalize the file path
- norm_path = os.path.normpath(file_path)
- if norm_path.startswith('..'):
- return {"error": f"Invalid file path: {file_path}"}
-
- full_path = os.path.join(base_path, norm_path)
-
- try:
- # Get file content
- if norm_path in code_content_cache:
- content = code_content_cache[norm_path]
- else:
- with open(full_path, 'r', encoding='utf-8') as f:
- content = f.read()
- code_content_cache[norm_path] = content
- # Save the updated cache
- ctx.request_context.lifespan_context.settings.save_cache(code_content_cache)
-
- # Basic file info
- lines = content.splitlines()
- line_count = len(lines)
-
- # File extension for language-specific analysis
- _, ext = os.path.splitext(norm_path)
-
- summary = {
- "file_path": norm_path,
- "line_count": line_count,
- "size_bytes": os.path.getsize(full_path),
- "extension": ext,
- }
-
- # Language-specific analysis
- if ext == '.py':
- # Python analysis
- imports = []
- classes = []
- functions = []
-
- for i, line in enumerate(lines):
- line = line.strip()
-
- # Check for imports
- if line.startswith('import ') or line.startswith('from '):
- imports.append(line)
-
- # Check for class definitions
- if line.startswith('class '):
- classes.append({
- "line": i + 1,
- "name": line.replace('class ', '').split('(')[0].split(':')[0].strip()
- })
-
- # Check for function definitions
- if line.startswith('def '):
- functions.append({
- "line": i + 1,
- "name": line.replace('def ', '').split('(')[0].strip()
- })
-
- summary.update({
- "imports": imports,
- "classes": classes,
- "functions": functions,
- "import_count": len(imports),
- "class_count": len(classes),
- "function_count": len(functions),
- })
-
- elif ext in ['.js', '.jsx', '.ts', '.tsx']:
- # JavaScript/TypeScript analysis
- imports = []
- classes = []
- functions = []
-
- for i, line in enumerate(lines):
- line = line.strip()
-
- # Check for imports
- if line.startswith('import ') or line.startswith('require('):
- imports.append(line)
-
- # Check for class definitions
- if line.startswith('class ') or 'class ' in line:
- class_name = ""
- if 'class ' in line:
- parts = line.split('class ')[1]
- class_name = parts.split(' ')[0].split('{')[0].split('extends')[0].strip()
- classes.append({
- "line": i + 1,
- "name": class_name
- })
-
- # Check for function definitions
- if 'function ' in line or '=>' in line:
- functions.append({
- "line": i + 1,
- "content": line
- })
-
- summary.update({
- "imports": imports,
- "classes": classes,
- "functions": functions,
- "import_count": len(imports),
- "class_count": len(classes),
- "function_count": len(functions),
- })
-
- return summary
- except Exception as e:
- return {"error": f"Error analyzing file: {e}"}
-
-@mcp.tool()
-def refresh_index(ctx: Context) -> str:
- """Refresh the project index."""
- base_path = ctx.request_context.lifespan_context.base_path
-
- # Check if base_path is set
- if not base_path:
- return "Error: Project path not set. Please use set_project_path to set a project directory first."
-
- # Clear existing index
- global file_index
- file_index.clear()
-
- # Re-index the project
- file_count = _index_project(base_path)
- ctx.request_context.lifespan_context.file_count = file_count
-
- # Save the updated index
- ctx.request_context.lifespan_context.settings.save_index(file_index)
-
- # Update the last indexed timestamp in config
- config = ctx.request_context.lifespan_context.settings.load_config()
- ctx.request_context.lifespan_context.settings.save_config({
- **config,
- 'last_indexed': ctx.request_context.lifespan_context.settings._get_timestamp()
- })
-
- return f"Project re-indexed. Found {file_count} files."
-
-@mcp.tool()
-def get_settings_info(ctx: Context) -> Dict[str, Any]:
- """Get information about the project settings."""
- base_path = ctx.request_context.lifespan_context.base_path
-
- # Check if base_path is set
- if not base_path:
- # Even if base_path is not set, we can still show the temp directory
- temp_dir = os.path.join(tempfile.gettempdir(), SETTINGS_DIR)
- return {
- "status": "not_configured",
- "message": "Project path not set. Please use set_project_path to set a project directory first.",
- "temp_directory": temp_dir,
- "temp_directory_exists": os.path.exists(temp_dir)
- }
-
- settings = ctx.request_context.lifespan_context.settings
-
- # Get config
- config = settings.load_config()
-
- # Get stats
- stats = settings.get_stats()
-
- # Get temp directory
- temp_dir = os.path.join(tempfile.gettempdir(), SETTINGS_DIR)
-
- return {
- "settings_directory": settings.settings_path,
- "temp_directory": temp_dir,
- "temp_directory_exists": os.path.exists(temp_dir),
- "config": config,
- "stats": stats,
- "exists": os.path.exists(settings.settings_path)
- }
-
-@mcp.tool()
-def create_temp_directory() -> Dict[str, Any]:
- """Create the temporary directory used for storing index data."""
- temp_dir = os.path.join(tempfile.gettempdir(), SETTINGS_DIR)
-
- result = {
- "temp_directory": temp_dir,
- "existed_before": os.path.exists(temp_dir),
- }
-
- try:
- # Use ProjectSettings to handle directory creation consistently
- temp_settings = ProjectSettings("", skip_load=True)
-
- result["created"] = not result["existed_before"]
- result["exists_now"] = os.path.exists(temp_dir)
- result["is_directory"] = os.path.isdir(temp_dir)
- except Exception as e:
- result["error"] = str(e)
-
- return result
-
-@mcp.tool()
-def check_temp_directory() -> Dict[str, Any]:
- """Check the temporary directory used for storing index data."""
- temp_dir = os.path.join(tempfile.gettempdir(), SETTINGS_DIR)
-
- result = {
- "temp_directory": temp_dir,
- "exists": os.path.exists(temp_dir),
- "is_directory": os.path.isdir(temp_dir) if os.path.exists(temp_dir) else False,
- "temp_root": tempfile.gettempdir(),
- }
-
- # If the directory exists, list its contents
- if result["exists"] and result["is_directory"]:
- try:
- contents = os.listdir(temp_dir)
- result["contents"] = contents
- result["subdirectories"] = []
-
- # Check each subdirectory
- for item in contents:
- item_path = os.path.join(temp_dir, item)
- if os.path.isdir(item_path):
- subdir_info = {
- "name": item,
- "path": item_path,
- "contents": os.listdir(item_path) if os.path.exists(item_path) else []
- }
- result["subdirectories"].append(subdir_info)
- except Exception as e:
- result["error"] = str(e)
-
- return result
-
-@mcp.tool()
-def clear_settings(ctx: Context) -> str:
- """Clear all settings and cached data."""
- settings = ctx.request_context.lifespan_context.settings
- settings.clear()
- return "Project settings, index, and cache have been cleared."
-
-@mcp.tool()
-def refresh_search_tools(ctx: Context) -> str:
- """
- Manually re-detect the available command-line search tools on the system.
- This is useful if you have installed a new tool (like ripgrep) after starting the server.
- """
- settings = ctx.request_context.lifespan_context.settings
- settings.refresh_available_strategies()
-
- config = settings.get_search_tools_config()
-
- return f"Search tools refreshed. Available: {config['available_tools']}. Preferred: {config['preferred_tool']}."
-
-# ----- PROMPTS -----
-
-@mcp.prompt()
-def analyze_code(file_path: str = "", query: str = "") -> list[types.PromptMessage]:
- """Prompt for analyzing code in the project."""
- messages = [
- types.PromptMessage(role="user", content=types.TextContent(type="text", text=f"""I need you to analyze some code from my project.
-
-{f'Please analyze the file: {file_path}' if file_path else ''}
-{f'I want to understand: {query}' if query else ''}
-
-First, let me give you some context about the project structure. Then, I'll provide the code to analyze.
-""")),
- types.PromptMessage(role="assistant", content=types.TextContent(type="text", text="I'll help you analyze the code. Let me first examine the project structure to get a better understanding of the codebase."))
- ]
- return messages
-
-@mcp.prompt()
-def code_search(query: str = "") -> types.TextContent:
- """Prompt for searching code in the project."""
- search_text = f"\"query\"" if not query else f"\"{query}\""
- return types.TextContent(type="text", text=f"""I need to search through my codebase for {search_text}.
-
-Please help me find all occurrences of this query and explain what each match means in its context.
-Focus on the most relevant files and provide a brief explanation of how each match is used in the code.
-
-If there are too many results, prioritize the most important ones and summarize the patterns you see.""")
-
-@mcp.prompt()
-def set_project() -> list[types.PromptMessage]:
- """Prompt for setting the project path."""
- messages = [
- types.PromptMessage(role="user", content=types.TextContent(type="text", text="""
- I need to analyze code from a project, but I haven't set the project path yet. Please help me set up the project path and index the code.
-
- First, I need to specify which project directory to analyze.
- """)),
- types.PromptMessage(role="assistant", content=types.TextContent(type="text", text="""
- Before I can help you analyze any code, we need to set up the project path. This is a required first step.
-
- Please provide the full path to your project folder. For example:
- - Windows: "C:/Users/username/projects/my-project"
- - macOS/Linux: "/home/username/projects/my-project"
-
- Once you provide the path, I'll use the `set_project_path` tool to configure the code analyzer to work with your project.
- """))
- ]
- return messages
-
-# ----- HELPER FUNCTIONS -----
-
-def _index_project(base_path: str) -> int:
- """
- Create an index of the project files.
- Returns the number of files indexed.
- """
- file_count = 0
- file_index.clear()
-
- for root, dirs, files in os.walk(base_path):
- # Skip hidden directories and common build/dependency directories
- dirs[:] = [d for d in dirs if not d.startswith('.') and
- d not in ['node_modules', 'venv', '__pycache__', 'build', 'dist']]
-
- # Create relative path from base_path
- rel_path = os.path.relpath(root, base_path)
- current_dir = file_index
-
- # Skip the '.' directory (base_path itself)
- if rel_path != '.':
- # Split the path and navigate/create the tree
- path_parts = rel_path.replace('\\', '/').split('/')
- for part in path_parts:
- if part not in current_dir:
- current_dir[part] = {}
- current_dir = current_dir[part]
-
- # Add files to current directory
- for file in files:
- # Skip hidden files and files with unsupported extensions
- _, ext = os.path.splitext(file)
- if file.startswith('.') or ext not in supported_extensions:
- continue
-
- # Store file information
- file_path = os.path.join(rel_path, file).replace('\\', '/')
- if rel_path == '.':
- file_path = file
-
- current_dir[file] = {
- "type": "file",
- "path": file_path,
- "ext": ext
- }
- file_count += 1
-
- return file_count
-
-def _count_files(directory: Dict) -> int:
- """
- Count the number of files in the index.
- """
- count = 0
- for name, value in directory.items():
- if isinstance(value, dict):
- if "type" in value and value["type"] == "file":
- count += 1
- else:
- count += _count_files(value)
- return count
-
-def _get_all_files(directory: Dict, prefix: str = "") -> List[Tuple[str, Dict]]:
- """Recursively get all files from the index."""
- all_files = []
- for name, item in directory.items():
- current_path = os.path.join(prefix, name)
- if item['type'] == 'file':
- all_files.append((current_path, item))
- elif item['type'] == 'directory':
- all_files.extend(_get_all_files(item['children'], current_path))
- return all_files
-
-def main():
- """Main function to run the MCP server."""
- # Run the server. Tools are discovered automatically via decorators.
- mcp.run()
-
-if __name__ == '__main__':
- # Set path to project root
- sys.path.append(os.path.dirname(os.path.dirname(os.path.abspath(__file__))))
- main()
+"""
+Code Index MCP Server
+
+This MCP server allows LLMs to index, search, and analyze code from a project directory.
+It provides tools for file discovery, content retrieval, and code analysis.
+"""
+from contextlib import asynccontextmanager
+from dataclasses import dataclass
+from typing import AsyncIterator, Dict, List, Optional, Tuple, Any
+import os
+import pathlib
+import json
+import fnmatch
+import sys
+import tempfile
+import subprocess
+from mcp.server.fastmcp import FastMCP, Context, Image
+from mcp import types
+
+# Import the ProjectSettings class and constants - using relative import
+from .project_settings import ProjectSettings
+from .constants import SETTINGS_DIR
+
+# Create the MCP server
+mcp = FastMCP("CodeIndexer", dependencies=["pathlib"])
+
+# In-memory references (will be loaded from persistent storage)
+file_index = {}
+code_content_cache = {}
+supported_extensions = [
+ '.py', '.js', '.ts', '.jsx', '.tsx', '.java', '.c', '.cpp', '.h', '.hpp',
+ '.cs', '.go', '.rb', '.php', '.swift', '.kt', '.rs', '.scala', '.sh',
+ '.bash', '.html', '.css', '.scss', '.md', '.json', '.xml', '.yml', '.yaml', '.zig',
+ # Frontend frameworks
+ '.vue', '.svelte', '.mjs', '.cjs',
+ # Style languages
+ '.less', '.sass', '.stylus', '.styl',
+ # Template engines
+ '.hbs', '.handlebars', '.ejs', '.pug',
+ # Modern frontend
+ '.astro', '.mdx',
+ # Database and SQL
+ '.sql', '.ddl', '.dml', '.mysql', '.postgresql', '.psql', '.sqlite',
+ '.mssql', '.oracle', '.ora', '.db2',
+ # Database objects
+ '.proc', '.procedure', '.func', '.function', '.view', '.trigger', '.index',
+ # Database frameworks and tools
+ '.migration', '.seed', '.fixture', '.schema',
+ # NoSQL and modern databases
+ '.cql', '.cypher', '.sparql', '.gql',
+ # Database migration tools
+ '.liquibase', '.flyway'
+]
+
+@dataclass
+class CodeIndexerContext:
+ """Context for the Code Indexer MCP server."""
+ base_path: str
+ settings: ProjectSettings
+ file_count: int = 0
+
+@asynccontextmanager
+async def indexer_lifespan(server: FastMCP) -> AsyncIterator[CodeIndexerContext]:
+ """Manage the lifecycle of the Code Indexer MCP server."""
+ # Don't set a default path, user must explicitly set project path
+ base_path = "" # Empty string to indicate no path is set
+
+ print("Initializing Code Indexer MCP server...")
+
+ # Initialize settings manager with skip_load=True to skip loading files
+ settings = ProjectSettings(base_path, skip_load=True)
+
+ # Initialize context
+ context = CodeIndexerContext(
+ base_path=base_path,
+ settings=settings
+ )
+
+ # Initialize global variables
+ global file_index, code_content_cache
+
+ try:
+ print("Server ready. Waiting for user to set project path...")
+ # Provide context to the server
+ yield context
+ finally:
+ # Only save index and cache if project path has been set
+ if context.base_path and file_index:
+ print(f"Saving index for project: {context.base_path}")
+ settings.save_index(file_index)
+
+ if context.base_path and code_content_cache:
+ print(f"Saving cache for project: {context.base_path}")
+ settings.save_cache(code_content_cache)
+
+# Initialize the server with our lifespan manager
+mcp = FastMCP("CodeIndexer", lifespan=indexer_lifespan)
+
+# ----- RESOURCES -----
+
+@mcp.resource("config://code-indexer")
+def get_config() -> str:
+ """Get the current configuration of the Code Indexer."""
+ ctx = mcp.get_context()
+
+ # Get the base path from context
+ base_path = ctx.request_context.lifespan_context.base_path
+
+ # Check if base_path is set
+ if not base_path:
+ return json.dumps({
+ "status": "not_configured",
+ "message": "Project path not set. Please use set_project_path to set a project directory first.",
+ "supported_extensions": supported_extensions
+ }, indent=2)
+
+ # Get file count
+ file_count = ctx.request_context.lifespan_context.file_count
+
+ # Get settings stats
+ settings = ctx.request_context.lifespan_context.settings
+ settings_stats = settings.get_stats()
+
+ config = {
+ "base_path": base_path,
+ "supported_extensions": supported_extensions,
+ "file_count": file_count,
+ "settings_directory": settings.settings_path,
+ "settings_stats": settings_stats
+ }
+
+ return json.dumps(config, indent=2)
+
+@mcp.resource("files://{file_path}")
+def get_file_content(file_path: str) -> str:
+ """Get the content of a specific file."""
+ ctx = mcp.get_context()
+
+ # Get the base path from context
+ base_path = ctx.request_context.lifespan_context.base_path
+
+ # Check if base_path is set
+ if not base_path:
+ return "Error: Project path not set. Please use set_project_path to set a project directory first."
+
+ # Handle absolute paths (especially Windows paths starting with drive letters)
+ if os.path.isabs(file_path) or (len(file_path) > 1 and file_path[1] == ':'):
+ # Absolute paths are not allowed via this endpoint
+ return f"Error: Absolute file paths like '{file_path}' are not allowed. Please use paths relative to the project root."
+
+ # Normalize the file path
+ norm_path = os.path.normpath(file_path)
+
+ # Check for path traversal attempts
+ if "..\\" in norm_path or "../" in norm_path or norm_path.startswith(".."):
+ return f"Error: Invalid file path: {file_path} (directory traversal not allowed)"
+
+ # Construct the full path and verify it's within the project bounds
+ full_path = os.path.join(base_path, norm_path)
+ real_full_path = os.path.realpath(full_path)
+ real_base_path = os.path.realpath(base_path)
+
+ if not real_full_path.startswith(real_base_path):
+ return f"Error: Access denied. File path must be within project directory."
+
+ try:
+ with open(full_path, 'r', encoding='utf-8') as f:
+ content = f.read()
+
+ # Cache the content for faster retrieval later
+ code_content_cache[norm_path] = content
+
+ return content
+ except UnicodeDecodeError:
+ return f"Error: File {file_path} appears to be a binary file or uses unsupported encoding."
+ except Exception as e:
+ return f"Error reading file: {e}"
+
+@mcp.resource("structure://project")
+def get_project_structure() -> str:
+ """Get the structure of the project as a JSON tree."""
+ ctx = mcp.get_context()
+
+ # Get the base path from context
+ base_path = ctx.request_context.lifespan_context.base_path
+
+ # Check if base_path is set
+ if not base_path:
+ return json.dumps({
+ "status": "not_configured",
+ "message": "Project path not set. Please use set_project_path to set a project directory first."
+ }, indent=2)
+
+ # Check if we need to refresh the index
+ if not file_index:
+ _index_project(base_path)
+ # Update file count in context
+ ctx.request_context.lifespan_context.file_count = _count_files(file_index)
+ # Save updated index
+ ctx.request_context.lifespan_context.settings.save_index(file_index)
+
+ return json.dumps(file_index, indent=2)
+
+@mcp.resource("settings://stats")
+def get_settings_stats() -> str:
+ """Get statistics about the settings directory and files."""
+ ctx = mcp.get_context()
+
+ # Get settings manager from context
+ settings = ctx.request_context.lifespan_context.settings
+
+ # Get settings stats
+ stats = settings.get_stats()
+
+ return json.dumps(stats, indent=2)
+
+# ----- TOOLS -----
+
+@mcp.tool()
+def set_project_path(path: str, ctx: Context) -> str:
+ """Set the base project path for indexing."""
+ # Validate and normalize path
+ try:
+ norm_path = os.path.normpath(path)
+ abs_path = os.path.abspath(norm_path)
+
+ if not os.path.exists(abs_path):
+ return f"Error: Path does not exist: {abs_path}"
+
+ if not os.path.isdir(abs_path):
+ return f"Error: Path is not a directory: {abs_path}"
+
+ # Clear existing in-memory index and cache
+ global file_index, code_content_cache
+ file_index.clear()
+ code_content_cache.clear()
+
+ # Update the base path in context
+ ctx.request_context.lifespan_context.base_path = abs_path
+
+ # Create a new settings manager for the new path (don't skip loading files)
+ ctx.request_context.lifespan_context.settings = ProjectSettings(abs_path, skip_load=False)
+
+ # Print the settings path for debugging
+ settings_path = ctx.request_context.lifespan_context.settings.settings_path
+ print(f"Project settings path: {settings_path}")
+
+ # Try to load existing index and cache
+ print(f"Project path set to: {abs_path}")
+ print(f"Attempting to load existing index and cache...")
+
+ # Try to load index
+ loaded_index = None
+ try:
+ loaded_index = ctx.request_context.lifespan_context.settings.load_index()
+ except Exception as e:
+ print(f"Could not load existing index, it might be an old format. A new index will be created. Error: {e}")
+
+ if loaded_index:
+ print(f"Existing index found and loaded successfully")
+ file_index = loaded_index
+ file_count = _count_files(file_index)
+ ctx.request_context.lifespan_context.file_count = file_count
+
+ # Try to load cache
+ loaded_cache = ctx.request_context.lifespan_context.settings.load_cache()
+ if loaded_cache:
+ print(f"Existing cache found and loaded successfully")
+ code_content_cache.update(loaded_cache)
+
+ # Get search capabilities info
+ search_tool = ctx.request_context.lifespan_context.settings.get_preferred_search_tool()
+
+ if search_tool is None:
+ search_info = " Basic search available."
+ else:
+ search_info = f" Advanced search enabled ({search_tool.name})."
+
+ return f"Project path set to: {abs_path}. Loaded existing index with {file_count} files.{search_info}"
+ else:
+ print(f"No existing index found, creating new index...")
+
+ # If no existing index, create a new one
+ file_count = _index_project(abs_path)
+ ctx.request_context.lifespan_context.file_count = file_count
+
+ # Save the new index
+ ctx.request_context.lifespan_context.settings.save_index(file_index)
+
+ # Save project config
+ config = {
+ "base_path": abs_path,
+ "supported_extensions": supported_extensions,
+ "last_indexed": ctx.request_context.lifespan_context.settings.load_config().get('last_indexed', None)
+ }
+ ctx.request_context.lifespan_context.settings.save_config(config)
+
+ # Get search capabilities info (this will trigger lazy detection)
+ search_tool = ctx.request_context.lifespan_context.settings.get_preferred_search_tool()
+
+ if search_tool is None:
+ search_info = " Basic search available."
+ else:
+ search_info = f" Advanced search enabled ({search_tool.name})."
+
+ return f"Project path set to: {abs_path}. Indexed {file_count} files.{search_info}"
+ except Exception as e:
+ return f"Error setting project path: {e}"
+
+@mcp.tool()
+def search_code_advanced(
+ pattern: str,
+ ctx: Context,
+ case_sensitive: bool = True,
+ context_lines: int = 0,
+ file_pattern: Optional[str] = None,
+ fuzzy: bool = False
+) -> Dict[str, Any]:
+ """
+ Search for a code pattern in the project using an advanced, fast tool.
+
+ This tool automatically selects the best available command-line search tool
+ (like ugrep, ripgrep, ag, or grep) for maximum performance.
+
+ Args:
+ pattern: The search pattern (can be a regex if fuzzy=True).
+ case_sensitive: Whether the search should be case-sensitive.
+ 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").
+ IMPORTANT: Different tools handle file patterns differently:
+ - ugrep: Uses glob patterns (*.py, *.{js,ts})
+ - ripgrep: Uses glob patterns (*.py, *.{js,ts})
+ - ag (Silver Searcher): Converts globs to regex internally (may have limitations)
+ - grep: Basic pattern matching only
+ For best compatibility, use simple patterns like "*.py" or "*.js".
+ fuzzy: If True, enables fuzzy/approximate matching.
+ IMPORTANT: Fuzzy matching support varies by tool:
+ - ugrep: Native fuzzy search with --fuzzy flag
+ - ripgrep: Safe fuzzy patterns using word boundaries
+ - ag: Safe fuzzy patterns using word boundaries
+ - grep: Safe fuzzy patterns using word boundaries
+ For literal string searches, set fuzzy=False (recommended for exact matches).
+
+ Returns:
+ A dictionary containing the search results or an error message.
+
+ """
+ base_path = ctx.request_context.lifespan_context.base_path
+ if not base_path:
+ return {"error": "Project path not set. Please use set_project_path first."}
+
+ settings = ctx.request_context.lifespan_context.settings
+ strategy = settings.get_preferred_search_tool()
+
+ if not strategy:
+ return {"error": "No search strategies available. This is unexpected."}
+
+ print(f"Using search strategy: {strategy.name}")
+
+ try:
+ results = strategy.search(
+ pattern=pattern,
+ base_path=base_path,
+ case_sensitive=case_sensitive,
+ context_lines=context_lines,
+ file_pattern=file_pattern,
+ fuzzy=fuzzy
+ )
+ return {"results": results}
+ except Exception as e:
+ return {"error": f"Search failed using '{strategy.name}': {e}"}
+
+@mcp.tool()
+def find_files(pattern: str, ctx: Context) -> List[str]:
+ """Find files in the project matching a specific glob pattern."""
+ base_path = ctx.request_context.lifespan_context.base_path
+
+ # Check if base_path is set
+ if not base_path:
+ return ["Error: Project path not set. Please use set_project_path to set a project directory first."]
+
+ # Check if we need to index the project
+ if not file_index:
+ _index_project(base_path)
+ ctx.request_context.lifespan_context.file_count = _count_files(file_index)
+ ctx.request_context.lifespan_context.settings.save_index(file_index)
+
+ matching_files = []
+ for file_path, _info in _get_all_files(file_index):
+ if fnmatch.fnmatch(file_path, pattern):
+ matching_files.append(file_path)
+
+ return matching_files
+
+@mcp.tool()
+def get_file_summary(file_path: str, ctx: Context) -> Dict[str, Any]:
+ """
+ Get a summary of a specific file, including:
+ - Line count
+ - Function/class definitions (for supported languages)
+ - Import statements
+ - Basic complexity metrics
+ """
+ base_path = ctx.request_context.lifespan_context.base_path
+
+ # Check if base_path is set
+ if not base_path:
+ return {"error": "Project path not set. Please use set_project_path to set a project directory first."}
+
+ # Normalize the file path
+ norm_path = os.path.normpath(file_path)
+ if norm_path.startswith('..'):
+ return {"error": f"Invalid file path: {file_path}"}
+
+ full_path = os.path.join(base_path, norm_path)
+
+ try:
+ # Get file content
+ if norm_path in code_content_cache:
+ content = code_content_cache[norm_path]
+ else:
+ with open(full_path, 'r', encoding='utf-8') as f:
+ content = f.read()
+ code_content_cache[norm_path] = content
+ # Save the updated cache
+ ctx.request_context.lifespan_context.settings.save_cache(code_content_cache)
+
+ # Basic file info
+ lines = content.splitlines()
+ line_count = len(lines)
+
+ # File extension for language-specific analysis
+ _, ext = os.path.splitext(norm_path)
+
+ summary = {
+ "file_path": norm_path,
+ "line_count": line_count,
+ "size_bytes": os.path.getsize(full_path),
+ "extension": ext,
+ }
+
+ # Language-specific analysis
+ if ext == '.py':
+ # Python analysis
+ imports = []
+ classes = []
+ functions = []
+
+ for i, line in enumerate(lines):
+ line = line.strip()
+
+ # Check for imports
+ if line.startswith('import ') or line.startswith('from '):
+ imports.append(line)
+
+ # Check for class definitions
+ if line.startswith('class '):
+ classes.append({
+ "line": i + 1,
+ "name": line.replace('class ', '').split('(')[0].split(':')[0].strip()
+ })
+
+ # Check for function definitions
+ if line.startswith('def '):
+ functions.append({
+ "line": i + 1,
+ "name": line.replace('def ', '').split('(')[0].strip()
+ })
+
+ summary.update({
+ "imports": imports,
+ "classes": classes,
+ "functions": functions,
+ "import_count": len(imports),
+ "class_count": len(classes),
+ "function_count": len(functions),
+ })
+
+ elif ext in ['.js', '.jsx', '.ts', '.tsx']:
+ # JavaScript/TypeScript analysis
+ imports = []
+ classes = []
+ functions = []
+
+ for i, line in enumerate(lines):
+ line = line.strip()
+
+ # Check for imports
+ if line.startswith('import ') or line.startswith('require('):
+ imports.append(line)
+
+ # Check for class definitions
+ if line.startswith('class ') or 'class ' in line:
+ class_name = ""
+ if 'class ' in line:
+ parts = line.split('class ')[1]
+ class_name = parts.split(' ')[0].split('{')[0].split('extends')[0].strip()
+ classes.append({
+ "line": i + 1,
+ "name": class_name
+ })
+
+ # Check for function definitions
+ if 'function ' in line or '=>' in line:
+ functions.append({
+ "line": i + 1,
+ "content": line
+ })
+
+ summary.update({
+ "imports": imports,
+ "classes": classes,
+ "functions": functions,
+ "import_count": len(imports),
+ "class_count": len(classes),
+ "function_count": len(functions),
+ })
+
+ return summary
+ except Exception as e:
+ return {"error": f"Error analyzing file: {e}"}
+
+@mcp.tool()
+def refresh_index(ctx: Context) -> str:
+ """Refresh the project index."""
+ base_path = ctx.request_context.lifespan_context.base_path
+
+ # Check if base_path is set
+ if not base_path:
+ return "Error: Project path not set. Please use set_project_path to set a project directory first."
+
+ # Clear existing index
+ global file_index
+ file_index.clear()
+
+ # Re-index the project
+ file_count = _index_project(base_path)
+ ctx.request_context.lifespan_context.file_count = file_count
+
+ # Save the updated index
+ ctx.request_context.lifespan_context.settings.save_index(file_index)
+
+ # Update the last indexed timestamp in config
+ config = ctx.request_context.lifespan_context.settings.load_config()
+ ctx.request_context.lifespan_context.settings.save_config({
+ **config,
+ 'last_indexed': ctx.request_context.lifespan_context.settings._get_timestamp()
+ })
+
+ return f"Project re-indexed. Found {file_count} files."
+
+@mcp.tool()
+def get_settings_info(ctx: Context) -> Dict[str, Any]:
+ """Get information about the project settings."""
+ base_path = ctx.request_context.lifespan_context.base_path
+
+ # Check if base_path is set
+ if not base_path:
+ # Even if base_path is not set, we can still show the temp directory
+ temp_dir = os.path.join(tempfile.gettempdir(), SETTINGS_DIR)
+ return {
+ "status": "not_configured",
+ "message": "Project path not set. Please use set_project_path to set a project directory first.",
+ "temp_directory": temp_dir,
+ "temp_directory_exists": os.path.exists(temp_dir)
+ }
+
+ settings = ctx.request_context.lifespan_context.settings
+
+ # Get config
+ config = settings.load_config()
+
+ # Get stats
+ stats = settings.get_stats()
+
+ # Get temp directory
+ temp_dir = os.path.join(tempfile.gettempdir(), SETTINGS_DIR)
+
+ return {
+ "settings_directory": settings.settings_path,
+ "temp_directory": temp_dir,
+ "temp_directory_exists": os.path.exists(temp_dir),
+ "config": config,
+ "stats": stats,
+ "exists": os.path.exists(settings.settings_path)
+ }
+
+@mcp.tool()
+def create_temp_directory() -> Dict[str, Any]:
+ """Create the temporary directory used for storing index data."""
+ temp_dir = os.path.join(tempfile.gettempdir(), SETTINGS_DIR)
+
+ result = {
+ "temp_directory": temp_dir,
+ "existed_before": os.path.exists(temp_dir),
+ }
+
+ try:
+ # Use ProjectSettings to handle directory creation consistently
+ temp_settings = ProjectSettings("", skip_load=True)
+
+ result["created"] = not result["existed_before"]
+ result["exists_now"] = os.path.exists(temp_dir)
+ result["is_directory"] = os.path.isdir(temp_dir)
+ except Exception as e:
+ result["error"] = str(e)
+
+ return result
+
+@mcp.tool()
+def check_temp_directory() -> Dict[str, Any]:
+ """Check the temporary directory used for storing index data."""
+ temp_dir = os.path.join(tempfile.gettempdir(), SETTINGS_DIR)
+
+ result = {
+ "temp_directory": temp_dir,
+ "exists": os.path.exists(temp_dir),
+ "is_directory": os.path.isdir(temp_dir) if os.path.exists(temp_dir) else False,
+ "temp_root": tempfile.gettempdir(),
+ }
+
+ # If the directory exists, list its contents
+ if result["exists"] and result["is_directory"]:
+ try:
+ contents = os.listdir(temp_dir)
+ result["contents"] = contents
+ result["subdirectories"] = []
+
+ # Check each subdirectory
+ for item in contents:
+ item_path = os.path.join(temp_dir, item)
+ if os.path.isdir(item_path):
+ subdir_info = {
+ "name": item,
+ "path": item_path,
+ "contents": os.listdir(item_path) if os.path.exists(item_path) else []
+ }
+ result["subdirectories"].append(subdir_info)
+ except Exception as e:
+ result["error"] = str(e)
+
+ return result
+
+@mcp.tool()
+def clear_settings(ctx: Context) -> str:
+ """Clear all settings and cached data."""
+ settings = ctx.request_context.lifespan_context.settings
+ settings.clear()
+ return "Project settings, index, and cache have been cleared."
+
+@mcp.tool()
+def refresh_search_tools(ctx: Context) -> str:
+ """
+ Manually re-detect the available command-line search tools on the system.
+ This is useful if you have installed a new tool (like ripgrep) after starting the server.
+ """
+ settings = ctx.request_context.lifespan_context.settings
+ settings.refresh_available_strategies()
+
+ config = settings.get_search_tools_config()
+
+ return f"Search tools refreshed. Available: {config['available_tools']}. Preferred: {config['preferred_tool']}."
+
+# ----- PROMPTS -----
+
+@mcp.prompt()
+def analyze_code(file_path: str = "", query: str = "") -> list[types.PromptMessage]:
+ """Prompt for analyzing code in the project."""
+ messages = [
+ types.PromptMessage(role="user", content=types.TextContent(type="text", text=f"""I need you to analyze some code from my project.
+
+{f'Please analyze the file: {file_path}' if file_path else ''}
+{f'I want to understand: {query}' if query else ''}
+
+First, let me give you some context about the project structure. Then, I'll provide the code to analyze.
+""")),
+ types.PromptMessage(role="assistant", content=types.TextContent(type="text", text="I'll help you analyze the code. Let me first examine the project structure to get a better understanding of the codebase."))
+ ]
+ return messages
+
+@mcp.prompt()
+def code_search(query: str = "") -> types.TextContent:
+ """Prompt for searching code in the project."""
+ search_text = f"\"query\"" if not query else f"\"{query}\""
+ return types.TextContent(type="text", text=f"""I need to search through my codebase for {search_text}.
+
+Please help me find all occurrences of this query and explain what each match means in its context.
+Focus on the most relevant files and provide a brief explanation of how each match is used in the code.
+
+If there are too many results, prioritize the most important ones and summarize the patterns you see.""")
+
+@mcp.prompt()
+def set_project() -> list[types.PromptMessage]:
+ """Prompt for setting the project path."""
+ messages = [
+ types.PromptMessage(role="user", content=types.TextContent(type="text", text="""
+ I need to analyze code from a project, but I haven't set the project path yet. Please help me set up the project path and index the code.
+
+ First, I need to specify which project directory to analyze.
+ """)),
+ types.PromptMessage(role="assistant", content=types.TextContent(type="text", text="""
+ Before I can help you analyze any code, we need to set up the project path. This is a required first step.
+
+ Please provide the full path to your project folder. For example:
+ - Windows: "C:/Users/username/projects/my-project"
+ - macOS/Linux: "/home/username/projects/my-project"
+
+ Once you provide the path, I'll use the `set_project_path` tool to configure the code analyzer to work with your project.
+ """))
+ ]
+ return messages
+
+# ----- HELPER FUNCTIONS -----
+
+def _index_project(base_path: str) -> int:
+ """
+ Create an index of the project files.
+ Returns the number of files indexed.
+ """
+ file_count = 0
+ file_index.clear()
+
+ for root, dirs, files in os.walk(base_path):
+ # Skip hidden directories and common build/dependency directories
+ dirs[:] = [d for d in dirs if not d.startswith('.') and
+ d not in ['node_modules', 'venv', '__pycache__', 'build', 'dist']]
+
+ # Create relative path from base_path
+ rel_path = os.path.relpath(root, base_path)
+ current_dir = file_index
+
+ # Skip the '.' directory (base_path itself)
+ if rel_path != '.':
+ # Split the path and navigate/create the tree
+ path_parts = rel_path.replace('\\', '/').split('/')
+ for part in path_parts:
+ if part not in current_dir:
+ current_dir[part] = {"type": "directory", "children": {}}
+ current_dir = current_dir[part]["children"]
+
+ # Add files to current directory
+ for file in files:
+ # Skip hidden files and files with unsupported extensions
+ _, ext = os.path.splitext(file)
+ if file.startswith('.') or ext not in supported_extensions:
+ continue
+
+ # Store file information
+ file_path = os.path.join(rel_path, file).replace('\\', '/')
+ if rel_path == '.':
+ file_path = file
+
+ current_dir[file] = {
+ "type": "file",
+ "path": file_path,
+ "ext": ext
+ }
+ file_count += 1
+
+ return file_count
+
+def _count_files(directory: Dict) -> int:
+ """
+ Count the number of files in the index.
+ """
+ count = 0
+ for name, value in directory.items():
+ if isinstance(value, dict):
+ if value.get("type") == "file":
+ count += 1
+ elif value.get("type") == "directory":
+ count += _count_files(value.get("children", {}))
+ return count
+
+def _get_all_files(directory: Dict, prefix: str = "") -> List[Tuple[str, Dict]]:
+ """Recursively get all files from the index."""
+ all_files = []
+ for name, item in directory.items():
+ current_path = os.path.join(prefix, name).replace('\\', '/')
+ if item.get('type') == 'file':
+ all_files.append((current_path, item))
+ elif item.get('type') == 'directory':
+ all_files.extend(_get_all_files(item.get('children', {}), current_path))
+ return all_files
+
+
+def main():
+ """Main function to run the MCP server."""
+ # Run the server. Tools are discovered automatically via decorators.
+ mcp.run()
+
+if __name__ == '__main__':
+ # Set path to project root
+ sys.path.append(os.path.dirname(os.path.dirname(os.path.abspath(__file__))))
+ main()
diff --git a/uv.lock b/uv.lock
index c7c516e..02dc1a4 100644
--- a/uv.lock
+++ b/uv.lock
@@ -49,7 +49,7 @@ wheels = [
[[package]]
name = "code-index-mcp"
-version = "0.1.5"
+version = "0.3.0"
source = { editable = "." }
dependencies = [
{ name = "mcp" },