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 - -
- -[![MCP Server](https://img.shields.io/badge/MCP-Server-blue)](https://modelcontextprotocol.io) -[![Python](https://img.shields.io/badge/Python-3.8%2B-green)](https://www.python.org/) -[![License](https://img.shields.io/badge/License-MIT-yellow)](LICENSE) - -A Model Context Protocol server for code indexing, searching, and analysis. - -
- - - code-index-mcp MCP server - - -## 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 + +
+ +[![MCP Server](https://img.shields.io/badge/MCP-Server-blue)](https://modelcontextprotocol.io) +[![Python](https://img.shields.io/badge/Python-3.8%2B-green)](https://www.python.org/) +[![License](https://img.shields.io/badge/License-MIT-yellow)](LICENSE) + +A Model Context Protocol server for code indexing, searching, and analysis. + +
+ + + code-index-mcp MCP server + + +## 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" },